From 7bcc29c6a8f028d0a0a646ad560732c712500cf0 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 20 Jun 2013 09:18:24 +0200 Subject: [PATCH 001/650] Adding warning if user exceeds balance thresholds * Warn user if they exceeded the configured AP max thresholds for their account balance This will address #220. There does not seem a perfect solution for this but this should help. --- public/include/smarty_globals.inc.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 923b7263..6d41bc5c 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -72,6 +72,10 @@ if (@$_SESSION['USERDATA']['id']) { $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 your accounts balance. Please transfer some ' . $config['currency'] . "!", 'TYPE' => 'errormsg'); } // Make it available in Smarty From 06d85b1459608be29dc0c9ddb383e8b209a1111e Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 20 Jun 2013 13:43:02 +0200 Subject: [PATCH 002/650] fixing BASEPATH in tickerupdate --- cronjobs/tickerupdate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cronjobs/tickerupdate.php b/cronjobs/tickerupdate.php index 7850ade9..d3778575 100755 --- a/cronjobs/tickerupdate.php +++ b/cronjobs/tickerupdate.php @@ -23,7 +23,7 @@ limitations under the License. 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'])) { From 27babf93c1a12fb5cbca65aa74c391e4189e5a23 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 20 Jun 2013 16:44:01 +0200 Subject: [PATCH 003/650] missed Bonus transaction type, re-added --- sql/mmcfe_ng_structure.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sql/mmcfe_ng_structure.sql b/sql/mmcfe_ng_structure.sql index 614d3f23..bfba666f 100644 --- a/sql/mmcfe_ng_structure.sql +++ b/sql/mmcfe_ng_structure.sql @@ -112,7 +112,7 @@ CREATE TABLE IF NOT EXISTS `shares_archive` ( PRIMARY KEY (`id`), UNIQUE KEY `share_id` (`share_id`), KEY `time` (`time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Archive shares for potential later debugging purposes'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Archive shares for potential later debugging purposes'; CREATE TABLE IF NOT EXISTS `statistics_shares` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, @@ -123,12 +123,12 @@ CREATE TABLE IF NOT EXISTS `statistics_shares` ( PRIMARY KEY (`id`), KEY `account_id` (`account_id`), KEY `block_id` (`block_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `transactions` ( `id` int(255) NOT NULL AUTO_INCREMENT, `account_id` int(255) unsigned NOT NULL, - `type` enum('Credit','Debit_MP','Debit_AP','Donation','Fee','Orphan_Credit','Orphan_Fee','Orphan_Donation','Credit_PPS','Fee_PPS','Donation_PPS','TXFee') DEFAULT NULL, + `type` enum('Credit','Debit_MP','Debit_AP','Donation','Fee','Orphan_Credit','Orphan_Fee','Orphan_Donation','Credit_PPS','Fee_PPS','Donation_PPS','TXFee','Bonus') DEFAULT NULL, `coin_address` varchar(255) DEFAULT NULL, `amount` double DEFAULT '0', `block_id` int(255) DEFAULT NULL, From 5eaf4cb3505ef22ebfec6dcfb8ff768a2016eadf Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 20 Jun 2013 16:45:14 +0200 Subject: [PATCH 004/650] Adding Bonus to issue-203 upgrade SQL --- sql/issue_203_transactions_upgrade.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/issue_203_transactions_upgrade.sql b/sql/issue_203_transactions_upgrade.sql index 9c824b89..13d19701 100644 --- a/sql/issue_203_transactions_upgrade.sql +++ b/sql/issue_203_transactions_upgrade.sql @@ -1 +1 @@ -ALTER TABLE `transactions` CHANGE `type` `type` ENUM( 'Credit', 'Debit_MP', 'Debit_AP', 'Donation', 'Fee', 'Orphan_Credit', 'Orphan_Fee', 'Orphan_Donation', 'Credit_PPS', 'Fee_PPS', 'Donation_PPS', 'TXFee' ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ; +ALTER TABLE `transactions` CHANGE `type` `type` ENUM( 'Credit', 'Debit_MP', 'Debit_AP', 'Donation', 'Fee', 'Orphan_Credit', 'Orphan_Fee', 'Orphan_Donation', 'Credit_PPS', 'Fee_PPS', 'Donation_PPS', 'TXFee', 'Bonus') CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ; From 595e0e2dba03eb47591469d247bf301565f1782c Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 20 Jun 2013 16:53:03 +0200 Subject: [PATCH 005/650] Update README.md Added new donor --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 817aab98..d2dbe90b 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ 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) Requirements ============ From 5221d7a7447a5dd980a2149dd10e2ca1acb01367 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 08:48:38 +0200 Subject: [PATCH 006/650] adding 3rd party lib folder --- public/include/{ => lib}/recaptchalib.php | 0 public/include/pages/register.inc.php | 2 +- public/include/pages/register/register.inc.php | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename public/include/{ => lib}/recaptchalib.php (100%) 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/pages/register.inc.php b/public/include/pages/register.inc.php index d0a1d713..29e1587c 100644 --- a/public/include/pages/register.inc.php +++ b/public/include/pages/register.inc.php @@ -8,7 +8,7 @@ if (!$config['website']['registration']) { $smarty->assign("CONTENT", "disabled.tpl"); } else { if ($config['recaptcha']['enabled']) { - require_once(INCLUDE_DIR . '/recaptchalib.php'); + require_once(INCLUDE_DIR . '/lib/recaptchalib.php'); $smarty->assign("RECAPTCHA", recaptcha_get_html($config['recaptcha']['public_key'])); } // Tempalte specifics diff --git a/public/include/pages/register/register.inc.php b/public/include/pages/register/register.inc.php index ce41630e..bf741424 100644 --- a/public/include/pages/register/register.inc.php +++ b/public/include/pages/register/register.inc.php @@ -4,7 +4,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); if ($config['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'], $_SERVER["REMOTE_ADDR"], From e4e88e5226db976fb28724453647cfc6f1e72714 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 11:16:02 +0200 Subject: [PATCH 007/650] Adding custom news posts via admin panel * Adding dynamic news posts from DB * Support Adding, Editing, Activating, Deactivating through admin panel * Display all active posts on news page * Implemented Markdown Library by Michelf Fixes #61 --- public/include/autoloader.inc.php | 3 + public/include/classes/base.class.php | 81 + public/include/classes/news.class.php | 102 + public/include/lib/Michelf/Markdown.php | 3094 +++++++++++++++++ public/include/lib/Michelf/MarkdownExtra.php | 40 + public/include/pages/admin/news.inc.php | 37 + public/include/pages/admin/news_edit.inc.php | 21 + public/include/pages/news.inc.php | 14 +- public/templates/mmcFE/admin/news/default.tpl | 38 + .../mmcFE/admin/news_edit/default.tpl | 32 + public/templates/mmcFE/global/navigation.tpl | 1 + public/templates/mmcFE/news/default.tpl | 23 +- 12 files changed, 3466 insertions(+), 20 deletions(-) create mode 100644 public/include/classes/base.class.php create mode 100644 public/include/classes/news.class.php create mode 100644 public/include/lib/Michelf/Markdown.php create mode 100644 public/include/lib/Michelf/MarkdownExtra.php create mode 100644 public/include/pages/admin/news.inc.php create mode 100644 public/include/pages/admin/news_edit.inc.php create mode 100644 public/templates/mmcFE/admin/news/default.tpl create mode 100644 public/templates/mmcFE/admin/news_edit/default.tpl diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index a78d4a62..23ab4556 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -7,6 +7,8 @@ require_once(CLASS_DIR . '/bitcoinwrapper.class.php'); require_once(INCLUDE_DIR . '/database.inc.php'); require_once(INCLUDE_DIR . '/smarty.inc.php'); // Load classes that need the above as dependencies +require_once(CLASS_DIR . '/base.class.php'); +require_once(CLASS_DIR . '/news.class.php'); require_once(CLASS_DIR . '/block.class.php'); require_once(CLASS_DIR . '/user.class.php'); require_once(CLASS_DIR . '/share.class.php'); @@ -16,3 +18,4 @@ 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(INCLUDE_DIR . '/lib/Michelf/Markdown.php'); diff --git a/public/include/classes/base.class.php b/public/include/classes/base.class.php new file mode 100644 index 00000000..6b3c2389 --- /dev/null +++ b/public/include/classes/base.class.php @@ -0,0 +1,81 @@ +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; + } + + /** + * 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; + } +} +?> diff --git a/public/include/classes/news.class.php b/public/include/classes/news.class.php new file mode 100644 index 00000000..5e90ad84 --- /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 * FROM $this->table WHERE active = 1"); + 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 * FROM $this->table"); + 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); + +?> 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/pages/admin/news.inc.php b/public/include/pages/admin/news.inc.php new file mode 100644 index 00000000..ef7fb3dc --- /dev/null +++ b/public/include/pages/admin/news.inc.php @@ -0,0 +1,37 @@ +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..7e6a6730 --- /dev/null +++ b/public/include/pages/admin/news_edit.inc.php @@ -0,0 +1,21 @@ +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/news.inc.php b/public/include/pages/news.inc.php index aecab054..38e1022d 100644 --- a/public/include/pages/news.inc.php +++ b/public/include/pages/news.inc.php @@ -1,9 +1,19 @@ getAllActive(); +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/templates/mmcFE/admin/news/default.tpl b/public/templates/mmcFE/admin/news/default.tpl new file mode 100644 index 00000000..fb3f3ced --- /dev/null +++ b/public/templates/mmcFE/admin/news/default.tpl @@ -0,0 +1,38 @@ +{include file="global/block_header.tpl" BLOCK_HEADER="News Posts"} +{include file="global/block_header.tpl" BLOCK_HEADER="Add News Post"} +
    + + + + + + + + + + + + + +
    Header
    Content
    +
    +{include file="global/block_footer.tpl"} +{section name=news loop=$NEWS} +{include + file="global/block_header.tpl" + BLOCK_HEADER="{$NEWS[news].header}" + BUTTONS=" + Edit  + Delete + " +} +{if $NEWS[news].active == 0}This post is inactive

    {/if} +{$NEWS[news].content} +
    + + + +
    +{include file="global/block_footer.tpl"} +{/section} +{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/admin/news_edit/default.tpl b/public/templates/mmcFE/admin/news_edit/default.tpl new file mode 100644 index 00000000..467d4a6c --- /dev/null +++ b/public/templates/mmcFE/admin/news_edit/default.tpl @@ -0,0 +1,32 @@ +{include file="global/block_header.tpl" BLOCK_HEADER="Edit news entry #{$NEWS.id}"} +
    + + + + + + + + + + + + + + + + + +
    + Active + + + +
    + Header +
    + Content +
    + +
    +{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index c53f61c7..f9a23f08 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -15,6 +15,7 @@ {/if} diff --git a/public/templates/mmcFE/news/default.tpl b/public/templates/mmcFE/news/default.tpl index 2d7b82b1..d4c65d27 100644 --- a/public/templates/mmcFE/news/default.tpl +++ b/public/templates/mmcFE/news/default.tpl @@ -1,18 +1,5 @@ -
    -
    -
    -
    -

    News

    -
    - -
    - -

    - - This is an experimental pool for now. Please use it at your own risk. We haven't had any issues with it so far but please keep us posted if you do have problems that could be solved by the pool owner. - -

    -
    -
    -
    -
    +{section name=news loop=$NEWS} + {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header}"} + {$NEWS[news].content} + {include file="global/block_footer.tpl"} +{/section} From 246789b0ed6a774e3f2765184eb2dff0e4c356f4 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 11:19:47 +0200 Subject: [PATCH 008/650] Adding new full and upgrade structures --- sql/issue_61_news_table.sql | 21 +++++++++++++++++++++ sql/mmcfe_ng_structure.sql | 15 ++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 sql/issue_61_news_table.sql diff --git a/sql/issue_61_news_table.sql b/sql/issue_61_news_table.sql new file mode 100644 index 00000000..70b26331 --- /dev/null +++ b/sql/issue_61_news_table.sql @@ -0,0 +1,21 @@ +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; + + +CREATE TABLE IF NOT EXISTS `news` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `account_id` int(10) unsigned NOT NULL, + `header` varchar(255) NOT NULL, + `content` text NOT NULL, + `active` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/sql/mmcfe_ng_structure.sql b/sql/mmcfe_ng_structure.sql index bfba666f..b8ab2c22 100644 --- a/sql/mmcfe_ng_structure.sql +++ b/sql/mmcfe_ng_structure.sql @@ -44,7 +44,16 @@ CREATE TABLE IF NOT EXISTS `blocks` ( PRIMARY KEY (`id`), UNIQUE KEY `height` (`height`,`blockhash`), KEY `time` (`time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Discovered blocks persisted from Litecoin Service'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Discovered blocks persisted from Litecoin Service'; + +CREATE TABLE IF NOT EXISTS `news` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `account_id` int(10) unsigned NOT NULL, + `header` varchar(255) NOT NULL, + `content` text NOT NULL, + `active` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `notifications` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, @@ -99,7 +108,7 @@ CREATE TABLE IF NOT EXISTS `shares` ( KEY `upstream_result` (`upstream_result`), KEY `our_result` (`our_result`), KEY `username` (`username`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `shares_archive` ( `id` int(255) unsigned NOT NULL AUTO_INCREMENT, @@ -137,7 +146,7 @@ CREATE TABLE IF NOT EXISTS `transactions` ( KEY `block_id` (`block_id`), KEY `account_id` (`account_id`), KEY `type` (`type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; From 84ababe9f72e16b2bb9616b6f1b1f8090f3fc956 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 12:11:13 +0200 Subject: [PATCH 009/650] Adding support for post time and author * Added new SQL upgrade and structure * Added post time and author to admin panel * Added post time and author to news list Fixes #226 --- public/include/autoloader.inc.php | 2 +- public/include/classes/news.class.php | 6 +++--- public/templates/mmcFE/admin/news/default.tpl | 2 +- public/templates/mmcFE/news/default.tpl | 2 +- sql/issue_226_news_upgrade.sql | 1 + sql/mmcfe_ng_structure.sql | 1 + 6 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 sql/issue_226_news_upgrade.sql diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index 23ab4556..51b9b815 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -8,7 +8,6 @@ require_once(INCLUDE_DIR . '/database.inc.php'); require_once(INCLUDE_DIR . '/smarty.inc.php'); // Load classes that need the above as dependencies require_once(CLASS_DIR . '/base.class.php'); -require_once(CLASS_DIR . '/news.class.php'); require_once(CLASS_DIR . '/block.class.php'); require_once(CLASS_DIR . '/user.class.php'); require_once(CLASS_DIR . '/share.class.php'); @@ -18,4 +17,5 @@ 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(INCLUDE_DIR . '/lib/Michelf/Markdown.php'); diff --git a/public/include/classes/news.class.php b/public/include/classes/news.class.php index 5e90ad84..61762a27 100644 --- a/public/include/classes/news.class.php +++ b/public/include/classes/news.class.php @@ -23,7 +23,7 @@ class News extends Base { **/ public function getAllActive() { $this->debug->append("STA " . __METHOD__, 4); - $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE active = 1"); + $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"); if ($stmt && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); // Catchall @@ -35,7 +35,7 @@ class News extends Base { **/ public function getAll() { $this->debug->append("STA " . __METHOD__, 4); - $stmt = $this->mysqli->prepare("SELECT * FROM $this->table"); + $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"); if ($stmt && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); // Catchall @@ -98,5 +98,5 @@ class News extends Base { $news = new News(); $news->setDebug($debug); $news->setMysql($mysqli); - +$news->setUser($user); ?> diff --git a/public/templates/mmcFE/admin/news/default.tpl b/public/templates/mmcFE/admin/news/default.tpl index fb3f3ced..7bd8c198 100644 --- a/public/templates/mmcFE/admin/news/default.tpl +++ b/public/templates/mmcFE/admin/news/default.tpl @@ -20,7 +20,7 @@ {section name=news loop=$NEWS} {include file="global/block_header.tpl" - BLOCK_HEADER="{$NEWS[news].header}" + BLOCK_HEADER="{$NEWS[news].header} posted {$NEWS[news].time} by {$NEWS[news].author}" BUTTONS=" Edit  Delete diff --git a/public/templates/mmcFE/news/default.tpl b/public/templates/mmcFE/news/default.tpl index d4c65d27..a54859db 100644 --- a/public/templates/mmcFE/news/default.tpl +++ b/public/templates/mmcFE/news/default.tpl @@ -1,5 +1,5 @@ {section name=news loop=$NEWS} - {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header}"} + {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header} posted {$NEWS[news].time} by {$NEWS[news].author}"} {$NEWS[news].content} {include file="global/block_footer.tpl"} {/section} diff --git a/sql/issue_226_news_upgrade.sql b/sql/issue_226_news_upgrade.sql new file mode 100644 index 00000000..dabc48bb --- /dev/null +++ b/sql/issue_226_news_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE `news` ADD `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `content` ; diff --git a/sql/mmcfe_ng_structure.sql b/sql/mmcfe_ng_structure.sql index b8ab2c22..cb39ed51 100644 --- a/sql/mmcfe_ng_structure.sql +++ b/sql/mmcfe_ng_structure.sql @@ -51,6 +51,7 @@ CREATE TABLE IF NOT EXISTS `news` ( `account_id` int(10) unsigned NOT NULL, `header` varchar(255) NOT NULL, `content` text NOT NULL, + `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `active` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; From 2e566f6e9728792a3d9f13612ab1b45e51c2a9a2 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 16:30:08 +0200 Subject: [PATCH 010/650] Only use valid shares for worker status Fixes #229 --- public/include/classes/worker.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/include/classes/worker.class.php b/public/include/classes/worker.class.php index 6a9f6524..6cac9827 100644 --- a/public/include/classes/worker.class.php +++ b/public/include/classes/worker.class.php @@ -108,8 +108,8 @@ 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 + ( SELECT SIGN(COUNT(id)) FROM " . $this->share->getTableName() . " WHERE our_result = 'Y' AND 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 our_result = 'Y' AND username = $this->table.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS hashrate FROM $this->table WHERE account_id = ?"); if ($this->checkStmt($stmt) && $stmt->bind_param('i', $account_id) && $stmt->execute() && $result = $stmt->get_result()) From 4eaa2c8d37c583e66d33d53b04ff3ad9f3fc01b0 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 20:21:27 +0200 Subject: [PATCH 011/650] hotfix for news page accessible by all users --- public/include/pages/admin/news.inc.php | 6 ++++++ public/include/pages/admin/news_edit.inc.php | 16 +++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/public/include/pages/admin/news.inc.php b/public/include/pages/admin/news.inc.php index ef7fb3dc..df0bb2fc 100644 --- a/public/include/pages/admin/news.inc.php +++ b/public/include/pages/admin/news.inc.php @@ -3,6 +3,12 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); +// Check user to ensure they are admin +if (!$user->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; diff --git a/public/include/pages/admin/news_edit.inc.php b/public/include/pages/admin/news_edit.inc.php index 7e6a6730..81cfcb56 100644 --- a/public/include/pages/admin/news_edit.inc.php +++ b/public/include/pages/admin/news_edit.inc.php @@ -3,15 +3,21 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); +// Check user to ensure they are admin +if (!$user->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'); - } + 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 From 706a3422eb08f6396f23c867e957b812ba6ce7f3 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 20:25:12 +0200 Subject: [PATCH 012/650] Making news page the default homepage Addresses #232 --- public/include/pages/home.inc.php | 14 ++++++++++++-- public/include/pages/news.inc.php | 19 ------------------- public/templates/mmcFE/home/default.tpl | 8 +++++--- public/templates/mmcFE/news/default.tpl | 5 ----- 4 files changed, 17 insertions(+), 29 deletions(-) delete mode 100644 public/include/pages/news.inc.php delete mode 100644 public/templates/mmcFE/news/default.tpl diff --git a/public/include/pages/home.inc.php b/public/include/pages/home.inc.php index aecab054..38e1022d 100644 --- a/public/include/pages/home.inc.php +++ b/public/include/pages/home.inc.php @@ -1,9 +1,19 @@ getAllActive(); +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/news.inc.php b/public/include/pages/news.inc.php deleted file mode 100644 index 38e1022d..00000000 --- a/public/include/pages/news.inc.php +++ /dev/null @@ -1,19 +0,0 @@ -getAllActive(); -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/templates/mmcFE/home/default.tpl b/public/templates/mmcFE/home/default.tpl index 5e4f403f..a54859db 100644 --- a/public/templates/mmcFE/home/default.tpl +++ b/public/templates/mmcFE/home/default.tpl @@ -1,3 +1,5 @@ -{include file="global/block_header.tpl" BLOCK_HEADER="ThePool Collective"} -

    Please head over to our pool page for more details. -{include file="global/block_footer.tpl"} +{section name=news loop=$NEWS} + {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header} posted {$NEWS[news].time} by {$NEWS[news].author}"} + {$NEWS[news].content} + {include file="global/block_footer.tpl"} +{/section} diff --git a/public/templates/mmcFE/news/default.tpl b/public/templates/mmcFE/news/default.tpl deleted file mode 100644 index a54859db..00000000 --- a/public/templates/mmcFE/news/default.tpl +++ /dev/null @@ -1,5 +0,0 @@ -{section name=news loop=$NEWS} - {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header} posted {$NEWS[news].time} by {$NEWS[news].author}"} - {$NEWS[news].content} - {include file="global/block_footer.tpl"} -{/section} From f39d9cca2d0b6fca321d81b8a3bb2e6d9cf81580 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 20:25:49 +0200 Subject: [PATCH 013/650] Removed News from navigation Fixes #232 --- public/templates/mmcFE/global/navigation.tpl | 1 - 1 file changed, 1 deletion(-) diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index f9a23f08..c53f61c7 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -15,7 +15,6 @@

    {/if} From b4c1c5b6672080d9d161c3188fe77912e2f88e68 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 20:28:29 +0200 Subject: [PATCH 014/650] removed wrong link, re-added admin, removed news --- public/templates/mmcFE/global/navigation.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index c53f61c7..407d5c6b 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -15,6 +15,7 @@ {/if} @@ -32,6 +33,5 @@
  • This Pool
  • -
  • News
  • {if $smarty.session.AUTHENTICATED|default == 1}
  • Logout
  • {else}
  • Register
  • {/if} From bfcf14a74c37b6fd6b9655e5151708b94ad5763c Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 20:29:21 +0200 Subject: [PATCH 015/650] Sort news by time, newest first Fixes #231 --- public/include/classes/news.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/include/classes/news.class.php b/public/include/classes/news.class.php index 61762a27..3f3f8631 100644 --- a/public/include/classes/news.class.php +++ b/public/include/classes/news.class.php @@ -23,7 +23,7 @@ class News extends Base { **/ 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"); + $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 @@ -35,7 +35,7 @@ class News extends Base { **/ 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"); + $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 From ff4cc88872c6867c481a3924d4037dcb6a9cc0a3 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 21 Jun 2013 20:32:04 +0200 Subject: [PATCH 016/650] Update README.md Added feature, News Posts --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index d2dbe90b..1bcc14af 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,7 @@ The following feature have been implemented so far: * Admin Panel * User Listing including statistics * Wallet information - * (Planned) News Posts - * (Planned) Pool Donations + * News Posts **NEW** * Notification system * IDLE Workers * New blocks found in pool From 2e7a4a8092a97031b5a6a5afeb7e2cc13eb6afcc Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 23 Jun 2013 20:12:34 +0200 Subject: [PATCH 017/650] Adding donors page to About dropdown Lists all donors, their donation setting and total donated amount. This will allow people to see who is contributing to the pool. Fixes #223 --- public/include/classes/transaction.class.php | 35 +++++++++++++++++-- public/include/pages/about/donors.inc.php | 11 ++++++ .../templates/mmcFE/about/donors/default.tpl | 27 ++++++++++++++ public/templates/mmcFE/global/navigation.tpl | 1 + 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 public/include/pages/about/donors.inc.php create mode 100644 public/templates/mmcFE/about/donors/default.tpl diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 38e1b429..038a30c5 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -9,11 +9,12 @@ class Transaction { private $table = 'transactions'; private $tableBlocks = 'blocks'; - public function __construct($debug, $mysqli, $config, $block) { + public function __construct($debug, $mysqli, $config, $block, $user) { $this->debug = $debug; $this->mysqli = $mysqli; $this->config = $config; $this->block = $block; + $this->user = $user; $this->debug->append("Instantiated Transaction class", 2); } @@ -116,6 +117,36 @@ class Transaction { return true; } + /** + * 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.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; + } + /** * Get total balance for all users locked in wallet * This includes any outstanding unconfirmed transactions! @@ -231,4 +262,4 @@ class Transaction { } } -$transaction = new Transaction($debug, $mysqli, $config, $block); +$transaction = new Transaction($debug, $mysqli, $config, $block, $user); 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/templates/mmcFE/about/donors/default.tpl b/public/templates/mmcFE/about/donors/default.tpl new file mode 100644 index 00000000..366ffbd3 --- /dev/null +++ b/public/templates/mmcFE/about/donors/default.tpl @@ -0,0 +1,27 @@ +{include file="global/block_header.tpl" BLOCK_HEADER="Pool Donors"} +
    +{include file="global/pagination.tpl"} + + + + + + + + + +{section name=donor loop=$DONORS} + + + + + +{sectionelse} + + + +{/section} + +
    Name%{$GLOBAL.config.currency} Total
    {$DONORS[donor].username}{$DONORS[donor].donate_percent}{$DONORS[donor].donation|number_format:"2"}
    No donors yet! Be the first one to donate!
    +
    +{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 407d5c6b..09901679 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -31,6 +31,7 @@
  • About
  • {if $smarty.session.AUTHENTICATED|default == 1}
  • Logout
  • {else}
  • Register
  • {/if} From 4113e05a100a0863f4178367c793cfdc763b2668 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 23 Jun 2013 20:41:43 +0200 Subject: [PATCH 018/650] Adding multi-API support This will allow users to change the API url, added coinchose as an example as pointed out by @vias79 . * tools class detects the API type * getPrice returns the price based on API URL parsed Fixes #236 --- cronjobs/tickerupdate.php | 10 +++-- public/include/classes/tools.class.php | 55 +++++++++++++++++++---- public/include/config/global.inc.dist.php | 12 ++++- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/cronjobs/tickerupdate.php b/cronjobs/tickerupdate.php index d3778575..48e3678d 100755 --- a/cronjobs/tickerupdate.php +++ b/cronjobs/tickerupdate.php @@ -26,11 +26,13 @@ require_once('shared.inc.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"); +verbose(" Price API Call ... "); +if ($price = $tools->getPrice()) { + verbose("found $price as price\n"); + if (!$setting->setValue('price', $price)) + verbose("unable to update value in settings table\n"); } else { - verbose("ERR Failed download JSON data from " . $config['price']['url'].$config['price']['target'] . "\n"); + verbose("failed to fetch API data: " . $tools->getError() . "\n"); } ?> diff --git a/public/include/classes/tools.class.php b/public/include/classes/tools.class.php index e97b3746..b160711d 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,49 @@ 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'; + } + $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; + } + // 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/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 29a46658..3d677814 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -1,7 +1,6 @@ '/ltc_usd/ticker', 'currency' => 'USD' // Used in ministats template ), +/** + * Another Example for API + 'price' => array( + 'url' => 'http://www.coinchoose.com', + 'target' => '/api.php', + 'currency' => 'BTC' + ), + **/ 'ap_threshold' => array( 'min' => 1, 'max' => 250 From d630329055768d671b293fc154bbe6eb10453a94 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 23 Jun 2013 20:58:42 +0200 Subject: [PATCH 019/650] Display error message to user on failed logins Warn a user with an error that there have been failed login attempts for their account. Login errors can be cleared by clicking on the URL in the message itself. Fixes #240 --- public/include/classes/user.class.php | 3 +-- public/include/pages/account/reset_failed.inc.php | 12 ++++++++++++ public/include/smarty_globals.inc.php | 2 ++ public/index.php | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 public/include/pages/account/reset_failed.inc.php diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 49de7b5f..c400ddc7 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -71,7 +71,7 @@ class User { $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); } @@ -111,7 +111,6 @@ class User { } if ( $this->checkUserPassword($username, $password)) { $this->createSession($username); - $this->setUserFailed($this->getUserId($username), 0); $this->setUserIp($this->getUserId($username), $_SERVER['REMOTE_ADDR']); return true; } 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..bce9b418 --- /dev/null +++ b/public/include/pages/account/reset_failed.inc.php @@ -0,0 +1,12 @@ +isAuthenticated()) { + // Reset failed login counter + $user->setUserFailed($_SESSION['USERDATA']['id'], 0); + header("Location: " . $_SERVER['HTTP_REFERER']); +} + +?> diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 6d41bc5c..beb24de8 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -76,6 +76,8 @@ if (@$_SESSION['USERDATA']['id']) { // Site-wide notifications, based on user events if ($aGlobal['userdata']['balance']['confirmed'] >= $config['ap_threshold']['max']) $_SESSION['POPUP'][] = array('CONTENT' => 'You have exceeded your accounts balance. Please transfer some ' . $config['currency'] . "!", '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'); } // Make it available in Smarty diff --git a/public/index.php b/public/index.php index e9100a57..80fd4680 100644 --- a/public/index.php +++ b/public/index.php @@ -78,7 +78,7 @@ $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'); -// Debguger +// Load debug information into template $debug->append("Loading debug information into template", 4); $smarty->assign('DebuggerInfo', $debug->getDebugInfo()); From 2095b09d69c1d82c99866358883119eb66553861 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 24 Jun 2013 10:36:59 +0200 Subject: [PATCH 020/650] More verbose output on cron updates * Added more verbosity to crons * Made the output look better on consoles * Added another error message to notifications class --- cronjobs/auto_payout.php | 13 +++-- cronjobs/notifications.php | 52 ++++++++++++------- cronjobs/statistics.php | 24 ++++++--- cronjobs/tickerupdate.php | 2 +- public/include/classes/notification.class.php | 4 +- 5 files changed, 65 insertions(+), 30 deletions(-) diff --git a/cronjobs/auto_payout.php b/cronjobs/auto_payout.php index 7e5609fc..aaf715e0 100755 --- a/cronjobs/auto_payout.php +++ b/cronjobs/auto_payout.php @@ -22,8 +22,10 @@ limitations under the License. // Include all settings and classes require_once('shared.inc.php'); +verbose("Running auto-payouts ..."); + if ($bitcoin->can_connect() !== true) { - verbose("Unable to connect to RPC server, exiting"); + verbose(" unable to connect to RPC server, exiting\n"); exit(1); } @@ -33,14 +35,17 @@ $setting->setValue('auto_payout_active', 1); // Fetch all users with setup AP $users = $user->getAllAutoPayout(); +// Quick summary +verbose(" found " . count($users) . " queued payout(s)\n"); + // Go through users and run transactions if (! empty($users)) { - verbose("UserID\tUsername\tBalance\tThreshold\tAddress\t\t\t\t\tStatus\n\n"); + verbose("\tUserID\tUsername\tBalance\tThreshold\tAddress\t\t\t\t\tStatus\n\n"); 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"); + verbose("\t" . $aUserData['id'] . "\t" . $aUserData['username'] . "\t" . $dBalance . "\t" . $aUserData['ap_threshold'] . "\t\t" . $aUserData['coin_address'] . "\t"); // Only run if balance meets threshold and can pay the potential transaction fee if ($dBalance > $aUserData['ap_threshold'] && $dBalance > $config['txfee']) { @@ -80,7 +85,7 @@ if (! empty($users)) { } } } else { - verbose("No user has configured their AP > 0\n"); + verbose(" no user has configured their AP > 0\n"); } // Mark this job as inactive diff --git a/cronjobs/notifications.php b/cronjobs/notifications.php index ec61002a..61a608ab 100755 --- a/cronjobs/notifications.php +++ b/cronjobs/notifications.php @@ -22,35 +22,51 @@ limitations under the License. // Include all settings and classes require_once('shared.inc.php'); +verbose("Running system notifications\n"); + +verbose(" IDLE Worker Notifications ..."); // Find all IDLE workers $aWorkers = $worker->getAllIdleWorkers(); if (empty($aWorkers)) { - verbose("No idle workers found\n"); + verbose(" no idle workers found\n"); } else { + verbose(" 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']); - if (!$notification->sendNotification($aWorker['account_id'], 'idle_worker', $aData)) - verbose($notification->getError() . "\n"); - } -} - -// We notified, lets check which recovered -$aNotifications = $notification->getAllActive('idle_worker'); -if (!empty($aNotifications)) { - foreach ($aNotifications as $aNotification) { - $aData = json_decode($aNotification['data'], true); - $aWorker = $worker->getWorker($aData['id']); - if ($aWorker['active'] == 1) { - if ($notification->setInactive($aNotification['id'])) { - verbose("Marked notification " . $aNotification['id'] . " as inactive\n"); - } else { - verbose("Failed to set notification inactive for " . $aWorker['username'] . "\n"); - } + verbose(" " . $aWorker['username'] . "..."); + if (!$notification->sendNotification($aWorker['account_id'], 'idle_worker', $aData)) { + verbose(" " . $notification->getError() . "\n"); + } else { + verbose(" sent\n"); } } } + + +verbose(" Reset IDLE Worker Notifications ..."); +// We notified, lets check which recovered +$aNotifications = $notification->getAllActive('idle_worker'); +if (!empty($aNotifications)) { + verbose(" found " . count($aNotifications) . " active notification(s)\n"); + foreach ($aNotifications as $aNotification) { + $aData = json_decode($aNotification['data'], true); + $aWorker = $worker->getWorker($aData['id']); + verbose(" " . $aWorker['username'] . " ..."); + if ($aWorker['active'] == 1) { + if ($notification->setInactive($aNotification['id'])) { + verbose(" updated #" . $aNotification['id'] . " for " . $aWorker['username'] . " as inactive\n"); + } else { + verbose(" failed to update #" . $aNotification['id'] . " for " . $aWorker['username'] . "\n"); + } + } else { + verbose(" still inactive\n"); + } + } +} else { + verbose(" no active IDLE worker notifications\n"); +} ?> diff --git a/cronjobs/statistics.php b/cronjobs/statistics.php index d4fe65e0..eefa9d70 100755 --- a/cronjobs/statistics.php +++ b/cronjobs/statistics.php @@ -25,25 +25,37 @@ require_once('shared.inc.php'); // Fetch all cachable values but disable fetching from cache $statistics->setGetCache(false); +// Verbose output +verbose("Running statistical cache updates\n"); + // Since fetching from cache is disabled, overwrite our stats +verbose(" getRoundShares ..."); if (!$statistics->getRoundShares()) - verbose("Unable to fetch and store current round shares\n"); + verbose(" update failed"); +verbose("\n getTopContributors shares ..."); if (!$statistics->getTopContributors('shares')) - verbose("Unable to fetch and store top share contributors\n"); + verbose(" update failed"); +verbose("\n getTopContributors hashes ..."); if (!$statistics->getTopContributors('hashes')) - verbose("Unable to fetch and store top hashrate contributors\n"); + verbose(" update failed"); +verbose("\n getCurrentHashrate ..."); if (!$statistics->getCurrentHashrate()) - verbose("Unable to fetch and store pool hashrate\n"); + verbose(" update failed"); // Admin specific statistics, we cache the global query due to slowness +verbose("\n getAllUserStats ..."); if (!$statistics->getAllUserStats('%')) - verbose("Unable to fetch and store admin panel full user list\n"); + verbose(" update failed"); +verbose("\n"); // Per user share statistics based on all shares submitted +verbose(" getUserShares ...\n"); $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()) { + verbose(" " . $row['username'] . " ..."); if (!$statistics->getUserShares($user->getUserId($row['username']))) - verbose("Failed to fetch and store user stats for " . $row['username'] . "\n"); + verbose(" update failed"); + verbose("\n"); } } ?> diff --git a/cronjobs/tickerupdate.php b/cronjobs/tickerupdate.php index 48e3678d..26323738 100755 --- a/cronjobs/tickerupdate.php +++ b/cronjobs/tickerupdate.php @@ -25,7 +25,7 @@ require_once('shared.inc.php'); // Include additional file not set in autoloader require_once(CLASS_DIR . '/tools.class.php'); -verbose("Running updates\n"); +verbose("Running scheduled updates\n"); verbose(" Price API Call ... "); if ($price = $tools->getPrice()) { verbose("found $price as price\n"); diff --git a/public/include/classes/notification.class.php b/public/include/classes/notification.class.php index b38bac2e..e07b3c32 100644 --- a/public/include/classes/notification.class.php +++ b/public/include/classes/notification.class.php @@ -173,8 +173,10 @@ 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)) { + $this->setErrorMessage('Error sending mail notification'); return true; + } } else { $this->setErrorMessage('User disabled ' . $strType . ' notifications'); } From c915902b727eda3d7b66fe0a792654b739b032d0 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 24 Jun 2013 16:46:23 +0300 Subject: [PATCH 021/650] Update mmcfe_ng_structure.sql Removed hard-set DB name in import file. --- sql/mmcfe_ng_structure.sql | 3 --- 1 file changed, 3 deletions(-) diff --git a/sql/mmcfe_ng_structure.sql b/sql/mmcfe_ng_structure.sql index cb39ed51..6fd3457a 100644 --- a/sql/mmcfe_ng_structure.sql +++ b/sql/mmcfe_ng_structure.sql @@ -6,9 +6,6 @@ SET time_zone = "+00:00"; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; -CREATE DATABASE IF NOT EXISTS `mmcfe_ng` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci; -USE `mmcfe_ng`; - CREATE TABLE IF NOT EXISTS `accounts` ( `id` int(255) NOT NULL AUTO_INCREMENT, `is_admin` tinyint(1) NOT NULL DEFAULT '0', From d5d2d3d36f47a564d8e69d101556ecb6c6439647 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 25 Jun 2013 08:49:13 +0200 Subject: [PATCH 022/650] Fixing unauthenticated navigation for stats Use different link to properly display pool statistics if not authenticated. Fixes #245 --- public/templates/mmcFE/global/navigation.tpl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 09901679..960ba343 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -19,13 +19,17 @@ {/if} + {if $smarty.session.AUTHENTICATED|default}
  • Statistics
  • + {else} +
  • Statistics + {/if}
  • Getting Started
  • Support
  • About From 2204b6d9f3c85988ee2a2cb57cb91d4444ee3254 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 20 Jun 2013 16:49:53 +0200 Subject: [PATCH 023/650] Adding support to pay out full block amount * Configuration setting added: `reward_type` * Default behaviour: `fixed` payout by `reward` setting * Optional: `block` to payout full block amount to users Fixes #76 --- cronjobs/findblock.php | 3 ++- cronjobs/proportional_payout.php | 3 ++- public/include/config/global.inc.dist.php | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index e68d9ff4..ae08af72 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -72,7 +72,7 @@ if (empty($aAllBlocks)) { verbose("No new unaccounted blocks found\n"); } else { // Loop through our unaccounted blocks - verbose("\nBlock ID\tBlock Height\tShare ID\tShares\tFinder\t\t\tStatus\n"); + verbose("\nBlock ID\tBlock Height\tAmount\tShare ID\tShares\tFinder\t\t\tStatus\n"); foreach ($aAllBlocks as $iIndex => $aBlock) { if (empty($aBlock['share_id'])) { // Fetch this blocks upstream ID @@ -107,6 +107,7 @@ if (empty($aAllBlocks)) { verbose( $aBlock['id'] . "\t\t" . $aBlock['height'] . "\t\t" + . $aBlock['amount'] . "\t" . $iCurrentUpstreamId . "\t\t" . $iRoundShares . "\t" . "[$iAccountId] " . $user->getUserName($iAccountId) . "\t\t" diff --git a/cronjobs/proportional_payout.php b/cronjobs/proportional_payout.php index 61092006..57b97c2c 100755 --- a/cronjobs/proportional_payout.php +++ b/cronjobs/proportional_payout.php @@ -42,6 +42,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $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"); @@ -56,7 +57,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { 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['payout'] = number_format(round(( $aData['percentage'] / 100 ) * $dReward, 8), 8); // Defaults $aData['fee' ] = 0; $aData['donation'] = 0; diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 3d677814..88513da0 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -60,6 +60,7 @@ $config = array( '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 + 'reward_type' => 'fixed', // Payout `fixed` reward as defined in reward or `block` amount 'confirmations' => '120', // Confirmations per block needed to credit transactions 'memcache' => array( 'enabled' => true, From 8dbc8c6a9bc28dfff1fb6f2f559f5dd554855713 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 25 Jun 2013 08:57:40 +0200 Subject: [PATCH 024/650] Display fixed or block reward in findblock cron Fixes #76 --- cronjobs/findblock.php | 1 + 1 file changed, 1 insertion(+) diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index ae08af72..3f3fb7e0 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -48,6 +48,7 @@ if (empty($aTransactions['transactions'])) { 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" . From 370b3475b993db73923082d6f69c6d9226cddccb Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 25 Jun 2013 10:57:56 +0200 Subject: [PATCH 025/650] Adding transactions admin panel * Lists all transactions for all users Addresses #251 --- public/include/classes/transaction.class.php | 25 ++++ .../include/pages/admin/transactions.inc.php | 11 ++ .../mmcFE/admin/transactions/default.tpl | 139 ++++++++++++++++++ public/templates/mmcFE/global/navigation.tpl | 1 + 4 files changed, 176 insertions(+) create mode 100644 public/include/pages/admin/transactions.inc.php create mode 100644 public/templates/mmcFE/admin/transactions/default.tpl diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 038a30c5..a9c665c6 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -108,6 +108,31 @@ class Transaction { return false; } + /** + * Fetch all transactions for all users + * @param none + * @return mixed array or false + **/ + public function getAllTransactions() { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare(" + SELECT + t.id AS id, + t.type AS type, + t.amount AS amount, + t.coin_address AS coin_address, + t.timestamp AS timestamp, + b.height AS height, + b.confirmations AS confirmations + FROM transactions AS t + LEFT JOIN blocks AS b ON t.block_id = b.id + ORDER BY id DESC"); + if ($this->checkStmt($stmt) && $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); diff --git a/public/include/pages/admin/transactions.inc.php b/public/include/pages/admin/transactions.inc.php new file mode 100644 index 00000000..93d4530d --- /dev/null +++ b/public/include/pages/admin/transactions.inc.php @@ -0,0 +1,11 @@ +isAuthenticated()) { + $aTransactions = $transaction->getAllTransactions(); + if (!$aTransactions) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any transaction', 'TYPE' => 'errormsg'); + $smarty->assign('TRANSACTIONS', $aTransactions); + $smarty->assign('CONTENT', 'default.tpl'); +} +?> diff --git a/public/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl new file mode 100644 index 00000000..c76e9fae --- /dev/null +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -0,0 +1,139 @@ +{include file="global/block_header.tpl" BLOCK_HEADER="Transaction Log" BUTTONS=array(Confirmed,Unconfirmed,Orphan)} +
    +
    + {include file="global/pagination.tpl"} + + + + + + + + + + + + +{section transaction $TRANSACTIONS} + {if ( + (($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus')and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) + or ($TRANSACTIONS[transaction].type == 'Donation' and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) + or ($TRANSACTIONS[transaction].type == 'Fee' and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) + or $TRANSACTIONS[transaction].type == 'Credit_PPS' + or $TRANSACTIONS[transaction].type == 'Fee_PPS' + or $TRANSACTIONS[transaction].type == 'Donation_PPS' + or $TRANSACTIONS[transaction].type == 'Debit_AP' + or $TRANSACTIONS[transaction].type == 'Debit_MP' + or $TRANSACTIONS[transaction].type == 'TXFee' + )} + + + + + + + + + {/if} +{/section} + +
    TX #DateTX TypePayment AddressBlock #Amount
    {$TRANSACTIONS[transaction].id}{$TRANSACTIONS[transaction].timestamp}{$TRANSACTIONS[transaction].type}{$TRANSACTIONS[transaction].coin_address}{if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if}{$TRANSACTIONS[transaction].amount}
    +

    + + Credit_AP = Auto Threshold Payment, Credit_MP = Manual Payment, Donation = Donation, Fee = Pool Fees (if applicable) + +

    +
    +
    +
    +
    + {include file="global/pagination.tpl" ID=2} + + + + + + + + + + + + +{section transaction $TRANSACTIONS} + {if ( + ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations + or ($TRANSACTIONS[transaction].type == 'Donation' and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) + or ($TRANSACTIONS[transaction].type == 'Fee' and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) + )} + + + + + + + + + {if $TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus'} + {assign var="credits" value="`$credits+$TRANSACTIONS[transaction].amount`"} + {else} + {assign var="debits" value="`$debits+$TRANSACTIONS[transaction].amount`"} + {/if} + {/if} +{/section} + + + + + +
    TX #DateTX TypePayment AddressBlock #Amount
    {$TRANSACTIONS[transaction].id}{$TRANSACTIONS[transaction].timestamp}{$TRANSACTIONS[transaction].type}{$TRANSACTIONS[transaction].coin_address}{if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if}{$TRANSACTIONS[transaction].amount}
    Unconfirmed Totals:{$credits|default - $debits|default}
    +

    Listed are your estimated rewards and donations/fees for all blocks awaiting {$GLOBAL.confirmations} confirmations.

    +
    +
    +
    +
    + {include file="global/pagination.tpl"} + + + + + + + + + + + + +{section transaction $TRANSACTIONS} + {if ( + $TRANSACTIONS[transaction].type == 'Orphan_Credit' + or $TRANSACTIONS[transaction].type == 'Orphan_Donation' + or $TRANSACTIONS[transaction].type == 'Orphan_Fee' + or $TRANSACTIONS[transaction].type == 'Orphan_Bonus' + )} + + + + + + + + + {if $TRANSACTIONS[transaction].type == 'Orphan_Credit' or $TRANSACTIONS[transaction].type == 'Orphan_Bonus'} + {assign var="orphan_credits" value="`$orphan_credits+$TRANSACTIONS[transaction].amount`"} + {else} + {assign var="orphan_debits" value="`$orphan_debits+$TRANSACTIONS[transaction].amount`"} + {/if} + {/if} +{/section} + + + + + +
    TX #DateTX TypePayment AddressBlock #Amount
    {$TRANSACTIONS[transaction].id}{$TRANSACTIONS[transaction].timestamp}{$TRANSACTIONS[transaction].type}{$TRANSACTIONS[transaction].coin_address}{if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if}{$TRANSACTIONS[transaction].amount}
    Orphaned Totals:{$orphan_credits|default - $orphan_debits|default}
    +

    Listed are your orphaned transactions for blocks not part of the main blockchain.

    +
    +
    +{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 960ba343..234bfda2 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -15,6 +15,7 @@
  • From a09bd1470ec11fd7b49a135a9e9a49318b1e8ea8 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 25 Jun 2013 11:39:44 +0200 Subject: [PATCH 026/650] Adding account name to all transactions Fixes #251 --- public/include/classes/transaction.class.php | 6 ++++-- public/templates/mmcFE/admin/transactions/default.tpl | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index a9c665c6..a432b65b 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -95,7 +95,7 @@ class Transaction { b.height AS height, b.confirmations AS confirmations FROM transactions AS t - LEFT JOIN blocks AS b ON t.block_id = b.id + LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id WHERE t.account_id = ? ORDER BY id DESC"); if ($this->checkStmt($stmt)) { @@ -118,6 +118,7 @@ class Transaction { $stmt = $this->mysqli->prepare(" SELECT t.id AS id, + a.username as username, t.type AS type, t.amount AS amount, t.coin_address AS coin_address, @@ -125,7 +126,8 @@ class Transaction { b.height AS height, b.confirmations AS confirmations FROM transactions AS t - LEFT JOIN blocks AS b ON t.block_id = b.id + 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 ORDER BY id DESC"); if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); diff --git a/public/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl index c76e9fae..c6497246 100644 --- a/public/templates/mmcFE/admin/transactions/default.tpl +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -6,6 +6,7 @@ TX # + Account Date TX Type Payment Address @@ -28,6 +29,7 @@ )} {$TRANSACTIONS[transaction].id} + {$TRANSACTIONS[transaction].username} {$TRANSACTIONS[transaction].timestamp} {$TRANSACTIONS[transaction].type} {$TRANSACTIONS[transaction].coin_address} @@ -52,6 +54,7 @@ TX # + Account Date TX Type Payment Address @@ -68,6 +71,7 @@ )} {$TRANSACTIONS[transaction].id} + {$TRANSACTIONS[transaction].username} {$TRANSACTIONS[transaction].timestamp} {$TRANSACTIONS[transaction].type} {$TRANSACTIONS[transaction].coin_address} @@ -97,6 +101,7 @@ TX # + Account Date TX Type Payment Address @@ -114,6 +119,7 @@ )} {$TRANSACTIONS[transaction].id} + {$TRANSACTIONS[transaction].username} {$TRANSACTIONS[transaction].timestamp} {$TRANSACTIONS[transaction].type} {$TRANSACTIONS[transaction].coin_address} From 65d8b08e8234286693eaf9a28804d70026423130 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 25 Jun 2013 14:09:01 +0200 Subject: [PATCH 027/650] Better looking news post header --- public/templates/mmcFE/home/default.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mmcFE/home/default.tpl b/public/templates/mmcFE/home/default.tpl index a54859db..81006aff 100644 --- a/public/templates/mmcFE/home/default.tpl +++ b/public/templates/mmcFE/home/default.tpl @@ -1,5 +1,5 @@ {section name=news loop=$NEWS} - {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header} posted {$NEWS[news].time} by {$NEWS[news].author}"} + {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header}, posted {$NEWS[news].time|date_format:"%b %e, %Y at %H:%M"} by {$NEWS[news].author}"} {$NEWS[news].content} {include file="global/block_footer.tpl"} {/section} From 61a2a309163a7a38485c60015d86a1693cc89a51 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 25 Jun 2013 14:11:25 +0200 Subject: [PATCH 028/650] Missing --- public/templates/mmcFE/home/default.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mmcFE/home/default.tpl b/public/templates/mmcFE/home/default.tpl index 81006aff..e898af86 100644 --- a/public/templates/mmcFE/home/default.tpl +++ b/public/templates/mmcFE/home/default.tpl @@ -1,5 +1,5 @@ {section name=news loop=$NEWS} - {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header}, posted {$NEWS[news].time|date_format:"%b %e, %Y at %H:%M"} by {$NEWS[news].author}"} + {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header}, posted {$NEWS[news].time|date_format:"%b %e, %Y at %H:%M"} by {$NEWS[news].author}"} {$NEWS[news].content} {include file="global/block_footer.tpl"} {/section} From 416d52078d4b2e9c2a0cd1c2cf54d88c17f67c68 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 26 Jun 2013 10:00:06 +0200 Subject: [PATCH 029/650] Adding an optimized valid/invalid shares query * This should speed up the process of finding shares for round and users Addresses #246 --- public/include/classes/statistics.class.php | 36 +++++++-------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/public/include/classes/statistics.class.php b/public/include/classes/statistics.class.php index a30aba1b..43529e81 100644 --- a/public/include/classes/statistics.class.php +++ b/public/include/classes/statistics.class.php @@ -134,14 +134,10 @@ class Statistics { if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__)) return $data; $stmt = $this->mysqli->prepare(" SELECT - ( SELECT IFNULL(count(id), 0) + SUM(IF(our_result='Y', 1, 0)) AS valid, + SUM(IF(our_result='N', 1, 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()); // Catchall @@ -159,25 +155,15 @@ class Statistics { if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; $stmt = $this->mysqli->prepare(" SELECT - ( - SELECT COUNT(s.id) - FROM " . $this->share->getTableName() . " AS s, + SUM(IF(our_result='Y', 1, 0)) AS valid, + SUM(IF(our_result='N', 1, 0)) + 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()) + 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); From 1126118cb995c7a439764270df877599a203f57e Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 26 Jun 2013 10:54:10 +0200 Subject: [PATCH 030/650] Fetch all user shares in one query This will fetch all user shares in a single run, not user by user as done previously. Saves one query and can possibly increase SQL execution time. Addresses #246 --- cronjobs/statistics.php | 35 ++++++++++++--------- public/include/classes/statistics.class.php | 28 +++++++++++++++++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/cronjobs/statistics.php b/cronjobs/statistics.php index eefa9d70..69c1db5f 100755 --- a/cronjobs/statistics.php +++ b/cronjobs/statistics.php @@ -30,32 +30,39 @@ verbose("Running statistical cache updates\n"); // Since fetching from cache is disabled, overwrite our stats verbose(" getRoundShares ..."); +$start = microtime(true); if (!$statistics->getRoundShares()) verbose(" update failed"); -verbose("\n getTopContributors shares ..."); +verbose(" " . number_format(microtime(true) - $start, 2) . " seconds\n"); +verbose(" getTopContributors shares ..."); +$start = microtime(true); if (!$statistics->getTopContributors('shares')) verbose(" update failed"); -verbose("\n getTopContributors hashes ..."); +verbose(" " . number_format(microtime(true) - $start, 2) . " seconds\n"); +verbose(" getTopContributors hashes ..."); +$start = microtime(true); if (!$statistics->getTopContributors('hashes')) verbose(" update failed"); -verbose("\n getCurrentHashrate ..."); +verbose(" " . number_format(microtime(true) - $start, 2) . " seconds\n"); +verbose(" getCurrentHashrate ..."); +$start = microtime(true); if (!$statistics->getCurrentHashrate()) verbose(" update failed"); +verbose(" " . number_format(microtime(true) - $start, 2) . " seconds\n"); // Admin specific statistics, we cache the global query due to slowness -verbose("\n getAllUserStats ..."); +verbose(" getAllUserStats ..."); +$start = microtime(true); if (!$statistics->getAllUserStats('%')) verbose(" update failed"); -verbose("\n"); +verbose(" " . number_format(microtime(true) - $start, 2) . " seconds\n"); // Per user share statistics based on all shares submitted -verbose(" getUserShares ...\n"); -$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()) { - verbose(" " . $row['username'] . " ..."); - if (!$statistics->getUserShares($user->getUserId($row['username']))) - verbose(" update failed"); - verbose("\n"); - } +verbose(" getAllUserShares ..."); +$start = microtime(true); +$aUserShares = $statistics->getAllUserShares(); +verbose(" " . number_format(microtime(true) - $start, 2) . " seconds"); +foreach ($aUserShares as $aShares) { + $memcache->setCache('getUserShares'. $aShares['id'], $aShares); } +verbose("\n"); ?> diff --git a/public/include/classes/statistics.class.php b/public/include/classes/statistics.class.php index 43529e81..50fd55a8 100644 --- a/public/include/classes/statistics.class.php +++ b/public/include/classes/statistics.class.php @@ -145,6 +145,34 @@ class Statistics { 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 ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__)) return $data; + $stmt = $this->mysqli->prepare(" + SELECT + SUM(IF(our_result='Y', 1, 0)) AS valid, + SUM(IF(our_result='N', 1, 0)) AS invalid, + u.id AS id, + 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) + GROUP BY u.id"); + if ($stmt && $stmt->execute() && $result = $stmt->get_result()) + return $this->memcache->setCache(__FUNCTION__, $result->fetch_all(MYSQLI_ASSOC)); + // 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 From d4f4b9073fa9b035b46da69afd66738fbf453ab9 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 13:45:14 +0200 Subject: [PATCH 031/650] Working jQuery Mobile frontend for mobile devices * Added mobile device detection PHP library * Call PHP library to decide which theme to use * Added theme as a configuration option into global config * Selectable Desktop theme (default: mmcFE) * Selectable Mobile theme (default: mobile) * Disable mobile theme support entirely **NOTE**: This requires updates to the `global.inc.php` so please check the dist file and update your config before filing a new issue! Addresses #25 --- public/include/autoloader.inc.php | 13 + public/include/classes/user.class.php | 10 +- public/include/config/global.inc.dist.php | 3 + public/include/lib/Mobile_Detect.php | 814 ++++++++++++++++++ public/include/pages/login.inc.php | 3 +- public/index.php | 4 +- public/templates/mobile/global/footer.tpl | 1 + public/templates/mobile/global/header.tpl | 1 + public/templates/mobile/global/navigation.tpl | 11 + public/templates/mobile/home/default.tpl | 8 + public/templates/mobile/login/default.tpl | 6 + public/templates/mobile/master.tpl | 31 + .../mobile/password/change/default.tpl | 12 + public/templates/mobile/password/default.tpl | 6 + .../mobile/statistics/blocks/default.tpl | 69 ++ .../mobile/statistics/blocks/small_table.tpl | 22 + .../templates/mobile/statistics/default.tpl | 21 + .../mobile/statistics/pool/authenticated.tpl | 58 ++ .../statistics/pool/contributors_hashrate.tpl | 35 + .../statistics/pool/contributors_shares.tpl | 28 + 20 files changed, 1149 insertions(+), 7 deletions(-) create mode 100644 public/include/lib/Mobile_Detect.php create mode 100644 public/templates/mobile/global/footer.tpl create mode 100644 public/templates/mobile/global/header.tpl create mode 100644 public/templates/mobile/global/navigation.tpl create mode 100644 public/templates/mobile/home/default.tpl create mode 100644 public/templates/mobile/login/default.tpl create mode 100644 public/templates/mobile/master.tpl create mode 100644 public/templates/mobile/password/change/default.tpl create mode 100644 public/templates/mobile/password/default.tpl create mode 100644 public/templates/mobile/statistics/blocks/default.tpl create mode 100644 public/templates/mobile/statistics/blocks/small_table.tpl create mode 100644 public/templates/mobile/statistics/default.tpl create mode 100644 public/templates/mobile/statistics/pool/authenticated.tpl create mode 100644 public/templates/mobile/statistics/pool/contributors_hashrate.tpl create mode 100644 public/templates/mobile/statistics/pool/contributors_shares.tpl diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index 51b9b815..3fddec3e 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -1,5 +1,18 @@ isMobile() && $config['website']['mobile']) { + // Set to mobile theme + $config['website']['mobile_theme'] ? $theme = $config['website']['mobile_theme'] : $theme = 'mobile'; +} else { + // Use configured theme, fallback to default theme + $config['website']['theme'] ? $theme = $config['website']['theme'] : $theme = 'mmcFE'; +} +define('THEME', $theme); + require_once(CLASS_DIR . '/debug.class.php'); require_once(CLASS_DIR . '/bitcoin.class.php'); require_once(CLASS_DIR . '/statscache.class.php'); diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index c400ddc7..180019c9 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -105,14 +105,18 @@ 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 ($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->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)) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 88513da0..6357595a 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -39,6 +39,9 @@ $config = array( 'max' => 250 ), 'website' => array( + 'theme' => 'mmcFE', // Overall default theme + 'mobile' => true, // Allow overwriting theme for mobile devices + 'mobile_theme' => 'mobile', // Set default or custom mobile theme 'registration' => true, // Allow new users to register 'name' => 'The Pool', 'slogan' => 'Resistance is futile', 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/pages/login.inc.php b/public/include/pages/login.inc.php index c157d720..d8bfbb12 100644 --- a/public/include/pages/login.inc.php +++ b/public/include/pages/login.inc.php @@ -6,8 +6,9 @@ if (!defined('SECURITY')) if ( $user->checkLogin($_POST['username'],$_POST['password']) ) { header('Location: index.php?page=home'); -} else { +} else if (@$_POST['username'] && @$_POST['password']) { $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to login: '. $user->getError(), 'TYPE' => 'errormsg'); } + $smarty->assign('CONTENT', 'default.tpl'); ?> diff --git a/public/index.php b/public/index.php index 80fd4680..dbb28f9a 100644 --- a/public/index.php +++ b/public/index.php @@ -29,9 +29,7 @@ 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'); // Load Classes, they name defines the $ variable used // We include all needed files here, even though our templates could load them themself diff --git a/public/templates/mobile/global/footer.tpl b/public/templates/mobile/global/footer.tpl new file mode 100644 index 00000000..dcb04482 --- /dev/null +++ b/public/templates/mobile/global/footer.tpl @@ -0,0 +1 @@ +

    Powered by mmcfe-ng

    diff --git a/public/templates/mobile/global/header.tpl b/public/templates/mobile/global/header.tpl new file mode 100644 index 00000000..a4de1e59 --- /dev/null +++ b/public/templates/mobile/global/header.tpl @@ -0,0 +1 @@ +

    {$GLOBAL.websitename}

    diff --git a/public/templates/mobile/global/navigation.tpl b/public/templates/mobile/global/navigation.tpl new file mode 100644 index 00000000..2821454f --- /dev/null +++ b/public/templates/mobile/global/navigation.tpl @@ -0,0 +1,11 @@ +
    + +
    diff --git a/public/templates/mobile/home/default.tpl b/public/templates/mobile/home/default.tpl new file mode 100644 index 00000000..69ecd30d --- /dev/null +++ b/public/templates/mobile/home/default.tpl @@ -0,0 +1,8 @@ +{section name=news loop=$NEWS} +
    +
    +

    {$NEWS[news].header}

    +

    {$NEWS[news].content}

    +
    +
    +{/section} diff --git a/public/templates/mobile/login/default.tpl b/public/templates/mobile/login/default.tpl new file mode 100644 index 00000000..3c3ded3d --- /dev/null +++ b/public/templates/mobile/login/default.tpl @@ -0,0 +1,6 @@ +
    +

    +

    +

    +
    +

    Forgot your password?

    diff --git a/public/templates/mobile/master.tpl b/public/templates/mobile/master.tpl new file mode 100644 index 00000000..a9004bdd --- /dev/null +++ b/public/templates/mobile/master.tpl @@ -0,0 +1,31 @@ + + + + Page Title + + + + + {if is_array($smarty.session.POPUP|default)}{/if} + + +
    +
    +{include file="global/header.tpl"} +{include file="global/navigation.tpl"} +
    + {if is_array($smarty.session.POPUP|default)} + +
    +

    Test

    +
    + {/if} +
    +{include file="$PAGE/$ACTION/$CONTENT"} +
    +
    +{include file="global/footer.tpl"} +
    +
    + + diff --git a/public/templates/mobile/password/change/default.tpl b/public/templates/mobile/password/change/default.tpl new file mode 100644 index 00000000..0d254677 --- /dev/null +++ b/public/templates/mobile/password/change/default.tpl @@ -0,0 +1,12 @@ +{include file="global/block_header.tpl" BLOCK_HEADER="Change Password"} +
    + + + + + + + +
    New Password:
    New Password Repeat:
    +
    +{include file="global/block_footer.tpl"} diff --git a/public/templates/mobile/password/default.tpl b/public/templates/mobile/password/default.tpl new file mode 100644 index 00000000..20f00297 --- /dev/null +++ b/public/templates/mobile/password/default.tpl @@ -0,0 +1,6 @@ +
    + + +

    If you have an email set for your account, enter your username to get your password reset

    +

    +
    diff --git a/public/templates/mobile/statistics/blocks/default.tpl b/public/templates/mobile/statistics/blocks/default.tpl new file mode 100644 index 00000000..b4f29b00 --- /dev/null +++ b/public/templates/mobile/statistics/blocks/default.tpl @@ -0,0 +1,69 @@ + + + + +{section block $BLOCKSFOUND step=-1 max=20} + +{/section} + + + + + +{section block $BLOCKSFOUND step=-1 max=20} + +{/section} + + + +{section block $BLOCKSFOUND step=-1 max=20} + +{/section} + + +
    Block Shares
    {$BLOCKSFOUND[block].height}
    Expected{round(pow(2,32 - $GLOBAL.config.targetdiff) * $BLOCKSFOUND[block].difficulty)}
    Actual{$BLOCKSFOUND[block].shares}
    +

    +

    +The graph above illustrates N shares to find a block vs. E Shares expected to find a block based on +target and network difficulty and assuming a zero variance scenario. +

    + +
    + + + + + + + + + + + + + + +{assign var=rank value=1} +{section block $BLOCKSFOUND} + + + + + + + + + + +{/section} + +
    BlockValidityFinderTimeDifficultyExpected SharesActual SharesPercentage
    {$BLOCKSFOUND[block].height} + {if $BLOCKSFOUND[block].confirmations >= $GLOBAL.confirmations} + Confirmed + {else if $BLOCKSFOUND[block].confirmations == -1} + Orphan + {else}{$GLOBAL.confirmations - $BLOCKSFOUND[block].confirmations} left{/if}{$BLOCKSFOUND[block].finder|default:"unknown"}{$BLOCKSFOUND[block].time|date_format:"%d/%m %H:%M:%S"}{$BLOCKSFOUND[block].difficulty|number_format:"2"}{(pow(2,32 - $GLOBAL.config.targetdiff) * $BLOCKSFOUND[block].difficulty)|number_format}{$BLOCKSFOUND[block].shares|number_format}{($BLOCKSFOUND[block].shares / (pow(2,32 - $GLOBAL.config.targetdiff) * $BLOCKSFOUND[block].difficulty) * 100)|number_format:"2"}
    +
    +
      +
    • Note: Round Earnings are not credited until {$GLOBAL.confirmations} confirms.
    • +
    diff --git a/public/templates/mobile/statistics/blocks/small_table.tpl b/public/templates/mobile/statistics/blocks/small_table.tpl new file mode 100644 index 00000000..2cfac05a --- /dev/null +++ b/public/templates/mobile/statistics/blocks/small_table.tpl @@ -0,0 +1,22 @@ + + + + + + + + + + + +{assign var=rank value=1} +{section block $BLOCKSFOUND} + + + + + + +{/section} + +
    BlockFinderTimeShares
    {$BLOCKSFOUND[block].height}{$BLOCKSFOUND[block].finder|default:"unknown"}{$BLOCKSFOUND[block].time|date_format:"%d/%m %H:%M"}{$BLOCKSFOUND[block].shares|number_format}
    diff --git a/public/templates/mobile/statistics/default.tpl b/public/templates/mobile/statistics/default.tpl new file mode 100644 index 00000000..994e60a3 --- /dev/null +++ b/public/templates/mobile/statistics/default.tpl @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + +
    Pool Hash Rate{$GLOBAL.hashrate / 1000} Mhash/s
    Current Total Miners{$GLOBAL.workers}
    Current Block{$CURRENTBLOCK}
    Current Difficulty{$DIFFICULTY}
    +
  • These stats are also available in JSON format HERE
  • diff --git a/public/templates/mobile/statistics/pool/authenticated.tpl b/public/templates/mobile/statistics/pool/authenticated.tpl new file mode 100644 index 00000000..b2a499e3 --- /dev/null +++ b/public/templates/mobile/statistics/pool/authenticated.tpl @@ -0,0 +1,58 @@ +
    +

    User Hashrates

    +{include file="statistics/pool/contributors_hashrate.tpl"} +
    + +
    +

    User Shares

    +{include file="statistics/pool/contributors_shares.tpl"} +
    + +
    +

    General Stats

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Pool Hash Rate{($GLOBAL.hashrate / 1000)|number_format:"3"} Mhash/s
    Pool Efficiency{(100 - (100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid))|number_format:"2"} %
    Current Active Workers{$GLOBAL.workers}
    Next Network Block{$CURRENTBLOCK + 1}    (Current: {$CURRENTBLOCK})
    Last Block Found{$LASTBLOCK|default:"0"}
    Current Difficulty{$DIFFICULTY}
    Est. Avg. Time per Round{$ESTTIME|seconds_to_words}
    Est. Shares this Round{(pow(2, 32 - $GLOBAL.config.targetdiff) * $DIFFICULTY)|number_format:"0"} (done: {(100 / (pow(2, 32 - $GLOBAL.config.targetdiff) * $DIFFICULTY) * $GLOBAL.roundshares.valid)|number_format:"2"} %)
    Time Since Last Block{$TIMESINCELAST|seconds_to_words}
    +
    + +
    +

    Last Blocks

    +{include file="statistics/blocks/small_table.tpl" ALIGN="right" SHORT=true} +
    diff --git a/public/templates/mobile/statistics/pool/contributors_hashrate.tpl b/public/templates/mobile/statistics/pool/contributors_hashrate.tpl new file mode 100644 index 00000000..fbfcba82 --- /dev/null +++ b/public/templates/mobile/statistics/pool/contributors_hashrate.tpl @@ -0,0 +1,35 @@ + + + + + + + + + + + +{assign var=rank value=1} +{assign var=listed value=0} +{section contrib $CONTRIBHASHES} + {math assign="estday" equation="round(reward / ( diff * pow(2,32) / ( hashrate * 1000 ) / 3600 / 24), 3)" diff=$DIFFICULTY reward=$REWARD hashrate=$CONTRIBHASHES[contrib].hashrate} + {if $GLOBAL.userdata.username == $CONTRIBSHARES[hashrate].account}{assign var=listed value=1}{/if} + + + + + + + +{/section} +{if $listed != 1} + + + + + + + +{/if} + +
    RankUser NameKH/s{$GLOBAL.config.currency}/Day{$GLOBAL.config.price.currency}/Day
    {$rank++}{$CONTRIBHASHES[contrib].account}{$CONTRIBHASHES[contrib].hashrate|number_format}{$estday|number_format:"3"}{($estday * $GLOBAL.price)|default:"n/a"|number_format:"2"}
    n/a{$GLOBAL.userdata.username}{$GLOBAL.userdata.hashrate}{$estday|number_format:"3"|default:"n/a"}{($estday * $GLOBAL.price)|default:"n/a"|number_format:"2"}
    diff --git a/public/templates/mobile/statistics/pool/contributors_shares.tpl b/public/templates/mobile/statistics/pool/contributors_shares.tpl new file mode 100644 index 00000000..e23b3998 --- /dev/null +++ b/public/templates/mobile/statistics/pool/contributors_shares.tpl @@ -0,0 +1,28 @@ + + + + + + + + + +{assign var=rank value=1} +{assign var=listed value=0} +{section hashrate $CONTRIBSHARES} +{if $GLOBAL.userdata.username == $CONTRIBSHARES[hashrate].account}{assign var=listed value=1}{/if} + + + + + +{/section} +{if $listed != 1} + + + + + +{/if} + +
    RankUser NameShares
    {$rank++}{$CONTRIBSHARES[hashrate].account}{$CONTRIBSHARES[hashrate].shares|number_format}
    n/a{$GLOBAL.userdata.username}{$GLOBAL.userdata.shares.valid|number_format}
    From 36acc858ed10dbd61c4f50d91e558abed5292800 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 13:50:22 +0200 Subject: [PATCH 032/650] Fixing global dist config Defined THEME must be removed for mobile device themes to work. --- public/include/config/global.inc.dist.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 6357595a..8f28e8df 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -2,9 +2,6 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -// What is our overall theme -define('THEME', 'mmcFE'); - // Our include directory for additional features define('INCLUDE_DIR', BASEPATH . 'include'); From 9f7cc5c0e14d88d75353d7126174450b70c7caba Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 15:34:40 +0200 Subject: [PATCH 033/650] fixing mobile hashrate table --- .../templates/mobile/statistics/pool/contributors_hashrate.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mobile/statistics/pool/contributors_hashrate.tpl b/public/templates/mobile/statistics/pool/contributors_hashrate.tpl index fbfcba82..15fae80a 100644 --- a/public/templates/mobile/statistics/pool/contributors_hashrate.tpl +++ b/public/templates/mobile/statistics/pool/contributors_hashrate.tpl @@ -13,7 +13,7 @@ {assign var=listed value=0} {section contrib $CONTRIBHASHES} {math assign="estday" equation="round(reward / ( diff * pow(2,32) / ( hashrate * 1000 ) / 3600 / 24), 3)" diff=$DIFFICULTY reward=$REWARD hashrate=$CONTRIBHASHES[contrib].hashrate} - {if $GLOBAL.userdata.username == $CONTRIBSHARES[hashrate].account}{assign var=listed value=1}{/if} + {if $GLOBAL.userdata.username == $CONTRIBSHARES[contrib].account}{assign var=listed value=1}{/if} {$rank++} {$CONTRIBHASHES[contrib].account} From 92f2243cfb5bebce37d3892bf9daf5c2522d0fe2 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 15:47:21 +0200 Subject: [PATCH 034/650] Fixing getuserstatus API call Fixes #257 --- public/include/classes/statistics.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/classes/statistics.class.php b/public/include/classes/statistics.class.php index 50fd55a8..aa8d1b23 100644 --- a/public/include/classes/statistics.class.php +++ b/public/include/classes/statistics.class.php @@ -184,7 +184,7 @@ class Statistics { $stmt = $this->mysqli->prepare(" SELECT SUM(IF(our_result='Y', 1, 0)) AS valid, - SUM(IF(our_result='N', 1, 0)) + SUM(IF(our_result='N', 1, 0)) AS invalid, FROM " . $this->share->getTableName() . " AS s, " . $this->user->getTableName() . " AS u WHERE From 5c6e87286717ae3368cbb15d1ae7ed0b09aed0d3 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 15:54:30 +0200 Subject: [PATCH 035/650] Fixing false shares result in API call Fixes #262 --- public/include/classes/statistics.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/include/classes/statistics.class.php b/public/include/classes/statistics.class.php index aa8d1b23..f304c54e 100644 --- a/public/include/classes/statistics.class.php +++ b/public/include/classes/statistics.class.php @@ -184,7 +184,7 @@ class Statistics { $stmt = $this->mysqli->prepare(" SELECT SUM(IF(our_result='Y', 1, 0)) AS valid, - SUM(IF(our_result='N', 1, 0)) AS invalid, + SUM(IF(our_result='N', 1, 0)) AS invalid FROM " . $this->share->getTableName() . " AS s, " . $this->user->getTableName() . " AS u WHERE @@ -195,6 +195,7 @@ class Statistics { return $this->memcache->setCache(__FUNCTION__ . $account_id, $result->fetch_assoc()); // Catchall $this->debug->append("Unable to fetch user round shares: " . $this->mysqli->error); + var_dump($this->debug); return false; } From 7b0ae8d86ec2787ab157783041a9e720a4d6f796 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 20:29:51 +0200 Subject: [PATCH 036/650] reduced blocks count to fixed 20 --- public/include/pages/statistics/blocks.inc.php | 2 +- public/templates/mmcFE/statistics/blocks/default.tpl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/include/pages/statistics/blocks.inc.php b/public/include/pages/statistics/blocks.inc.php index e83aa8a8..011f4b5c 100644 --- a/public/include/pages/statistics/blocks.inc.php +++ b/public/include/pages/statistics/blocks.inc.php @@ -5,7 +5,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); if (!$user->isAuthenticated()) header("Location: index.php?page=home"); // Grab the last blocks found -$iLimit = 30; +$iLimit = 20; $aBlocksFoundData = $statistics->getBlocksFound($iLimit); $aBlockData = $aBlocksFoundData[0]; diff --git a/public/templates/mmcFE/statistics/blocks/default.tpl b/public/templates/mmcFE/statistics/blocks/default.tpl index 3d7afdde..2fcaa9e5 100644 --- a/public/templates/mmcFE/statistics/blocks/default.tpl +++ b/public/templates/mmcFE/statistics/blocks/default.tpl @@ -3,7 +3,7 @@ Block Shares -{section block $BLOCKSFOUND step=-1 max=20} +{section block $BLOCKSFOUND step=-1} {$BLOCKSFOUND[block].height} {/section} @@ -11,13 +11,13 @@ Expected -{section block $BLOCKSFOUND step=-1 max=20} +{section block $BLOCKSFOUND step=-1} {round(pow(2,32 - $GLOBAL.config.targetdiff) * $BLOCKSFOUND[block].difficulty)} {/section} Actual -{section block $BLOCKSFOUND step=-1 max=20} +{section block $BLOCKSFOUND step=-1} {$BLOCKSFOUND[block].shares} {/section} From 9e59f99230aa3c8706ccf93c3d705b3cf42f6f11 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 20:32:18 +0200 Subject: [PATCH 037/650] remove block confirm notice for pps --- public/templates/mmcFE/statistics/blocks/default.tpl | 2 ++ public/templates/mmcFE/statistics/blocks/small_table.tpl | 2 ++ 2 files changed, 4 insertions(+) diff --git a/public/templates/mmcFE/statistics/blocks/default.tpl b/public/templates/mmcFE/statistics/blocks/default.tpl index 2fcaa9e5..823433b7 100644 --- a/public/templates/mmcFE/statistics/blocks/default.tpl +++ b/public/templates/mmcFE/statistics/blocks/default.tpl @@ -67,7 +67,9 @@ target and network difficulty and assuming a zero variance scenario. +{if $GLOBAL.config.payout_system != 'pps'}
    • Note: Round Earnings are not credited until {$GLOBAL.confirmations} confirms.
    +{if} {include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/statistics/blocks/small_table.tpl b/public/templates/mmcFE/statistics/blocks/small_table.tpl index 9152fcc7..731d57ef 100644 --- a/public/templates/mmcFE/statistics/blocks/small_table.tpl +++ b/public/templates/mmcFE/statistics/blocks/small_table.tpl @@ -22,7 +22,9 @@ +{if $GLOBAL.config.payout_system != 'pps'}
    • Note: Round Earnings are not credited until {$GLOBAL.confirmations} confirms.
    +{/if} {include file="global/block_footer.tpl"} From 99a58e7119c17bbe366131b4965ae18e40504acb Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 13 Jun 2013 22:26:12 +0200 Subject: [PATCH 038/650] fixed missing /if --- public/templates/mmcFE/statistics/blocks/default.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mmcFE/statistics/blocks/default.tpl b/public/templates/mmcFE/statistics/blocks/default.tpl index 823433b7..1a5a46ba 100644 --- a/public/templates/mmcFE/statistics/blocks/default.tpl +++ b/public/templates/mmcFE/statistics/blocks/default.tpl @@ -71,5 +71,5 @@ target and network difficulty and assuming a zero variance scenario.
    • Note: Round Earnings are not credited until {$GLOBAL.confirmations} confirms.
    -{if} +{/if} {include file="global/block_footer.tpl"} From d11950f9cf95c9e7d013d18ef31302bf33cc7199 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 20:39:26 +0200 Subject: [PATCH 039/650] re-introduce graph width detection --- public/site_assets/mmcFE/js/custom.js | 5 +++++ 1 file changed, 5 insertions(+) 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'] From 8f20009475970d824c46bd892895a01657c249ea Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 21:32:59 +0200 Subject: [PATCH 040/650] Distinguish between admin and user API call Fixes #268 --- .../include/pages/api/getuserstatus.inc.php | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/public/include/pages/api/getuserstatus.inc.php b/public/include/pages/api/getuserstatus.inc.php index c91ade94..ac8f6654 100644 --- a/public/include/pages/api/getuserstatus.inc.php +++ b/public/include/pages/api/getuserstatus.inc.php @@ -5,18 +5,30 @@ if (!defined('SECURITY')) die('Hacking attempt'); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $user->checkApiKey($_REQUEST['api_key']); -// We have to check if that user is admin too -if ( ! $user->isAdmin($id) ) { +/** + * This check will ensure the user can do the following: + * Admin: Check any user via request id + * Regular: Check your own status + * Other: Deny access via checkApiKey + **/ +if ( ! $user->isAdmin($user_id) && ($_REQUEST['id'] != $user_id && !empty($_REQUEST['id']))) { + // User is admin and tries to access an ID that is not their own header("HTTP/1.1 401 Unauthorized"); die("Access denied"); +} else if ($user->isAdmin($user_id)) { + // Admin, so allow any ID passed in request + $id = $_REQUEST['id']; + // 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']); +} else { + // Not admin, only allow own user ID + $id = $user_id; + $username = $user->getUserName($id); } -// Is it a username or a user ID -ctype_digit($_REQUEST['id']) ? $username = $user->getUserName($_REQUEST['id']) : $username = $_REQUEST['id']; -ctype_digit($_REQUEST['id']) ? $id = $_REQUEST['id'] : $id = $user->getUserId($_REQUEST['id']); - // Output JSON format echo json_encode(array('getuserstatus' => array( 'username' => $username, From 3421cf63b699653203d8dcd1376bd63ca5899e0d Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 21:38:03 +0200 Subject: [PATCH 041/650] Allow regular users to check their own workers Fixes #270 --- public/include/pages/api/getuserworkers.inc.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/public/include/pages/api/getuserworkers.inc.php b/public/include/pages/api/getuserworkers.inc.php index 23bdcf5d..9aaca562 100644 --- a/public/include/pages/api/getuserworkers.inc.php +++ b/public/include/pages/api/getuserworkers.inc.php @@ -5,17 +5,19 @@ if (!defined('SECURITY')) die('Hacking attempt'); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $user->checkApiKey($_REQUEST['api_key']); // We have to check if that user is admin too -if ( ! $user->isAdmin($id) ) { +if ( ! $user->isAdmin($user_id) && ($_REQUEST['id'] != $user_id && !empty($_REQUEST['id']))) { header("HTTP/1.1 401 Unauthorized"); die("Access denied"); +} else if ($user->isAdmin($user_id)) { + $id = $_REQUEST['id']; + ctype_digit($_REQUEST['id']) ? $id = $_REQUEST['id'] : $id = $user->getUserId($_REQUEST['id']); +} else { + $id = $user_id; } -// Is it a username or a user ID -ctype_digit($_REQUEST['id']) ? $id = $_REQUEST['id'] : $id = $user->getUserId($_REQUEST['id']); - // Output JSON format echo json_encode(array('getuserworkers' => $worker->getWorkers($id))); From ee9a6eed6a0882fe061a1a37487375a775630964 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 21:52:28 +0200 Subject: [PATCH 042/650] Added % of invalids for pool and user Fixes #272 --- public/templates/mmcFE/global/sidebar.tpl | 4 ++-- public/templates/mmcFE/global/sidebar_pps.tpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/templates/mmcFE/global/sidebar.tpl b/public/templates/mmcFE/global/sidebar.tpl index 58d49c67..38616226 100644 --- a/public/templates/mmcFE/global/sidebar.tpl +++ b/public/templates/mmcFE/global/sidebar.tpl @@ -33,11 +33,11 @@ Pool Invalid - {$GLOBAL.roundshares.invalid|number_format} + {$GLOBAL.roundshares.invalid|number_format} ({100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid}%) Your Invalid - {$GLOBAL.userdata.shares.invalid|number_format} + {$GLOBAL.userdata.shares.invalid|number_format} ({100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid}%) {$GLOBAL.config.currency} Round Estimate diff --git a/public/templates/mmcFE/global/sidebar_pps.tpl b/public/templates/mmcFE/global/sidebar_pps.tpl index b7aa8617..f8e7476a 100644 --- a/public/templates/mmcFE/global/sidebar_pps.tpl +++ b/public/templates/mmcFE/global/sidebar_pps.tpl @@ -30,11 +30,11 @@ Pool Invalid - {$GLOBAL.roundshares.invalid|number_format} + {$GLOBAL.roundshares.invalid|number_format} ({100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid}%) Your Invalid - {$GLOBAL.userdata.shares.invalid|number_format} + {$GLOBAL.userdata.shares.invalid|number_format} ({100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid}%)   {$GLOBAL.config.currency} Estimates From f80648e1fd847db1d6109f4695f3cc3c90e3dba9 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 28 Jun 2013 22:00:46 +0200 Subject: [PATCH 043/650] proper number format for invalid % --- public/templates/mmcFE/global/sidebar.tpl | 4 ++-- public/templates/mmcFE/global/sidebar_pps.tpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/templates/mmcFE/global/sidebar.tpl b/public/templates/mmcFE/global/sidebar.tpl index 38616226..0c50e577 100644 --- a/public/templates/mmcFE/global/sidebar.tpl +++ b/public/templates/mmcFE/global/sidebar.tpl @@ -33,11 +33,11 @@ Pool Invalid - {$GLOBAL.roundshares.invalid|number_format} ({100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid}%) + {$GLOBAL.roundshares.invalid|number_format} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid)|number_format:"2"}%) Your Invalid - {$GLOBAL.userdata.shares.invalid|number_format} ({100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid}%) + {$GLOBAL.userdata.shares.invalid|number_format} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid)|number_format:"2"}%) {$GLOBAL.config.currency} Round Estimate diff --git a/public/templates/mmcFE/global/sidebar_pps.tpl b/public/templates/mmcFE/global/sidebar_pps.tpl index f8e7476a..dcd436d5 100644 --- a/public/templates/mmcFE/global/sidebar_pps.tpl +++ b/public/templates/mmcFE/global/sidebar_pps.tpl @@ -30,11 +30,11 @@ Pool Invalid - {$GLOBAL.roundshares.invalid|number_format} ({100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid}%) + {$GLOBAL.roundshares.invalid|number_format} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid)|number_format:"2"}%) Your Invalid - {$GLOBAL.userdata.shares.invalid|number_format} ({100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid}%) + {$GLOBAL.userdata.shares.invalid|number_format} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid)|number_format:"2"}%)   {$GLOBAL.config.currency} Estimates From eb41138f3636e83f2c6344ede4ad4d8a7adeba98 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sat, 29 Jun 2013 14:51:21 +0200 Subject: [PATCH 044/650] Wider sidebar, smaller font Fixes #274 --- public/site_assets/mmcFE/css/style.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 */ From c66c448836526e0bf49b37a9e775226367c7cec1 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sat, 29 Jun 2013 15:11:57 +0200 Subject: [PATCH 045/650] Adding mobile dashboard and news page * Adding mobile detection to home page * Allow home page to default to news for desktops * Use payout specific sidebar for mobile homescreen * Added News navigation option to mobile Fixes #278 Fixes #279 --- public/include/pages/home.inc.php | 14 ++++- public/include/pages/news.inc.php | 19 ++++++ public/templates/mmcFE/news/default.tpl | 5 ++ public/templates/mobile/global/navigation.tpl | 3 +- public/templates/mobile/home/default.tpl | 59 ++++++++++++++++--- public/templates/mobile/home/pps.tpl | 50 ++++++++++++++++ public/templates/mobile/news/default.tpl | 8 +++ 7 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 public/include/pages/news.inc.php create mode 100644 public/templates/mmcFE/news/default.tpl create mode 100644 public/templates/mobile/home/pps.tpl create mode 100644 public/templates/mobile/news/default.tpl diff --git a/public/include/pages/home.inc.php b/public/include/pages/home.inc.php index 38e1022d..ffd08520 100644 --- a/public/include/pages/home.inc.php +++ b/public/include/pages/home.inc.php @@ -13,7 +13,17 @@ foreach ($aNews as $key => $aData) { $aNews[$key]['content'] = Markdown::defaultTransform($aData['content']); } -// Tempalte specifics +// Load news entries in case news is the homepage $smarty->assign("NEWS", $aNews); -$smarty->assign("CONTENT", "default.tpl"); + +// Tempalte specifics +if ($detect->isMobile()) { + if ($config['payout_system'] == 'pps') { + $smarty->assign("CONTENT", "pps.tpl"); + } else { + $smarty->assign("CONTENT", "default.tpl"); + } +} else { + $smarty->assign("CONTENT", "default.tpl"); +} ?> diff --git a/public/include/pages/news.inc.php b/public/include/pages/news.inc.php new file mode 100644 index 00000000..38e1022d --- /dev/null +++ b/public/include/pages/news.inc.php @@ -0,0 +1,19 @@ +getAllActive(); +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/templates/mmcFE/news/default.tpl b/public/templates/mmcFE/news/default.tpl new file mode 100644 index 00000000..e898af86 --- /dev/null +++ b/public/templates/mmcFE/news/default.tpl @@ -0,0 +1,5 @@ +{section name=news loop=$NEWS} + {include file="global/block_header.tpl" BLOCK_HEADER="{$NEWS[news].header}, posted {$NEWS[news].time|date_format:"%b %e, %Y at %H:%M"} by {$NEWS[news].author}"} + {$NEWS[news].content} + {include file="global/block_footer.tpl"} +{/section} diff --git a/public/templates/mobile/global/navigation.tpl b/public/templates/mobile/global/navigation.tpl index 2821454f..92785791 100644 --- a/public/templates/mobile/global/navigation.tpl +++ b/public/templates/mobile/global/navigation.tpl @@ -1,6 +1,7 @@
      -
    • Home
    • +
    • Dash
    • +
    • News
    • {if $smarty.session.AUTHENTICATED|default:"0" == 1}
    • Statistics
    • Logout
    • diff --git a/public/templates/mobile/home/default.tpl b/public/templates/mobile/home/default.tpl index 69ecd30d..7fdef4c8 100644 --- a/public/templates/mobile/home/default.tpl +++ b/public/templates/mobile/home/default.tpl @@ -1,8 +1,51 @@ -{section name=news loop=$NEWS} -
      -
      -

      {$NEWS[news].header}

      -

      {$NEWS[news].content}

      -
      -
      -{/section} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Your Stats
      Hashrate{$GLOBAL.userdata.hashrate|number_format} KH/s
      Round Shares
      Pool Valid{$GLOBAL.roundshares.valid|number_format}
      Pool Invalid{$GLOBAL.roundshares.invalid|number_format} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid)|number_format:"2"}%)
      Your Valid{$GLOBAL.userdata.shares.valid|number_format}
      Your Invalid{$GLOBAL.userdata.shares.invalid|number_format} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid)|number_format:"2"}%)
      {$GLOBAL.config.currency} Round Estimate
      Block{$GLOBAL.userdata.est_block|number_format:"3"}
      Fees{$GLOBAL.userdata.est_fee|number_format:"3"}
      Donation{$GLOBAL.userdata.est_donation|number_format:"3"}
      Payout{$GLOBAL.userdata.est_payout|number_format:"3"}
       
      {$GLOBAL.config.currency} Account Balance
      Confirmed{$GLOBAL.userdata.balance.confirmed|default:"0"}
      Unconfirmed{$GLOBAL.userdata.balance.unconfirmed|default:"0"}
      diff --git a/public/templates/mobile/home/pps.tpl b/public/templates/mobile/home/pps.tpl new file mode 100644 index 00000000..bbabab3b --- /dev/null +++ b/public/templates/mobile/home/pps.tpl @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Your Stats
      Hashrate{$GLOBAL.userdata.hashrate|number_format} KH/s
      Share Rate{$GLOBAL.userdata.sharerate|number_format:"2"} S/s
      PPS Value{$GLOBAL.ppsvalue}
      Round Shares
      Pool Valid{$GLOBAL.roundshares.valid|number_format}
      Pool Invalid{$GLOBAL.roundshares.invalid|number_format} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid)|number_format:"2"}%)
      Your Invalid{$GLOBAL.userdata.shares.invalid|number_format} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid)|number_format:"2"}%)
       
      {$GLOBAL.config.currency} Estimates
      in 24 hours{($GLOBAL.userdata.sharerate * 24 * 60 * 60 * $GLOBAL.ppsvalue)|number_format:"8"}
      in 7 days{($GLOBAL.userdata.sharerate * 7 * 24 * 60 * 60 * $GLOBAL.ppsvalue)|number_format:"8"}
      in 14 days{($GLOBAL.userdata.sharerate * 14 * 24 * 60 * 60 * $GLOBAL.ppsvalue)|number_format:"8"}
       
      {$GLOBAL.config.currency} Account Balance
      Confirmed{$GLOBAL.userdata.balance.confirmed|default:"0"}
      Unconfirmed{$GLOBAL.userdata.balance.unconfirmed|default:"0"}
      diff --git a/public/templates/mobile/news/default.tpl b/public/templates/mobile/news/default.tpl new file mode 100644 index 00000000..69ecd30d --- /dev/null +++ b/public/templates/mobile/news/default.tpl @@ -0,0 +1,8 @@ +{section name=news loop=$NEWS} +
      +
      +

      {$NEWS[news].header}

      +

      {$NEWS[news].content}

      +
      +
      +{/section} From f9f776a0156a24329c26d61e3edb44c5c1ef7ad6 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sat, 29 Jun 2013 15:26:16 +0200 Subject: [PATCH 046/650] Display newspage on Mobile if not authed * Do not show dashboard if user is not logged in * Show news on Mobile and Desktop by default * Show Dasboard on mobile once logged in * Make News Navbar item default for unauthed users on mobile * Add Dash Navbar item for authed users on mobile --- public/include/pages/home.inc.php | 6 +++--- public/templates/mobile/global/navigation.tpl | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/public/include/pages/home.inc.php b/public/include/pages/home.inc.php index ffd08520..ac846304 100644 --- a/public/include/pages/home.inc.php +++ b/public/include/pages/home.inc.php @@ -13,17 +13,17 @@ foreach ($aNews as $key => $aData) { $aNews[$key]['content'] = Markdown::defaultTransform($aData['content']); } -// Load news entries in case news is the homepage +// Load news entries for Desktop site and unauthenticated users $smarty->assign("NEWS", $aNews); // Tempalte specifics -if ($detect->isMobile()) { +if ($detect->isMobile() && $_SESSION['AUTHENTICATED'] == true) { if ($config['payout_system'] == 'pps') { $smarty->assign("CONTENT", "pps.tpl"); } else { $smarty->assign("CONTENT", "default.tpl"); } } else { - $smarty->assign("CONTENT", "default.tpl"); + $smarty->assign("CONTENT", "../news/default.tpl"); } ?> diff --git a/public/templates/mobile/global/navigation.tpl b/public/templates/mobile/global/navigation.tpl index 92785791..4fd17134 100644 --- a/public/templates/mobile/global/navigation.tpl +++ b/public/templates/mobile/global/navigation.tpl @@ -1,11 +1,12 @@
      From f6242f2c740b74c8c5a2d7db01b4943d9699a23a Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sat, 29 Jun 2013 21:26:58 +0200 Subject: [PATCH 047/650] Adding swipable sidebar * Login to mobile version to enable sidebar * Swipe right to display your old dashboard * Modified `home.inc.php` to default to news at all times again * Modified sidebar implementation to change via config setting * Modified mobile navigation bar and header * Added `sidebar_prop.tpl` file for both themes Some breaking changes might be introduced here if you are running your own template implementation. Please ensure that `home/default.tpl` will display the news posts. `sidebar.tpl` is now `sidebar_prop.tpl`. If the files are missing you will get a PHP error. Check your logs what file is missing and create them from my original samples. Fixes #283 --- public/include/pages/home.inc.php | 12 +--- .../global/{sidebar.tpl => sidebar_prop.tpl} | 0 public/templates/mmcFE/master.tpl | 7 +-- public/templates/mmcFE/news/default.tpl | 5 -- public/templates/mobile/global/navigation.tpl | 8 ++- .../{home/pps.tpl => global/sidebar_pps.tpl} | 16 ++--- .../templates/mobile/global/sidebar_prop.tpl | 54 +++++++++++++++++ public/templates/mobile/home/default.tpl | 59 +++---------------- public/templates/mobile/master.tpl | 30 +++++++++- public/templates/mobile/news/default.tpl | 8 --- 10 files changed, 107 insertions(+), 92 deletions(-) rename public/templates/mmcFE/global/{sidebar.tpl => sidebar_prop.tpl} (100%) delete mode 100644 public/templates/mmcFE/news/default.tpl rename public/templates/mobile/{home/pps.tpl => global/sidebar_pps.tpl} (71%) create mode 100644 public/templates/mobile/global/sidebar_prop.tpl delete mode 100644 public/templates/mobile/news/default.tpl diff --git a/public/include/pages/home.inc.php b/public/include/pages/home.inc.php index ac846304..2d0f5b23 100644 --- a/public/include/pages/home.inc.php +++ b/public/include/pages/home.inc.php @@ -15,15 +15,5 @@ foreach ($aNews as $key => $aData) { // Load news entries for Desktop site and unauthenticated users $smarty->assign("NEWS", $aNews); - -// Tempalte specifics -if ($detect->isMobile() && $_SESSION['AUTHENTICATED'] == true) { - if ($config['payout_system'] == 'pps') { - $smarty->assign("CONTENT", "pps.tpl"); - } else { - $smarty->assign("CONTENT", "default.tpl"); - } -} else { - $smarty->assign("CONTENT", "../news/default.tpl"); -} +$smarty->assign("CONTENT", "default.tpl"); ?> diff --git a/public/templates/mmcFE/global/sidebar.tpl b/public/templates/mmcFE/global/sidebar_prop.tpl similarity index 100% rename from public/templates/mmcFE/global/sidebar.tpl rename to public/templates/mmcFE/global/sidebar_prop.tpl diff --git a/public/templates/mmcFE/master.tpl b/public/templates/mmcFE/master.tpl index 8709e974..de478b38 100644 --- a/public/templates/mmcFE/master.tpl +++ b/public/templates/mmcFE/master.tpl @@ -47,11 +47,8 @@
      From bb0e9dff3915c3db9f109b74304d6b5ccf234c62 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 8 Jul 2013 14:16:45 +0200 Subject: [PATCH 146/650] disable smarty cache by default in dist --- public/include/config/global.inc.dist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index e335405c..49692646 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -288,6 +288,6 @@ $config['cookie']['domain'] = ''; * cache = 0, disabled * cache_lifetime = 30 seconds **/ -$config['smarty']['cache'] = 1; +$config['smarty']['cache'] = 0; $config['smarty']['cache_lifetime'] = 30; ?> From d25387f0b597923ff4e8b7b71943c84279535c72 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 8 Jul 2013 14:16:52 +0200 Subject: [PATCH 147/650] Disable caching check on Smarty globals This will ensure data is available for those pages relying on global data. A better step might be to load template specific data always on the pages that require the data instead of relying on global data to be available. Fixes #309 --- public/include/smarty_globals.inc.php | 216 +++++++++++++------------- 1 file changed, 106 insertions(+), 110 deletions(-) diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 0f1e2896..8f543699 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -2,121 +2,117 @@ // Make sure we are called from index.php if (!defined('SECURITY')) - die('Hacking attempt'); + die('Hacking attempt'); // Globally available variables $debug->append('Global smarty variables', 3); -if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { - $debug->append('No cached page detected, loading smarty globals', 3); - // Defaults to get rid of PHP Notice warnings - $dDifficulty = 1; - $aRoundShares = 1; +$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'); - if (is_array($dDifficulty) && array_key_exists('proof-of-work', $dDifficulty)) - $dDifficulty = $dDifficulty['proof-of-work']; - } +// Only run these if the user is logged in +if (@$_SESSION['AUTHENTICATED']) { + $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 - $bitcoin->can_connect() === true ? $dNetworkHashrate = $bitcoin->query('getnetworkhashps') : $dNetworkHashrate = 0; - - // Fetch some data - $iCurrentActiveWorkers = $worker->getCountAllActiveWorkers(); - $iCurrentPoolHashrate = $statistics->getCurrentHashrate(); - $iCurrentPoolShareRate = $statistics->getCurrentShareRate(); - - // Avoid confusion, ensure our nethash isn't higher than poolhash - if ($iCurrentPoolHashrate > $dNetworkHashrate) $dNetworkHashrate = $iCurrentPoolHashrate; - - // Global data for Smarty - $aGlobal = array( - 'slogan' => $config['website']['slogan'], - 'websitename' => $config['website']['name'], - 'hashrate' => $iCurrentPoolHashrate, - 'nethashrate' => $dNetworkHashrate, - 'sharerate' => $iCurrentPoolShareRate, - 'workers' => $iCurrentActiveWorkers, - 'roundshares' => $aRoundShares, - 'fees' => $config['fees'], - 'confirmations' => $config['confirmations'], - 'reward' => $config['reward'], - 'price' => $setting->getValue('price'), - 'blockexplorer' => $config['blockexplorer'], - 'chaininfo' => $config['chaininfo'], - 'config' => array( - 'website' => array( 'title' => $config['website']['title'] ), - 'price' => array( 'currency' => $config['price']['currency'] ), - 'targetdiff' => $config['difficulty'], - 'currency' => $config['currency'], - 'txfee' => $config['txfee'], - 'payout_system' => $config['payout_system'], - 'ap_threshold' => array( - 'min' => $config['ap_threshold']['min'], - 'max' => $config['ap_threshold']['max'] - ) - ) - ); - - // Special calculations for PPS Values based on reward_type setting and/or available blocks - if ($config['reward_type'] != 'block') { - $aGlobal['ppsvalue'] = number_format(round(50 / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); - } else { - // Try to find the last block value and use that for future payouts, revert to fixed reward if none found - if ($aLastBlock = $block->getLast()) { - $aGlobal['ppsvalue'] = number_format(round($aLastBlock['amount'] / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); - } else { - $aGlobal['ppsvalue'] = number_format(round($config['reward'] / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 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(); - $aGlobal['userdata']['balance'] = $transaction->getBalance($_SESSION['USERDATA']['id']); - - // Other userdata that we can cache savely - $aGlobal['userdata']['shares'] = $statistics->getUserShares($_SESSION['USERDATA']['id']); - $aGlobal['userdata']['hashrate'] = $statistics->getUserHashrate($_SESSION['USERDATA']['id']); - $aGlobal['userdata']['sharerate'] = $statistics->getUserSharerate($_SESSION['USERDATA']['id']); - - switch ($config['payout_system']) { - case 'pps': - break; - default: - // 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']['est_fee'] = round(((float)$config['fees'] / 100) * (float)$aGlobal['userdata']['est_block'], 8); - $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; - } - break; - } - - // Site-wide notifications, based on user events - if ($aGlobal['userdata']['balance']['confirmed'] >= $config['ap_threshold']['max']) - $_SESSION['POPUP'][] = array('CONTENT' => 'You have exceeded your accounts balance. Please transfer some ' . $config['currency'] . "!", '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'); - - // Make it available in Smarty - $smarty->assign('PATH', 'site_assets/' . THEME); - $smarty->assign('GLOBAL', $aGlobal); -} else { - $debug->append('We found a cached page, not loaded smarty globals', 3); } +// Always fetch this since we need for ministats header +$bitcoin->can_connect() === true ? $dNetworkHashrate = $bitcoin->query('getnetworkhashps') : $dNetworkHashrate = 0; + +// Fetch some data +$iCurrentActiveWorkers = $worker->getCountAllActiveWorkers(); +$iCurrentPoolHashrate = $statistics->getCurrentHashrate(); +$iCurrentPoolShareRate = $statistics->getCurrentShareRate(); + +// Avoid confusion, ensure our nethash isn't higher than poolhash +if ($iCurrentPoolHashrate > $dNetworkHashrate) $dNetworkHashrate = $iCurrentPoolHashrate; + +// Global data for Smarty +$aGlobal = array( + 'slogan' => $config['website']['slogan'], + 'websitename' => $config['website']['name'], + 'hashrate' => $iCurrentPoolHashrate, + 'nethashrate' => $dNetworkHashrate, + 'sharerate' => $iCurrentPoolShareRate, + 'workers' => $iCurrentActiveWorkers, + 'roundshares' => $aRoundShares, + 'fees' => $config['fees'], + 'confirmations' => $config['confirmations'], + 'reward' => $config['reward'], + 'price' => $setting->getValue('price'), + 'blockexplorer' => $config['blockexplorer'], + 'chaininfo' => $config['chaininfo'], + 'config' => array( + 'website' => array( 'title' => $config['website']['title'] ), + 'price' => array( 'currency' => $config['price']['currency'] ), + 'targetdiff' => $config['difficulty'], + 'currency' => $config['currency'], + 'txfee' => $config['txfee'], + 'payout_system' => $config['payout_system'], + 'ap_threshold' => array( + 'min' => $config['ap_threshold']['min'], + 'max' => $config['ap_threshold']['max'] + ) + ) +); + +// Special calculations for PPS Values based on reward_type setting and/or available blocks +if ($config['reward_type'] != 'block') { + $aGlobal['ppsvalue'] = number_format(round(50 / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); +} else { + // Try to find the last block value and use that for future payouts, revert to fixed reward if none found + if ($aLastBlock = $block->getLast()) { + $aGlobal['ppsvalue'] = number_format(round($aLastBlock['amount'] / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); + } else { + $aGlobal['ppsvalue'] = number_format(round($config['reward'] / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 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(); + $aGlobal['userdata']['balance'] = $transaction->getBalance($_SESSION['USERDATA']['id']); + + // Other userdata that we can cache savely + $aGlobal['userdata']['shares'] = $statistics->getUserShares($_SESSION['USERDATA']['id']); + $aGlobal['userdata']['hashrate'] = $statistics->getUserHashrate($_SESSION['USERDATA']['id']); + $aGlobal['userdata']['sharerate'] = $statistics->getUserSharerate($_SESSION['USERDATA']['id']); + + switch ($config['payout_system']) { + case 'pps': + break; + default: + // 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']['est_fee'] = round(((float)$config['fees'] / 100) * (float)$aGlobal['userdata']['est_block'], 8); + $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; + } + break; + } + + // Site-wide notifications, based on user events + if ($aGlobal['userdata']['balance']['confirmed'] >= $config['ap_threshold']['max']) + $_SESSION['POPUP'][] = array('CONTENT' => 'You have exceeded your accounts balance. Please transfer some ' . $config['currency'] . "!", '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'); + +// Make it available in Smarty +$smarty->assign('PATH', 'site_assets/' . THEME); +$smarty->assign('GLOBAL', $aGlobal); ?> From 308b01c700e899d614a0d5bdea36a87872aa9310 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 8 Jul 2013 15:40:21 +0200 Subject: [PATCH 148/650] Better number format for block percentages --- public/templates/mmcFE/statistics/blocks/default.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mmcFE/statistics/blocks/default.tpl b/public/templates/mmcFE/statistics/blocks/default.tpl index 539897c7..af30f093 100644 --- a/public/templates/mmcFE/statistics/blocks/default.tpl +++ b/public/templates/mmcFE/statistics/blocks/default.tpl @@ -68,7 +68,7 @@ target and network difficulty and assuming a zero variance scenario. {$BLOCKSFOUND[block].shares|number_format} {math assign="percentage" equation="shares / estshares * 100" shares=$BLOCKSFOUND[block].shares estshares=$estshares} - {$percentage} + {$percentage|number_format:"2"} {/section} From 3da5a226e16e1f7ebd1ff3e032bc2f74db7a421d Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 8 Jul 2013 15:42:23 +0200 Subject: [PATCH 149/650] Better number format for est. shares --- public/templates/mmcFE/statistics/blocks/default.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mmcFE/statistics/blocks/default.tpl b/public/templates/mmcFE/statistics/blocks/default.tpl index af30f093..f404673c 100644 --- a/public/templates/mmcFE/statistics/blocks/default.tpl +++ b/public/templates/mmcFE/statistics/blocks/default.tpl @@ -63,7 +63,7 @@ target and network difficulty and assuming a zero variance scenario. {$BLOCKSFOUND[block].amount|number_format:"2"} {math assign="estshares" equation="(pow(2,32 - targetdiff) * blockdiff)" targetdiff=$GLOBAL.config.targetdiff blockdiff=$BLOCKSFOUND[block].difficulty} - {$estshares} + {$estshares|number_format} {$BLOCKSFOUND[block].shares|number_format} From 0f69032fd3dad9d06d226db70bd14d4d9ee56838 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 8 Jul 2013 17:10:58 +0200 Subject: [PATCH 150/650] Adding 3rd party Scrypt library This will allow us to start checking a blockhash against a solution submitted to the database. Details on this in the ticket. Just a WIP to save file states. Addresses #405 --- public/include/autoloader.inc.php | 1 + public/include/lib/scrypt.php | 526 ++++++++++++++++++++++++++++++ 2 files changed, 527 insertions(+) create mode 100644 public/include/lib/scrypt.php diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index c74fa62b..68109049 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -33,3 +33,4 @@ require_once(CLASS_DIR . '/mail.class.php'); require_once(CLASS_DIR . '/notification.class.php'); require_once(CLASS_DIR . '/news.class.php'); require_once(INCLUDE_DIR . '/lib/Michelf/Markdown.php'); +require_once(INCLUDE_DIR . '/lib/scrypt.php'); diff --git a/public/include/lib/scrypt.php b/public/include/lib/scrypt.php new file mode 100644 index 00000000..87ddff2e --- /dev/null +++ b/public/include/lib/scrypt.php @@ -0,0 +1,526 @@ + 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))); + */ + + +?> From 4c531360c1e011e4f745feb1c8852d854c6c437e Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 08:54:20 +0200 Subject: [PATCH 151/650] fixing empty variable when using cache --- public/include/pages/home.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/pages/home.inc.php b/public/include/pages/home.inc.php index ac7b458e..9431e219 100644 --- a/public/include/pages/home.inc.php +++ b/public/include/pages/home.inc.php @@ -16,11 +16,11 @@ if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { $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("NEWS", $aNews); $smarty->assign("CONTENT", "default.tpl"); ?> From 7f32bbb7bac256137d76814dbe9c01c40e2875dd Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 08:58:46 +0200 Subject: [PATCH 152/650] Adding more descriptive message for txfee Fixes #400 --- public/include/config/global.inc.dist.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index c5a298c7..9390b534 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -133,7 +133,19 @@ $config['recaptcha']['private_key'] = 'YOUR_PRIVATE_RECAPTCHA_KEY'; // Currency system used in this pool, default: `LTC` $config['currency'] = 'LTC'; -// Default transaction fee, added by RPC server, default: 0.1 +/** + * 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) From f6b350370d9488314b4d3033844af36a4f64e92b Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 11:27:20 +0200 Subject: [PATCH 153/650] Adding solution detections for blocks This will finally fix all block finding issues with a 4 way detection. The find upstream method will continue to try other ways to find a proper share until they are all exhausted or a match was found. * Use stratum solution, create scrypt hash from block header * Use pushpoold solution, create solution string from block header * Use first available upstream share in timerange of block time * Use *any* first available valid share older than time of block This will fix #405 - no more unknown blocks. Ever. --- cronjobs/findblock.php | 3 +- public/include/classes/share.class.php | 44 +++++++++++++++++++++++--- public/include/lib/scrypt.php | 10 ++++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index 28dddb4b..5c168448 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -72,7 +72,8 @@ if (empty($aAllBlocks)) { foreach ($aAllBlocks as $iIndex => $aBlock) { if (empty($aBlock['share_id'])) { // Fetch this blocks upstream ID - if ($share->setUpstream($block->getLastUpstreamId(), $aBlock['time'])) { + $aBlockInfo = $bitcoin->query('getblock', $aBlock['blockhash']); + if ($share->setUpstream($aBlockInfo, $block->getLastUpstreamId())) { $iCurrentUpstreamId = $share->getUpstreamId(); $iAccountId = $user->getUserId($share->getUpstreamFinder()); } else { diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index 5e4db85e..ca5b1478 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -177,7 +177,41 @@ class Share { * @param last int Skips all shares up to last to find new share * @return bool **/ - public function setUpstream($last=0, $time=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)); + $scrypt_hash = swapEndian(bin2hex(Scrypt::calc($header_bin, $header_bin, 1024, 1, 1, 32))); + + + // 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']); + echo "ppheader : $ppheader \n"; + echo "header : $header_hex \n"; + echo "Scrypt hash : $scrypt_hash \n"; + + $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()->num_rows > 0) { + $this->oUpstream = $result->fetch_object(); + if (!empty($this->oUpstream->account) && is_int($this->oUpstream->id)) + return true; + } + + // Failed to fetch via startum solution, try pushpoold + $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(); + 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 @@ -185,13 +219,15 @@ class Share { 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('ii', $last, $time) && $stmt->execute() && $result = $stmt->get_result()) { + if ($this->checkStmt($stmt) && $stmt->bind_param('iii', $last, $aBlock['time'], $Block['time']) && $stmt->execute() && $result = $stmt->get_result()) { $this->oUpstream = $result->fetch_object(); if (!empty($this->oUpstream->account) && is_int($this->oUpstream->id)) return true; } - // First attempt failed, we do a fallback with any share available for now + + // We failed again, now we take ANY result matching the timestamp $stmt = $this->mysqli->prepare(" SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, id @@ -200,7 +236,7 @@ class Share { AND id > ? AND UNIX_TIMESTAMP(time) >= ? ORDER BY id ASC LIMIT 1"); - if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $last, $time) && $stmt->execute() && $result = $stmt->get_result()) { + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $last, $aBlock['time']) && $stmt->execute() && $result = $stmt->get_result()) { $this->oUpstream = $result->fetch_object(); if (!empty($this->oUpstream->account) && is_int($this->oUpstream->id)) return true; diff --git a/public/include/lib/scrypt.php b/public/include/lib/scrypt.php index 87ddff2e..3d501b7d 100644 --- a/public/include/lib/scrypt.php +++ b/public/include/lib/scrypt.php @@ -523,4 +523,14 @@ $value = Scrypt::calc($i, $i, 1024, 1, 1, 32); */ +// 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; +} + ?> From e4f853716533af078ee8be8c7480a909791203f8 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 11:34:49 +0200 Subject: [PATCH 154/650] removing debug output --- public/include/classes/share.class.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index ca5b1478..4fea43c5 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -189,12 +189,8 @@ class Share { $header_hex = implode(unpack("H*", $header_bin)); $scrypt_hash = swapEndian(bin2hex(Scrypt::calc($header_bin, $header_bin, 1024, 1, 1, 32))); - // 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']); - echo "ppheader : $ppheader \n"; - echo "header : $header_hex \n"; - echo "Scrypt hash : $scrypt_hash \n"; $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()->num_rows > 0) { From 7f759708c8b77814823007ba2bcec680eab8359c Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 11:43:12 +0200 Subject: [PATCH 155/650] Adding share type to log output This will display which detection mechanism caught the share. Fixed stratum detection Fixes #405 --- cronjobs/findblock.php | 5 +++-- public/include/classes/share.class.php | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index 5c168448..b98bbc3a 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -68,7 +68,7 @@ if (empty($aAllBlocks)) { $log->logDebug('No new blocks without share_id found in database'); } else { // Loop through our unaccounted blocks - $log->logInfo("Block ID\t\tHeight\tAmount\tShare ID\tShares\tFinder"); + $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 @@ -105,7 +105,8 @@ if (empty($aAllBlocks)) { . $aBlock['amount'] . "\t" . $iCurrentUpstreamId . "\t\t" . $iRoundShares . "\t" - . "[$iAccountId] " . $user->getUserName($iAccountId) + . "[$iAccountId] " . $user->getUserName($iAccountId) . "\t" + . $share->share_type ); // Notify users diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index 4fea43c5..e3f6e33d 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -193,8 +193,9 @@ class Share { $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 = ? LIMIT 1"); - if ($this->checkStmt($stmt) && $stmt->bind_param('s', $scrypt_hash) && $stmt->execute() && $result = $stmt->get_result()->num_rows > 0) { + 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; } @@ -203,6 +204,7 @@ class Share { $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; } @@ -219,6 +221,7 @@ class Share { ORDER BY id ASC LIMIT 1"); if ($this->checkStmt($stmt) && $stmt->bind_param('iii', $last, $aBlock['time'], $Block['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; } @@ -234,6 +237,7 @@ class Share { 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; } From abef09b08c8465fd03ad864439387392aca51580 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 14:50:23 +0200 Subject: [PATCH 156/650] Added blockhash solution checking Stratum of @moopless supports blockhash as a solution. Check for that one first. Addresses #405 --- public/include/classes/share.class.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index e3f6e33d..c2ce65e2 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -187,11 +187,18 @@ class Share { $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))); - - // 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 = ? LIMIT 1"); if ($this->checkStmt($stmt) && $stmt->bind_param('s', $scrypt_hash) && $stmt->execute() && $result = $stmt->get_result()) { $this->oUpstream = $result->fetch_object(); @@ -201,6 +208,8 @@ class Share { } // 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(); From 52e02a545b41fb50780e8b7e367da1a24ae7f81e Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 15:43:42 +0200 Subject: [PATCH 157/650] fixing issue with upstream shares not being found --- public/include/classes/share.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index c2ce65e2..496652f0 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -226,9 +226,9 @@ class Share { WHERE upstream_result = 'Y' AND id > ? AND UNIX_TIMESTAMP(time) >= ? - AND UNIX_TIMESTAMP(time) <= ? + 60 + AND UNIX_TIMESTAMP(time) <= ( ? + 60 ) ORDER BY id ASC LIMIT 1"); - if ($this->checkStmt($stmt) && $stmt->bind_param('iii', $last, $aBlock['time'], $Block['time']) && $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)) From b33f572774814964ba2d231212a381e4061b71f7 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 16:13:30 +0200 Subject: [PATCH 158/650] adding missing PHP closing tag --- public/include/pages/statistics.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/public/include/pages/statistics.inc.php b/public/include/pages/statistics.inc.php index dc27fde6..3307dd21 100644 --- a/public/include/pages/statistics.inc.php +++ b/public/include/pages/statistics.inc.php @@ -18,3 +18,4 @@ if ($bitcoin->can_connect() === true){ $smarty->assign("CURRENTBLOCK", $iBlock); $smarty->assign("DIFFICULTY", $dDifficulty); $smarty->assign("CONTENT", "default.tpl"); +?> From 5acebc37d3c1f0439c4ed0bb9ae8035305c360e4 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 16:13:43 +0200 Subject: [PATCH 159/650] Allow guest access to pages with config options This will allow pool owners to make certain pages public accessible without user registration or login. Please check the config dist file for details. Fixes #408 --- public/include/config/global.inc.dist.php | 16 ++++++++++++++++ public/include/pages/statistics/blocks.inc.php | 7 +++++-- public/include/pages/statistics/pool.inc.php | 4 +++- public/include/smarty_globals.inc.php | 2 +- public/templates/mmcFE/global/navigation.tpl | 8 ++++++++ .../statistics/pool/contributors_hashrate.tpl | 2 +- .../statistics/pool/contributors_shares.tpl | 2 +- 7 files changed, 35 insertions(+), 6 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 9390b534..162bba00 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -121,6 +121,22 @@ $config['website']['theme'] = 'mmcFE'; $config['website']['mobile'] = true; $config['website']['mobile_theme'] = 'mobile'; +/** + * Some basic access restrictions on some pages + * + * Explanation: + * Some pools would like to run a few pages for public access instead + * of enforcing a login. You can change visibility of some pages here. + * + * Options: + * 'public' : Allow guest access and authenticated user to view page + * 'private' : Only allow logged in users access to view page + * + * Defaults: + * 'private' for every page + **/ +$config['website']['acl']['statistics']['pool'] = 'private'; +$config['website']['acl']['statistics']['blocks'] = 'private'; /** * Re-Captcha settings diff --git a/public/include/pages/statistics/blocks.inc.php b/public/include/pages/statistics/blocks.inc.php index 6c3b00b8..22eeadff 100644 --- a/public/include/pages/statistics/blocks.inc.php +++ b/public/include/pages/statistics/blocks.inc.php @@ -2,7 +2,6 @@ // 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 = 20; @@ -12,5 +11,9 @@ $aBlocksFoundData = $statistics->getBlocksFound($iLimit); $smarty->assign("BLOCKSFOUND", $aBlocksFoundData); $smarty->assign("BLOCKLIMIT", $iLimit); -$smarty->assign("CONTENT", "default.tpl"); +if ($config['website']['acl']['statistics']['blocks'] == 'public') { + $smarty->assign("CONTENT", "default.tpl"); +} else if ($user->isAuthenticated()) { + $smarty->assign("CONTENT", "default.tpl"); +} ?> diff --git a/public/include/pages/statistics/pool.inc.php b/public/include/pages/statistics/pool.inc.php index 1cab1009..27395cc1 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -53,7 +53,9 @@ count($aBlockData) > 0 ? $smarty->assign("LASTBLOCK", $aBlockData['height']) : $ $smarty->assign("DIFFICULTY", $dDifficulty); $smarty->assign("REWARD", $config['reward']); -if ($user->isAuthenticated()) { +if ($config['website']['acl']['statistics']['pool'] == 'public') { + $smarty->assign("CONTENT", "authenticated.tpl"); +} else if ($user->isAuthenticated() && $config['website']['acl']['statistics']['pool'] == 'private') { $smarty->assign("CONTENT", "authenticated.tpl"); } else { $smarty->assign("CONTENT", "../default.tpl"); diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 147b38e9..be65e39e 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -47,7 +47,7 @@ $aGlobal = array( 'blockexplorer' => $config['blockexplorer'], 'chaininfo' => $config['chaininfo'], 'config' => array( - 'website' => array( 'title' => $config['website']['title'] ), + 'website' => array( 'title' => $config['website']['title'], 'acl' => $config['website']['acl'] ), 'price' => array( 'currency' => $config['price']['currency'] ), 'targetdiff' => $config['difficulty'], 'currency' => $config['currency'], diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 70619098..6cafd1f6 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -31,6 +31,14 @@ {else}
    • Statistics +
        + {if $GLOBAL.config.website.acl.statistics.pool == 'public'} +
      • Pool Stats
      • + {/if} + {if $GLOBAL.config.website.acl.statistics.blocks == 'public'} +
      • Block Stats
      • + {/if} +
      {/if}
    • Getting Started
    • Support
    • diff --git a/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl b/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl index 7b294265..a2a6ed58 100644 --- a/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl +++ b/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl @@ -23,7 +23,7 @@ {if $GLOBAL.config.price.currency}{($estday * $GLOBAL.price)|default:"n/a"|number_format:"2"}{/if} {/section} -{if $listed != 1} +{if $listed != 1 && $GLOBAL.userdata.username|default:""} {if $GLOBAL.userdata.hashrate > 0}{math assign="myestday" equation="round(reward / ( diff * pow(2,32) / ( hashrate * 1000 ) / 3600 / 24), 3)" diff=$DIFFICULTY reward=$REWARD hashrate=$GLOBAL.userdata.hashrate}{/if} n/a diff --git a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl index 444effa4..2a482209 100644 --- a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl +++ b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl @@ -18,7 +18,7 @@ {$CONTRIBSHARES[hashrate].shares|number_format} {/section} -{if $listed != 1} +{if $listed != 1 && $GLOBAL.userdata.username|default:""} n/a {$GLOBAL.userdata.username} From 9e878b06d5aebe9986e839a000a14f46fb54f402 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 16:29:00 +0200 Subject: [PATCH 160/650] Ensure people know what the memcache setting does Even though memcache can be disabled, the memcache library is a requirement as listed in the README.md. I clarified that in the configuration file itself to ensure people install it even though they might be disabling it for debugging purpose. Fixes #409 --- public/include/config/global.inc.dist.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 9390b534..b939a0e4 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -229,11 +229,15 @@ $config['confirmations'] = 120; /** * Memcache configuration * - * 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! + * Even though you can disable the memcache for debugging purposes, the memcache + * library is still required for mmcfe-ng to work. You should not disable this in + * a live environment since a lot of data is cached for the website to increase load + * times! * * 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 mmcfe-ng instances on one host * expiration : Default expiration time in seconds of all cached keys. * Increase if caches expire too fast. From dc51d874a74fd33231b30167ccd14bf6491c2fca Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 21:26:06 +0200 Subject: [PATCH 161/650] Adding block height to blockupdate output This should make tracing block updates easier. --- cronjobs/blockupdate.php | 4 ++-- public/include/classes/block.class.php | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/cronjobs/blockupdate.php b/cronjobs/blockupdate.php index 89a27be1..d257a69d 100755 --- a/cronjobs/blockupdate.php +++ b/cronjobs/blockupdate.php @@ -30,12 +30,12 @@ if ( $bitcoin->can_connect() !== true ) { // Fetch all unconfirmed blocks $aAllBlocks = $block->getAllUnconfirmed($config['confirmations']); -$log->logInfo("ID\tBlockhash\tConfirmations"); +$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]); - $log->logInfo($aBlock['id'] . "\t" . $aBlock['blockhash'] . "\t" . $aBlock['confirmations'] . " -> " . $aBlockInfo['confirmations']); + $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)) { diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index 2fcbad09..81d1806a 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -85,14 +85,9 @@ class Block { * @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(); + $stmt = $this->mysqli->prepare("SELECT id, height, 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()) return $result->fetch_all(MYSQLI_ASSOC); - } return false; } From 2f2acdad6dc4597ddfb84690ee8813883a600601 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 3 Jul 2013 18:57:36 +0200 Subject: [PATCH 162/650] First working version of PPLNS payouts * Based PPLNS on Prop Payout script * Using defaults from prop payout, no class adjustments * Added more methods required for PPLNS * Added block methods for dynamic payout calculations * Added PPLNS Sidebar that also displays the PPLNS Target * Shares beyond this target will not be included in payouts * Shares missing to this target will be added from archives * Enabled archiving by default for PPLNS * Added configuration options for PPLNS * Documented the usage for PPLNS, defaults are sane * Added pplns_payout to run-crons Addresses #143 and if accepted will fix it --- README.md | 4 +- cronjobs/pplns_payout.php | 142 ++++++++++++++++++ cronjobs/run-crons.sh | 2 +- public/include/classes/block.class.php | 24 +++ public/include/classes/share.class.php | 102 ++++++++----- public/include/config/global.inc.dist.php | 24 +++ public/include/smarty_globals.inc.php | 7 + .../templates/mmcFE/global/sidebar_pplns.tpl | 74 +++++++++ .../templates/mobile/global/sidebar_pplns.tpl | 63 ++++++++ 9 files changed, 399 insertions(+), 43 deletions(-) create mode 100755 cronjobs/pplns_payout.php create mode 100644 public/templates/mmcFE/global/sidebar_pplns.tpl create mode 100644 public/templates/mobile/global/sidebar_pplns.tpl diff --git a/README.md b/README.md index 14526524..885c5573 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,11 @@ Features The following feature have been implemented so far: -* Mobile WebUI **NEW** +* Mobile WebUI * Reward Systems * Propotional * PPS - * (Planned) PPLNS + * PPLNS **NEW** * Use of memcache for statistics instead of a cronjob * Web User accounts * Re-Captcha protected registration form diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php new file mode 100755 index 00000000..295702c9 --- /dev/null +++ b/cronjobs/pplns_payout.php @@ -0,0 +1,142 @@ +#!/usr/bin/php +getAllUnaccounted('ASC'); +if (empty($aAllBlocks)) { + verbose("No new unaccounted blocks found\n"); + exit(0); +} + +// We support some dynamic share targets but fall back to our fixed value +if ($config['pplns']['shares']['type'] == 'blockavg' && $block->getBlockCount() > 0) { + $pplns_target = round($block->getAvgBlockShares($config['pplns']['type']['blockavg']['blockcount'])); +} else { + $pplns_target = $config['pplns']['shares']['default'] ; +} + +$count = 0; +foreach ($aAllBlocks as $iIndex => $aBlock) { + if (!$aBlock['accounted']) { + $iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; + $iCurrentUpstreamId = $aBlock['share_id']; + $iRoundShares = $share->getRoundShares($iPreviousShareId, $aBlock['share_id']); + $config['reward_type'] == 'block' ? $dReward = $aBlock['amount'] : $dReward = $config['reward']; + $aRoundAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']); + + if ($iRoundShares >= $pplns_target) { + verbose("Matching or exceeding PPLNS target of $pplns_target\n"); + $aAccountShares = $share->getSharesForAccounts($aBlock['share_id'] - $pplns_target + 1, $aBlock['share_id']); + } else { + verbose("Not able to match PPLNS target of $pplns_target\n"); + // We need to fill up with archived shares + // Grab the full current round shares since we didn't match target + $aAccountShares = $aRoundAccountShares; + // Grab only the most recent shares from Archive that fill the missing shares + $aArchiveShares = $share->getArchiveShares($share->getMaxArchiveShareId() - ($pplns_target- $iRoundShares) + 1, $share->getMaxArchiveShareId()); + // Add archived shares to users current shares, if we have any in archive + if (is_array($aArchiveShares)) { + foreach($aAccountShares as $key => $aData) { + if (array_key_exists($aData['username'], $aArchiveShares)) { + $aAccountShares[$key]['valid'] += $aArchiveShares[$aData['username']]['valid']; + $aAccountShares[$key]['invalid'] += $aArchiveShares[$aData['username']]['invalid']; + } + } + } + } + if (empty($aAccountShares)) { + verbose("\nNo shares found for this block\n\n"); + sleep(2); + continue; + } + + // 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 PPLNS target shares, proportional payout for all users + $aData['percentage'] = number_format(round(( 100 / $pplns_target) * $aData['valid'], 8), 8); + $aData['payout'] = number_format(round(( $aData['percentage'] / 100 ) * $dReward, 8), 8); + // Defaults + $aData['fee' ] = 0; + $aData['donation'] = 0; + + if ($config['fees'] > 0) + $aData['fee'] = number_format(round($config['fees'] / 100 * $aData['payout'], 8), 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); + + // 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"); + + $strStatus = "OK"; + // Add full round share statistics, not just PPLNS + foreach ($aRoundAccountShares as $key => $aRoundData) { + if ($aRoundData['username'] == $aData['username']) + if (!$statistics->updateShareStatistics($aRoundData, $aBlock['id'])) + $strStatus = "Stats Failed"; + } + // Add new credit transaction + if (!$transaction->addTransaction($aData['id'], $aData['payout'], 'Credit', $aBlock['id'])) + $strStatus = "Transaction Failed"; + // Add new fee debit for this block + if ($aData['fee'] > 0 && $config['fees'] > 0) + if (!$transaction->addTransaction($aData['id'], $aData['fee'], 'Fee', $aBlock['id'])) + $strStatus = "Fee Failed"; + // 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"); + } + + // Move counted shares to archive before this blockhash upstream share + $share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId); + // Delete all accounted shares + if (!$share->deleteAccountedShares($iCurrentUpstreamId, $iPreviousShareId)) { + verbose("\nERROR : Failed to delete accounted shares from $iPreviousShareId to $iCurrentUpstreamId, aborting!\n"); + exit(1); + } + // Mark this block as accounted for + if (!$block->setAccounted($aBlock['id'])) { + verbose("\nERROR : Failed to mark block as accounted! Aborting!\n"); + } + + verbose("------------------------------------------------------------------------\n\n"); + } +} diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index c9dfd949..b6b241b4 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -16,7 +16,7 @@ PIDFILE='/tmp/mmcfe-ng-cron.pid' CRONHOME='.' # List of cruns to execute -CRONS="findblock.php proportional_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php" +CRONS="findblock.php proportional_payout.php pplns_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php" # Additional arguments to pass to cronjobs CRONARGS="-v" diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index 81d1806a..8f52efd9 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -79,6 +79,30 @@ class Block { 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($limit=10) { + $stmt = $this->mysqli->prepare("SELECT AVG(shares) AS average FROM $this->table LIMIT ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $limit) && $stmt->execute() && $result = $stmt->get_result()) + return (float)$result->fetch_object()->average; + return false; + } + /** * Fetch all unconfirmed blocks from table * @param confirmations int Required confirmations to consider block confirmed diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index 496652f0..ed6078ea 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -13,9 +13,10 @@ class Share { // This defines each share public $rem_host, $username, $our_result, $upstream_result, $reason, $solution, $time; - public function __construct($debug, $mysqli, $salt) { + public function __construct($debug, $mysqli, $user) { $this->debug = $debug; $this->mysqli = $mysqli; + $this->user = $user; $this->debug->append("Instantiated Share class", 2); } @@ -86,47 +87,67 @@ 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, + SUM(IF(our_result='Y', 1, 0)) AS valid, + SUM(IF(our_result='N', 1, 0)) 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 BETWEEN ? AND ? + 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 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($left, $right) { + $stmt = $this->mysqli->prepare(" + SELECT + a.id, + SUBSTRING_INDEX( s.username , '.', 1 ) as username, + SUM(IF(our_result='Y', 1, 0)) AS valid, + SUM(IF(our_result='N', 1, 0)) AS invalid + FROM $this->tableArchive AS s + LEFT JOIN " . $this->user->getTableName() . " AS a + ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) + WHERE s.id BETWEEN ? AND ? + GROUP BY username DESC + "); + if ($this->checkStmt($stmt) && $stmt->bind_param("ii", $left, $right) && $stmt->execute() && $result = $stmt->get_result()) { + $aData = NULL; + while ($row = $result->fetch_assoc()) { + $aData[$row['username']] = $row; + } + if (is_array($aData)) return $aData; + } + 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 +156,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) + SELECT id, username, our_result, upstream_result, ?, time + FROM $this->table + WHERE id BETWEEN ? AND ?"); if ($this->checkStmt($archive_stmt) && $archive_stmt->bind_param('iii', $block_id, $previous_upstream, $current_upstream) && $archive_stmt->execute()) { $archive_stmt->close(); return true; @@ -267,4 +289,4 @@ class Share { } } -$share = new Share($debug, $mysqli, SALT); +$share = new Share($debug, $mysqli, $user); diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 9390b534..995fc0b2 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -183,6 +183,30 @@ $config['chaininfo'] = 'http://allchains.info'; // 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 + * 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-mmcfe-ng/wiki/FAQ $config['difficulty'] = 20; diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 147b38e9..fd5083d6 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -83,6 +83,13 @@ if (@$_SESSION['USERDATA']['id']) { $aGlobal['userdata']['sharerate'] = $statistics->getUserSharerate($_SESSION['USERDATA']['id']); switch ($config['payout_system']) { + case 'pplns': + if ($iAvgBlockShares = round($block->getAvgBlockShares($config['pplns']['type']['blockavg']['blockcount']))) { + $aGlobal['pplns']['target'] = $iAvgBlockShares; + } else { + $aGlobal['pplns']['target'] = $config['pplns']['shares']['default']; + } + break; case 'pps': break; default: diff --git a/public/templates/mmcFE/global/sidebar_pplns.tpl b/public/templates/mmcFE/global/sidebar_pplns.tpl new file mode 100644 index 00000000..deb0c1cc --- /dev/null +++ b/public/templates/mmcFE/global/sidebar_pplns.tpl @@ -0,0 +1,74 @@ +
      +
      +
      +
      +

      Dashboard

      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      +
      +
      diff --git a/public/templates/mobile/global/sidebar_pplns.tpl b/public/templates/mobile/global/sidebar_pplns.tpl new file mode 100644 index 00000000..95dcf93c --- /dev/null +++ b/public/templates/mobile/global/sidebar_pplns.tpl @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      PPLNS Target{$GLOBAL.pplns.target|number_format}
       
      Your Stats
      Hashrate{$GLOBAL.userdata.hashrate|number_format} KH/s
      Unpaid Shares
      Your Valid{$GLOBAL.userdata.shares.valid|number_format}
      Pool Valid{$GLOBAL.roundshares.valid|number_format}
      Round Shares
      Pool Valid{$GLOBAL.roundshares.valid|number_format}
      Pool Invalid{$GLOBAL.roundshares.invalid|number_format}{if $GLOBAL.roundshares.valid > 0} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid)|number_format:"2"}%){/if}
      Your Invalid{$GLOBAL.userdata.shares.invalid|number_format}{if $GLOBAL.roundshares.valid > 0} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid)|number_format:"2"}%){/if}
      {$GLOBAL.config.currency} Round Estimate
      Block{$GLOBAL.userdata.est_block|number_format:"3"}
      Fees{$GLOBAL.userdata.est_fee|number_format:"3"}
      Donation{$GLOBAL.userdata.est_donation|number_format:"3"}
      Payout{$GLOBAL.userdata.est_payout|number_format:"3"}
       
      {$GLOBAL.config.currency} Account Balance
      Confirmed{$GLOBAL.userdata.balance.confirmed|default:"0"}
      Unconfirmed{$GLOBAL.userdata.balance.unconfirmed|default:"0"}
      From 394d8b52a94d22a1be835407345d8c44952d1fd4 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 7 Jul 2013 00:24:21 +0200 Subject: [PATCH 163/650] Abort payout process if missing share_id As discussed in #392 we should abort any payouts for rounds until the issue is fixed. --- cronjobs/pplns_payout.php | 1 + 1 file changed, 1 insertion(+) diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php index 295702c9..7249586d 100755 --- a/cronjobs/pplns_payout.php +++ b/cronjobs/pplns_payout.php @@ -47,6 +47,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { if (!$aBlock['accounted']) { $iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; $iCurrentUpstreamId = $aBlock['share_id']; + if (!is_number($iCurrentUpstreamId)) die("Block " . $aBlock['height'] . " has no share_id associated with it, not going to continue\n"); $iRoundShares = $share->getRoundShares($iPreviousShareId, $aBlock['share_id']); $config['reward_type'] == 'block' ? $dReward = $aBlock['amount'] : $dReward = $config['reward']; $aRoundAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']); From ba240000ab3cf1e9bc3dbdb96ca9788d3d73608e Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 8 Jul 2013 13:25:34 +0200 Subject: [PATCH 164/650] Latest version for PPLNS payouts * Rebased with next to add logging functions * Added block method to fetch specific block * Modified getArchive shares method to be more constistent * Added new global configuration * Adjusted smarty globals with new configuration * More verbose pplns cron with logging * Re-target the round in case of PPLNS not being matched by archive Fixes #143 --- cronjobs/pplns_payout.php | 116 ++++++++++++++-------- cronjobs/proportional_payout.php | 3 +- public/include/classes/block.class.php | 12 +++ public/include/classes/share.class.php | 51 ++++++++-- public/include/config/global.inc.dist.php | 31 +++++- public/include/smarty_globals.inc.php | 2 +- 6 files changed, 157 insertions(+), 58 deletions(-) diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php index 7249586d..827b598c 100755 --- a/cronjobs/pplns_payout.php +++ b/cronjobs/pplns_payout.php @@ -24,62 +24,90 @@ require_once('shared.inc.php'); // Check if we are set as the payout system if ($config['payout_system'] != 'pplns') { - verbose("Please activate this cron in configuration via payout_system = pplns\n"); + $log->logInfo("Please activate this cron in configuration via payout_system = pplns"); 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"); exit(0); } -// We support some dynamic share targets but fall back to our fixed value -if ($config['pplns']['shares']['type'] == 'blockavg' && $block->getBlockCount() > 0) { - $pplns_target = round($block->getAvgBlockShares($config['pplns']['type']['blockavg']['blockcount'])); -} else { - $pplns_target = $config['pplns']['shares']['default'] ; -} - $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($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_number($iCurrentUpstreamId)) die("Block " . $aBlock['height'] . " has no share_id associated with it, not going to continue\n"); + if (!is_numeric($iCurrentUpstreamId)) { + $log->logFatal("Block " . $aBlock['height'] . " has no share_id associated with it, not going to continue"); + 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) { - verbose("Matching or exceeding PPLNS target of $pplns_target\n"); + $log->logDebug("Matching or exceeding PPLNS target of $pplns_target with $iRoundShares"); $aAccountShares = $share->getSharesForAccounts($aBlock['share_id'] - $pplns_target + 1, $aBlock['share_id']); + if (empty($aAccountShares)) { + $log->logFatal("No shares found for this block, aborted!"); + exit(1); + } } else { - verbose("Not able to match PPLNS target of $pplns_target\n"); + $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!"); + exit(1); + } + // Grab only the most recent shares from Archive that fill the missing shares - $aArchiveShares = $share->getArchiveShares($share->getMaxArchiveShareId() - ($pplns_target- $iRoundShares) + 1, $share->getMaxArchiveShareId()); - // Add archived shares to users current shares, if we have any in archive - if (is_array($aArchiveShares)) { - foreach($aAccountShares as $key => $aData) { - if (array_key_exists($aData['username'], $aArchiveShares)) { - $aAccountShares[$key]['valid'] += $aArchiveShares[$aData['username']]['valid']; - $aAccountShares[$key]['invalid'] += $aArchiveShares[$aData['username']]['invalid']; + $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']; + } } } - if (empty($aAccountShares)) { - verbose("\nNo shares found for this block\n\n"); - sleep(2); - continue; + + // 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 - verbose("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee\t\tStatus\n"); + $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) { @@ -96,48 +124,50 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $aData['donation'] = number_format(round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8), 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" . + $aData['percentage'] . "\t" . + $aData['payout'] . "\t" . + $aData['donation'] . "\t" . + $aData['fee']); - $strStatus = "OK"; // Add full round share statistics, not just PPLNS foreach ($aRoundAccountShares as $key => $aRoundData) { if ($aRoundData['username'] == $aData['username']) if (!$statistics->updateShareStatistics($aRoundData, $aBlock['id'])) - $strStatus = "Stats Failed"; + $log->logError('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 - $share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId); + 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)) { - verbose("\nERROR : Failed to delete accounted shares from $iPreviousShareId to $iCurrentUpstreamId, aborting!\n"); + $log->logFatal("Failed to delete accounted shares from $iPreviousShareId to $iCurrentUpstreamId, aborting!"); exit(1); } + // If we don't keep archives, delete some now to release disk space + if (!$share->purgeArchive()) { + $log->logError("Failed to delete archived shares, not critical but should be checked!"); + } // 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!"); + exit(1); } - - verbose("------------------------------------------------------------------------\n\n"); } } diff --git a/cronjobs/proportional_payout.php b/cronjobs/proportional_payout.php index 8a7dd2cd..faca41ca 100755 --- a/cronjobs/proportional_payout.php +++ b/cronjobs/proportional_payout.php @@ -97,7 +97,8 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { } // 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)) { $log->logFatal('Failed to delete accounted shares from ' . $iPreviousShareId . ' to ' . $iCurrentUpstreamId . ', aborted'); diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index 8f52efd9..711ed0b5 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -43,6 +43,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 diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index ed6078ea..fc59f0f4 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -13,10 +13,12 @@ class Share { // This defines each share public $rem_host, $username, $our_result, $upstream_result, $reason, $solution, $time; - public function __construct($debug, $mysqli, $user) { + 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); } @@ -126,28 +128,55 @@ class Share { * param right int Right/highest share ID * return array data Returns an array with usernames as keys for easy access **/ - function getArchiveShares($left, $right) { + function getArchiveShares($iCount) { + $iMinId = $this->getMaxArchiveShareId() - $iCount; + $iMaxId = $this->getMaxArchiveShareId(); $stmt = $this->mysqli->prepare(" SELECT a.id, - SUBSTRING_INDEX( s.username , '.', 1 ) as username, - SUM(IF(our_result='Y', 1, 0)) AS valid, - SUM(IF(our_result='N', 1, 0)) AS invalid + SUBSTRING_INDEX( s.username , '.', 1 ) as account, + IFNULL(SUM(IF(our_result='Y', 1, 0)), 0) AS valid, + IFNULL(SUM(IF(our_result='N', 1, 0)), 0) AS invalid FROM $this->tableArchive AS s LEFT JOIN " . $this->user->getTableName() . " AS a ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) - WHERE s.id BETWEEN ? AND ? - GROUP BY username DESC - "); - if ($this->checkStmt($stmt) && $stmt->bind_param("ii", $left, $right) && $stmt->execute() && $result = $stmt->get_result()) { + 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['username']] = $row; + $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'], $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', $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 @@ -289,4 +318,4 @@ class Share { } } -$share = new Share($debug, $mysqli, $user); +$share = new Share($debug, $mysqli, $user, $block, $config); diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 995fc0b2..cee6d5de 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -169,8 +169,35 @@ $config['block_bonus'] = 0; **/ $config['payout_system'] = 'prop'; -// For debugging purposes you can archive shares in the archive_shares table, default: true -$config['archive_shares'] = true; +/** + * 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 * 60 * 24 (24h) + **/ +$config['archive']['maxrounds'] = 10; +$config['archive']['maxage'] = 60 * 60 * 24; // URL prefix for block searches, used for block links, default: `http://explorer.litecoin.net/search?q=` // If empty, the block link to the block information page will be removed diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index fd5083d6..de5a2694 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -84,7 +84,7 @@ if (@$_SESSION['USERDATA']['id']) { switch ($config['payout_system']) { case 'pplns': - if ($iAvgBlockShares = round($block->getAvgBlockShares($config['pplns']['type']['blockavg']['blockcount']))) { + if ($iAvgBlockShares = round($block->getAvgBlockShares($config['pplns']['blockavg']['blockcount']))) { $aGlobal['pplns']['target'] = $iAvgBlockShares; } else { $aGlobal['pplns']['target'] = $config['pplns']['shares']['default']; From 00b3c45d2a8e2a983943de3d45d193b7d8f47eb0 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 14:12:54 +0200 Subject: [PATCH 165/650] Adding archive cleanup cron Since this needs to be done for all payouts, moved into it's own cron. * Adjusted run-crons.sh to trigger archive cleanup * Adjusted payouts to remove purgeArchive calls * Adjusted pps payout, removed old configuration variable Further addresses #143 --- cronjobs/archive_cleanup.php | 29 +++++++++++++++++++++++++++++ cronjobs/pplns_payout.php | 4 ---- cronjobs/pps_payout.php | 2 +- cronjobs/run-crons.sh | 2 +- 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100755 cronjobs/archive_cleanup.php diff --git a/cronjobs/archive_cleanup.php b/cronjobs/archive_cleanup.php new file mode 100755 index 00000000..6f008d69 --- /dev/null +++ b/cronjobs/archive_cleanup.php @@ -0,0 +1,29 @@ +#!/usr/bin/php +purgeArchive()) { + $log->logError("Failed to delete archived shares, not critical but should be checked!"); +} +?> diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php index 827b598c..54c229bd 100755 --- a/cronjobs/pplns_payout.php +++ b/cronjobs/pplns_payout.php @@ -160,10 +160,6 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $log->logFatal("Failed to delete accounted shares from $iPreviousShareId to $iCurrentUpstreamId, aborting!"); exit(1); } - // If we don't keep archives, delete some now to release disk space - if (!$share->purgeArchive()) { - $log->logError("Failed to delete archived shares, not critical but should be checked!"); - } // Mark this block as accounted for if (!$block->setAccounted($aBlock['id'])) { $log->logFatal("Failed to mark block as accounted! Aborting!"); diff --git a/cronjobs/pps_payout.php b/cronjobs/pps_payout.php index ee35e2ad..58b8dc83 100755 --- a/cronjobs/pps_payout.php +++ b/cronjobs/pps_payout.php @@ -120,7 +120,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $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)) $log->logError("Archving failed"); } diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index b6b241b4..e797cb44 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -16,7 +16,7 @@ PIDFILE='/tmp/mmcfe-ng-cron.pid' CRONHOME='.' # List of cruns to execute -CRONS="findblock.php proportional_payout.php pplns_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php" +CRONS="findblock.php proportional_payout.php pplns_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php archive_cleanup.php" # Additional arguments to pass to cronjobs CRONARGS="-v" From 8808913ca5a25ec909b9ce8f259f55146062ed1d Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 22:02:12 +0200 Subject: [PATCH 166/650] Fixing Sidebar estimations on PPLNS This will address a new issue in #143, estimates where not displayed properly when pplns was active. Fixes #143 --- public/include/smarty_globals.inc.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index de5a2694..cc162501 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -83,16 +83,7 @@ if (@$_SESSION['USERDATA']['id']) { $aGlobal['userdata']['sharerate'] = $statistics->getUserSharerate($_SESSION['USERDATA']['id']); switch ($config['payout_system']) { - case 'pplns': - if ($iAvgBlockShares = round($block->getAvgBlockShares($config['pplns']['blockavg']['blockcount']))) { - $aGlobal['pplns']['target'] = $iAvgBlockShares; - } else { - $aGlobal['pplns']['target'] = $config['pplns']['shares']['default']; - } - break; - case 'pps': - break; - default: + 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); @@ -105,6 +96,14 @@ if (@$_SESSION['USERDATA']['id']) { $aGlobal['userdata']['est_donation'] = 0; $aGlobal['userdata']['est_payout'] = 0; } + case 'pplns': + if ($iAvgBlockShares = round($block->getAvgBlockShares($config['pplns']['blockavg']['blockcount']))) { + $aGlobal['pplns']['target'] = $iAvgBlockShares; + } else { + $aGlobal['pplns']['target'] = $config['pplns']['shares']['default']; + } + break; + case 'pps': break; } From 85c1a8eaf55a963789e52e3ea16ae69111414ba3 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 22:41:07 +0200 Subject: [PATCH 167/650] more debug information on non-existing block shares --- cronjobs/pplns_payout.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php index 54c229bd..9657b394 100755 --- a/cronjobs/pplns_payout.php +++ b/cronjobs/pplns_payout.php @@ -61,7 +61,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $log->logDebug("Matching or exceeding PPLNS target of $pplns_target with $iRoundShares"); $aAccountShares = $share->getSharesForAccounts($aBlock['share_id'] - $pplns_target + 1, $aBlock['share_id']); if (empty($aAccountShares)) { - $log->logFatal("No shares found for this block, aborted!"); + $log->logFatal("No shares found for this block, aborted! Block Height : " . $aBlock['height']); exit(1); } } else { @@ -70,7 +70,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { // 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!"); + $log->logFatal("No shares found for this block, aborted! Block height: " . $aBlock['height']); exit(1); } From ecf3db3a5b456470d245051d13bfdab8f2eef42e Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 22:41:21 +0200 Subject: [PATCH 168/650] Removed BETWEEN call for round shares Now shares are calculated proper with `id > ? AND id <= ?` as boundaries. This will ensure the upstream result is also calculated for the round. Addresses #143 --- public/include/classes/share.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index fc59f0f4..7cf1ef3d 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -73,7 +73,7 @@ class Share { count(id) 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); @@ -102,7 +102,7 @@ class Share { FROM $this->table AS s LEFT JOIN " . $this->user->getTableName() . " AS a ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) - WHERE s.id BETWEEN ? AND ? + 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()) @@ -189,7 +189,7 @@ class Share { 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 ?"); + 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; @@ -199,7 +199,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 From b4b6b118bf06603c82d6f37ab2bd7474d608d2fe Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 23:19:36 +0200 Subject: [PATCH 169/650] Ensure to return 0 if no valid/invalid shares are found --- public/include/classes/share.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index 7cf1ef3d..dddabf33 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -97,8 +97,8 @@ class Share { SELECT a.id, SUBSTRING_INDEX( s.username , '.', 1 ) as username, - SUM(IF(our_result='Y', 1, 0)) AS valid, - SUM(IF(our_result='N', 1, 0)) AS invalid + IFNULL(SUM(IF(our_result='Y', 1, 0)), 0) AS valid, + IFNULL(SUM(IF(our_result='N', 1, 0)), 0) AS invalid FROM $this->table AS s LEFT JOIN " . $this->user->getTableName() . " AS a ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) From adceaf6913e1de4f08e5055068cb5e24bdbeda91 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 23:28:59 +0200 Subject: [PATCH 170/650] Update README.md Added new donor --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 885c5573..3b3902f6 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ These people have supported this project with a donation: * [vias](https://github.com/vias79) * [WKNiGHT](https://github.com/WKNiGHT-) * [ZC](https://github.com/zccopwrx) +* Nutnut Pools running mmcfe-ng ====================== From 94d9c1eb4c67c4f3da524c8adc5500269df8364b Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 10 Jul 2013 10:40:11 +0200 Subject: [PATCH 171/650] Added cronjob monitoring to admin panel * Added monitoring class to deal with monitoring events * Added event calls to all important cronjobs * Added cron_end include file for monitoring cleanups on successful runs * Added Monitoring to autoloader * Modified account page to check for running auto_payout in monitoring * Added monitoring to Navigation bar * Added monitoring controller page Fixes #415 --- cronjobs/archive_cleanup.php | 7 ++ cronjobs/auto_payout.php | 11 ++- cronjobs/blockupdate.php | 6 ++ cronjobs/cron_end.inc.php | 28 ++++++++ cronjobs/findblock.php | 9 ++- cronjobs/pplns_payout.php | 18 +++++ cronjobs/pps_payout.php | 16 ++++- cronjobs/proportional_payout.php | 38 ++++++---- cronjobs/shared.inc.php | 18 ++++- public/include/autoloader.inc.php | 1 + public/include/classes/monitoring.class.php | 49 +++++++++++++ public/include/pages/account/edit.inc.php | 3 +- public/include/pages/admin/monitoring.inc.php | 70 +++++++++++++++++++ .../mmcFE/admin/monitoring/default.tpl | 39 +++++++++++ public/templates/mmcFE/global/navigation.tpl | 1 + 15 files changed, 290 insertions(+), 24 deletions(-) create mode 100644 cronjobs/cron_end.inc.php create mode 100644 public/include/classes/monitoring.class.php create mode 100644 public/include/pages/admin/monitoring.inc.php create mode 100644 public/templates/mmcFE/admin/monitoring/default.tpl diff --git a/cronjobs/archive_cleanup.php b/cronjobs/archive_cleanup.php index 6f008d69..ebdd5b7e 100755 --- a/cronjobs/archive_cleanup.php +++ b/cronjobs/archive_cleanup.php @@ -25,5 +25,12 @@ require_once('shared.inc.php'); // If we don't keep archives, delete some now to release disk space if (!$share->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 943a6cd7..c55cc913 100755 --- a/cronjobs/auto_payout.php +++ b/cronjobs/auto_payout.php @@ -24,12 +24,12 @@ require_once('shared.inc.php'); if ($bitcoin->can_connect() !== true) { $log->logFatal(" unable to connect to RPC server, exiting\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); } -// Mark this job as active -$setting->setValue('auto_payout_active', 1); - // Fetch all users with setup AP $users = $user->getAllAutoPayout(); @@ -80,7 +80,6 @@ if (! empty($users)) { $log->logDebug(" no user has configured their AP > 0\n"); } -// 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 d257a69d..67d106b7 100755 --- a/cronjobs/blockupdate.php +++ b/cronjobs/blockupdate.php @@ -24,6 +24,9 @@ require_once('shared.inc.php'); if ( $bitcoin->can_connect() !== true ) { $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); } @@ -51,3 +54,6 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $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..177e5296 --- /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]); + +// Mark cron as running for monitoring +$monitoring->setStatus($cron_name . '_active', "yesno", 0); +?> diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index b98bbc3a..0336deea 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -32,6 +32,9 @@ if ( $bitcoin->can_connect() === true ){ $aTransactions = $bitcoin->query('listsinceblock', $strLastBlockHash); } else { $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); } @@ -78,6 +81,9 @@ if (empty($aAllBlocks)) { $iAccountId = $user->getUserId($share->getUpstreamFinder()); } else { $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; } @@ -124,5 +130,6 @@ if (empty($aAllBlocks)) { } } } -?> +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php index 9657b394..6bb4234a 100755 --- a/cronjobs/pplns_payout.php +++ b/cronjobs/pplns_payout.php @@ -32,6 +32,9 @@ if ($config['payout_system'] != 'pplns') { $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); } @@ -50,6 +53,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $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']); @@ -62,6 +68,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $aAccountShares = $share->getSharesForAccounts($aBlock['share_id'] - $pplns_target + 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); } } else { @@ -71,6 +80,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $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); } @@ -163,7 +175,13 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { // 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 58b8dc83..bb48e62e 100755 --- a/cronjobs/pps_payout.php +++ b/cronjobs/pps_payout.php @@ -36,12 +36,15 @@ if ( $bitcoin->can_connect() === true ){ $dDifficulty = $dDifficulty['proof-of-work']; } else { $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 if ($config['reward_type'] != 'block') { -$pps_value = number_format(round($config['reward'] / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); + $pps_value = number_format(round($config['reward'] / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); } else { // Try to find the last block value and use that for future payouts, revert to fixed reward if none found if ($aLastBlock = $block->getLast()) { @@ -111,6 +114,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $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 @@ -127,12 +133,20 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { // Delete shares if ($aBlock['share_id'] < $iLastShareId && !$share->deleteAccountedShares($aBlock['share_id'], $iLastBlockShare)) { $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'])) { $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.inc.php'); ?> diff --git a/cronjobs/proportional_payout.php b/cronjobs/proportional_payout.php index faca41ca..36a9a1fe 100755 --- a/cronjobs/proportional_payout.php +++ b/cronjobs/proportional_payout.php @@ -32,6 +32,9 @@ if ($config['payout_system'] != 'prop') { $aAllBlocks = $block->getAllUnaccounted('ASC'); if (empty($aAllBlocks)) { $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); } @@ -42,17 +45,15 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { 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."); - $log->logFatal("Please assign a valid share ID to this block to continue the payout process."); - exit(1); - } $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)) { $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); } @@ -72,13 +73,13 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { // Verbose output of this users calculations $log->logInfo($aData['id'] . "\t" . - $aData['username'] . "\t" . - $aData['valid'] . "\t" . - $aData['invalid'] . "\t" . - $aData['percentage'] . "\t" . - $aData['payout'] . "\t" . - $aData['donation'] . "\t" . - $aData['fee']); + $aData['username'] . "\t" . + $aData['valid'] . "\t" . + $aData['invalid'] . "\t" . + $aData['percentage'] . "\t" . + $aData['payout'] . "\t" . + $aData['donation'] . "\t" . + $aData['fee']); // Update user share statistics if (!$statistics->updateShareStatistics($aData, $aBlock['id'])) @@ -102,10 +103,21 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { // Delete all accounted shares if (!$share->deleteAccountedShares($iCurrentUpstreamId, $iPreviousShareId)) { $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'])) + if (!$block->setAccounted($aBlock['id'])) { $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); + } } } + +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/shared.inc.php b/cronjobs/shared.inc.php index 9d3ae322..14bed27e 100644 --- a/cronjobs/shared.inc.php +++ b/cronjobs/shared.inc.php @@ -22,6 +22,13 @@ 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 * + *****************************************************/ +// Our cron name +$cron_name = basename($_SERVER['PHP_SELF'], '.php'); + // Our security check define("SECURITY", 1); @@ -32,6 +39,13 @@ require_once(BASEPATH . 'include/config/global.inc.php'); require_once(INCLUDE_DIR . '/autoloader.inc.php'); // Load 3rd party logging library for running crons -$log = new KLogger ( 'logs/' . basename($_SERVER['PHP_SELF'], '.php') . '.txt' , KLogger::DEBUG ); -$log->LogDebug('Starting ' . basename($_SERVER['PHP_SELF'], '.php')); +$log = new KLogger ( 'logs/' . $cron_name . '.txt' , KLogger::DEBUG ); +$log->LogDebug('Starting ' . $cron_name); + +// 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); ?> diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index 68109049..281b39c6 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -24,6 +24,7 @@ require_once(INCLUDE_DIR . '/smarty.inc.php'); require_once(CLASS_DIR . '/base.class.php'); require_once(CLASS_DIR . '/block.class.php'); require_once(CLASS_DIR . '/setting.class.php'); +require_once(CLASS_DIR . '/monitoring.class.php'); require_once(CLASS_DIR . '/user.class.php'); require_once(CLASS_DIR . '/share.class.php'); require_once(CLASS_DIR . '/worker.class.php'); 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/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index 5548dd82..96fb432d 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -30,7 +30,8 @@ if ($user->isAuthenticated()) { if ($continue == true) { // Send balance to address, mind fee for transaction! try { - if ($setting->getValue('auto_payout_active') == 0) { + $auto_payout = $monitoring->getStatus('auto_payout_active'); + if ($auto_payout['value'] == 0) { $bitcoin->sendtoaddress($sCoinAddress, $dBalance); } else { $_SESSION['POPUP'][] = array('CONTENT' => 'Auto-payout active, please contact site support immidiately to revoke invalid transactions.', '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..18db3949 --- /dev/null +++ b/public/include/pages/admin/monitoring.inc.php @@ -0,0 +1,70 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +// Fetch settings to propagate to template +$aCronStatus = array( + 'auto_payout' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('auto_payout_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('auto_payout_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('auto_payout_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('auto_payout_runtime') ) + ), + 'archive_cleanup' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('archive_cleanup_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('archive_cleanup_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('archive_cleanup_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('archive_cleanup_runtime') ) + ), + 'blockupdate' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('blockupdate_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('blockupdate_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('blockupdate_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('blockupdate_runtime') ) + ), + 'findblock' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('findblock_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('findblock_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('findblock_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('findblock_runtime') ) + ) +); +// Payout system specifics +switch ($config['payout_system']) { +case 'pplns': + $aCronStatus['pplns_payout'] = array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pplns_payout_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pplns_payout_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pplns_payout_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pplns_payout_runtime') ) + ); + break; +case 'pps': + $aCronStatus['pps_payout'] = array( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pps_payout_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pps_payout_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pps_payout_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pps_payout_runtime') ) + ); + break; +case 'prop': + $aCronStatus['proportional_payout'] = array( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('proportional_payout_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('proportional_payout_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('proportional_payout_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('proportional_payout_runtime') ) + ); + break; +} +$smarty->assign("CRONSTATUS", $aCronStatus); + +// Tempalte specifics +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/templates/mmcFE/admin/monitoring/default.tpl b/public/templates/mmcFE/admin/monitoring/default.tpl new file mode 100644 index 00000000..416d5fcf --- /dev/null +++ b/public/templates/mmcFE/admin/monitoring/default.tpl @@ -0,0 +1,39 @@ +{foreach $CRONSTATUS as $k=>$v} + {include file="global/block_header.tpl" BLOCK_HEADER="$k"} + + + + + + + {foreach $v as $event} + + + + + {/foreach} + +
      Event NameStatus
      {$event.NAME} + {if $event.STATUS.type == 'okerror'} + {if $event.STATUS.value == 0} + OK + {else} + ERROR + {/if} + {else if $event.STATUS.type == 'message'} + {$event.STATUS.value} + {else if $event.STATUS.type == 'yesno'} + {if $event.STATUS.value == 1} + Yes + {else} + No + {/if} + {else if $event.STATUS.type == 'time'} + {$event.STATUS.value|number_format:"2"} seconds + {else} + {$event.STATUS.value} + {/if} +
      + + {include file="global/block_footer.tpl"} +{/foreach} diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 6cafd1f6..42c89e9a 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -13,6 +13,7 @@ {if $smarty.session.AUTHENTICATED|default:"0" == 1 && $GLOBAL.userdata.is_admin == 1}
    • Admin Panel
        +
      • Monitoring
      • User Info
      • Wallet Info
      • Transactions
      • From 0c014da726fc44f42bbd2f7772ce31fb53bfd325 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 10 Jul 2013 10:44:10 +0200 Subject: [PATCH 172/650] unknown index PHP warning fix --- public/include/pages/account/edit.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index 96fb432d..1ced549b 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -54,7 +54,7 @@ if ($user->isAuthenticated()) { $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to wallet RPC service', 'TYPE' => 'errormsg'); } } else { - $_SESSION['POPUP'][] = array('CONTENT' => 'Insufficient funds, you need more than ' . $config['txfee'] . ' ' . $conifg['currency'] . ' to cover transaction fees', 'TYPE' => 'errormsg'); + $_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); } From a6d752a09699d31c37fa5a965828486fcb4a65f5 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 10 Jul 2013 10:45:10 +0200 Subject: [PATCH 173/650] adding some defaults on template --- public/templates/mmcFE/admin/monitoring/default.tpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/templates/mmcFE/admin/monitoring/default.tpl b/public/templates/mmcFE/admin/monitoring/default.tpl index 416d5fcf..4a929e02 100644 --- a/public/templates/mmcFE/admin/monitoring/default.tpl +++ b/public/templates/mmcFE/admin/monitoring/default.tpl @@ -25,9 +25,9 @@ No {/if} {else if $event.STATUS.type == 'time'} - {$event.STATUS.value|number_format:"2"} seconds + {$event.STATUS.value|default:"0"|number_format:"2"} seconds {else} - {$event.STATUS.value} + {$event.STATUS.value|default:""} {/if} From 0e98d81af070f33fcd31977d57fca0bd2845bdb7 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 10 Jul 2013 11:22:57 +0200 Subject: [PATCH 174/650] adding new monitoring table to SQL imports --- sql/002_monitoring.sql | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 sql/002_monitoring.sql diff --git a/sql/002_monitoring.sql b/sql/002_monitoring.sql new file mode 100644 index 00000000..e5a21682 --- /dev/null +++ b/sql/002_monitoring.sql @@ -0,0 +1,21 @@ +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; + + +CREATE TABLE IF NOT EXISTS `monitoring` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(30) NOT NULL, + `type` varchar(15) NOT NULL, + `value` varchar(25) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf32 COMMENT='Monitoring events from cronjobs'; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; From ceeed45bb39ac76282c224db9111b0e936af4038 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 10 Jul 2013 11:26:06 +0200 Subject: [PATCH 175/650] Adding statistics cron to monitoring Useful to trace cron runtimes in order to adjust caching times for long rounds. Addresses #145 and adds a new cron to monitoring page --- cronjobs/statistics.php | 2 ++ public/include/pages/admin/monitoring.inc.php | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/cronjobs/statistics.php b/cronjobs/statistics.php index 9ce96bde..2a145692 100755 --- a/cronjobs/statistics.php +++ b/cronjobs/statistics.php @@ -55,4 +55,6 @@ $log->logInfo("getAllUserShares " . number_format(microtime(true) - $start, 2) . foreach ($aUserShares as $aShares) { $memcache->setCache('getUserShares'. $aShares['id'], $aShares); } + +require_once('cron_end.inc.php'); ?> diff --git a/public/include/pages/admin/monitoring.inc.php b/public/include/pages/admin/monitoring.inc.php index 18db3949..c891c201 100644 --- a/public/include/pages/admin/monitoring.inc.php +++ b/public/include/pages/admin/monitoring.inc.php @@ -11,6 +11,12 @@ if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { // Fetch settings to propagate to template $aCronStatus = array( + 'statistics' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('statistics_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('statistics_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('statistics_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('statistics_runtime') ) + ), 'auto_payout' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('auto_payout_status') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('auto_payout_message') ), From 652bf8ed4b23c9a8b93aec965e8367e8e4405792 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 10 Jul 2013 11:30:41 +0200 Subject: [PATCH 176/650] Update README.md Updated features --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3b3902f6..a7787bb2 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ The following feature have been implemented so far: * Propotional * PPS * PPLNS **NEW** -* Use of memcache for statistics instead of a cronjob +* Statistics are cached in Memcache by Cronjob for quick data access * Web User accounts * Re-Captcha protected registration form * Worker accounts @@ -88,9 +88,11 @@ The following feature have been implemented so far: * Auto payout * Transaction list (confirmed and unconfirmed) * Admin Panel + * Cron Monitoring Overview **NEW** * User Listing including statistics * Wallet information - * News Posts **NEW** + * User Transactions **NEW** + * News Posts * Notification system * IDLE Workers * New blocks found in pool From 271f7f8381be3dedace988a61c5a6a4b8d39eaf3 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 10 Jul 2013 11:39:16 +0200 Subject: [PATCH 177/650] Better monitoring layout --- public/include/pages/admin/monitoring.inc.php | 32 +++++++++---------- .../mmcFE/admin/monitoring/default.tpl | 28 ++++++++-------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/public/include/pages/admin/monitoring.inc.php b/public/include/pages/admin/monitoring.inc.php index c891c201..efc20a6c 100644 --- a/public/include/pages/admin/monitoring.inc.php +++ b/public/include/pages/admin/monitoring.inc.php @@ -13,33 +13,33 @@ if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { $aCronStatus = array( 'statistics' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('statistics_status') ), - array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('statistics_message') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('statistics_active') ), - array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('statistics_runtime') ) + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('statistics_runtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('statistics_message') ), ), 'auto_payout' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('auto_payout_status') ), - array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('auto_payout_message') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('auto_payout_active') ), - array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('auto_payout_runtime') ) + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('auto_payout_runtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('auto_payout_message') ), ), 'archive_cleanup' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('archive_cleanup_status') ), - array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('archive_cleanup_message') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('archive_cleanup_active') ), - array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('archive_cleanup_runtime') ) + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('archive_cleanup_runtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('archive_cleanup_message') ), ), 'blockupdate' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('blockupdate_status') ), - array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('blockupdate_message') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('blockupdate_active') ), - array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('blockupdate_runtime') ) + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('blockupdate_runtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('blockupdate_message') ), ), 'findblock' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('findblock_status') ), - array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('findblock_message') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('findblock_active') ), - array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('findblock_runtime') ) + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('findblock_runtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('findblock_message') ), ) ); // Payout system specifics @@ -47,25 +47,25 @@ switch ($config['payout_system']) { case 'pplns': $aCronStatus['pplns_payout'] = array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pplns_payout_status') ), - array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pplns_payout_message') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pplns_payout_active') ), - array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pplns_payout_runtime') ) + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pplns_payout_runtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pplns_payout_message') ), ); break; case 'pps': $aCronStatus['pps_payout'] = array( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pps_payout_status') ), - array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pps_payout_message') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pps_payout_active') ), - array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pps_payout_runtime') ) + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pps_payout_runtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pps_payout_message') ), ); break; case 'prop': $aCronStatus['proportional_payout'] = array( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('proportional_payout_status') ), - array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('proportional_payout_message') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('proportional_payout_active') ), - array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('proportional_payout_runtime') ) + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('proportional_payout_runtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('proportional_payout_message') ), ); break; } diff --git a/public/templates/mmcFE/admin/monitoring/default.tpl b/public/templates/mmcFE/admin/monitoring/default.tpl index 4a929e02..6213a164 100644 --- a/public/templates/mmcFE/admin/monitoring/default.tpl +++ b/public/templates/mmcFE/admin/monitoring/default.tpl @@ -1,14 +1,17 @@ -{foreach $CRONSTATUS as $k=>$v} - {include file="global/block_header.tpl" BLOCK_HEADER="$k"} - +{include file="global/block_header.tpl" BLOCK_HEADER="Monitoring"} +
        - - + + + + + - {foreach $v as $event} +{foreach $CRONSTATUS as $cron=>$v} - + + {foreach $v as $event} + {/foreach} - {/foreach} +{/foreach}
        Event NameStatusCronjobExit CodeActiveRuntimeMessage
        {$event.NAME}{$cron} {if $event.STATUS.type == 'okerror'} {if $event.STATUS.value == 0} @@ -20,9 +23,9 @@ {$event.STATUS.value} {else if $event.STATUS.type == 'yesno'} {if $event.STATUS.value == 1} - Yes + Yes {else} - No + No {/if} {else if $event.STATUS.type == 'time'} {$event.STATUS.value|default:"0"|number_format:"2"} seconds @@ -30,10 +33,9 @@ {$event.STATUS.value|default:""} {/if}
        - - {include file="global/block_footer.tpl"} -{/foreach} +{include file="global/block_footer.tpl"} From 993ddaf3bc316854bd01dee0ce9ea575c0d77a44 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 10 Jul 2013 14:17:20 +0200 Subject: [PATCH 178/650] fixing 24h shares retention in dist config --- public/include/config/global.inc.dist.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 896d5daf..a57c1f22 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -210,10 +210,10 @@ $config['payout_system'] = 'prop'; * * Default: * maxrounds = 10 - * maxage = 60 * 60 * 24 (24h) + * maxage = 60 * 24 (24h) **/ $config['archive']['maxrounds'] = 10; -$config['archive']['maxage'] = 60 * 60 * 24; +$config['archive']['maxage'] = 60 * 24; // URL prefix for block searches, used for block links, default: `http://explorer.litecoin.net/search?q=` // If empty, the block link to the block information page will be removed From fdf97c88325135dd0b2c963414e69574f8b96588 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 10 Jul 2013 14:24:15 +0200 Subject: [PATCH 179/650] fixing config access in share class for archive purge --- public/include/classes/share.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index dddabf33..7bcc6d14 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -165,12 +165,12 @@ class Share { $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'], $config['archive']['maxage']) && $stmt->execute()) + 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', $config['archive']['maxage']) && $stmt->execute()) + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $this->config['archive']['maxage']) && $stmt->execute()) return true; } // Catchall From 39d1193e6a59358843847185fc104cf347a9227b Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 08:51:40 +0200 Subject: [PATCH 180/650] Add default value for active pool workers Fixes #418 --- public/templates/mmcFE/global/header.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mmcFE/global/header.tpl b/public/templates/mmcFE/global/header.tpl index b3d150f6..4ee9d9b5 100644 --- a/public/templates/mmcFE/global/header.tpl +++ b/public/templates/mmcFE/global/header.tpl @@ -8,7 +8,7 @@
      • Network Hashrate: {($GLOBAL.nethashrate / 1000 / 1000 )|default:"0"|number_format:"3"} MH/s    
      • Pool Hashrate: {($GLOBAL.hashrate / 1000)|number_format:"3"} MH/s    
      • Pool Sharerate: {$GLOBAL.sharerate|number_format:"2"} Shares/s    
      • -
      • Pool Workers: {$GLOBAL.workers}    
      • +
      • Pool Workers: {$GLOBAL.workers|default:"0"}    
    • From aace0dd014ededaad206a7985f7e243fb62044d1 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 08:57:11 +0200 Subject: [PATCH 181/650] More descriptive PPLNS options Fixes #421 and #422 --- public/include/config/global.inc.dist.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index a57c1f22..73325a5c 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -179,6 +179,7 @@ $config['block_bonus'] = 0; * Available options: * prop: Proportional payout system * pps : Pay Per Share payout system + * pplns : Pay Per Last N Shares payout system * * Default: * prop @@ -232,6 +233,19 @@ $config['fees'] = 0; * 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. + * * default : Default target shares for PPLNS * type : Payout type used in PPLNS * blockcount : Amount of blocks to check for avg shares @@ -260,7 +274,7 @@ $config['difficulty'] = 20; * * Explanation: * - * Proportional Payout System + * 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`. From 6632920fa1ba4c0e37c2a50b98f5f1d9104ff2ed Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 2 Jul 2013 14:08:33 +0200 Subject: [PATCH 182/650] Add detailed smarty cache documentation to config Instead of just making it availble document the smarty cache feature. It might work for users, but it's advised to rely on the memcache instead. Fixes #309 --- public/include/config/global.inc.dist.php | 26 +++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index a57c1f22..8bff1114 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -341,8 +341,30 @@ $config['cookie']['path'] = '/'; $config['cookie']['name'] = 'POOLERCOOKIE'; $config['cookie']['domain'] = ''; -// Disable or enable smarty cache -// This is usually not required, default: 0 + +/** + * 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 mmcfe-ng. + * + * Ensure that the folder `templates/cache` is writable by the webserver! + * + * Options: + * 0 = disabled + * 1 = enabled + * + * Default: + * 0 = disabled + **/ $config['cache'] = 0; ?> From e9311f08a53912ea8b2a491124345b5fa11ed1b1 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 2 Jul 2013 14:30:07 +0200 Subject: [PATCH 183/650] Adding cache lifetime option to smarty config * Renamed configuration array to `smarty` => `cache` * Added `smarty` => `cache_lifetime` to expire cache files properly This should be safe to use, be aware that each page request is cached! That includes any POST/GET calls to the site. It does help in speeding up the site, up to 100% on some requests. For a high traffic site it probably makes sense to enable this option with a low cache lifetime to ensure most recent data. Addresses #309 --- .gitignore | 1 + public/include/config/global.inc.dist.php | 21 ++++++++++++++------- public/include/smarty.inc.php | 3 ++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 66b6c501..a8f1b1ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /public/include/config/global.inc.php /public/templates/compile/*.php /cronjobs/logs/*.txt +/public/templates/cache/*.php diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 8bff1114..da41eb90 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -358,13 +358,20 @@ $config['cookie']['domain'] = ''; * * Ensure that the folder `templates/cache` is writable by the webserver! * - * Options: - * 0 = disabled - * 1 = enabled + * cache = Enable/Disable the cache + * cache_lifetime = Time to keep files in seconds before updating them * - * Default: - * 0 = disabled + * Options: + * cache: + * 0 = disabled + * 1 = enabled + * cache_lifetime: + * time in seconds + * + * Defaults: + * cache = 0, disabled + * cache_lifetime = 30 seconds **/ -$config['cache'] = 0; - +$config['smarty']['cache'] = 1; +$config['smarty']['cache_lifetime'] = 30; ?> diff --git a/public/include/smarty.inc.php b/public/include/smarty.inc.php index 8a320581..46a38750 100644 --- a/public/include/smarty.inc.php +++ b/public/include/smarty.inc.php @@ -20,6 +20,7 @@ $smarty->template_dir = BASEPATH . 'templates/' . THEME . '/'; $smarty->compile_dir = BASEPATH . 'templates/compile/'; // Optional smarty caching, check Smarty documentation for details -$smarty->caching = $config['cache']; +$smarty->caching = $config['smarty']['cache']; +$smarty->cache_lifetime = $config['smarty']['cache_lifetime']; $smarty->cache_dir = BASEPATH . "templates/cache"; ?> From 426268f71db7c5a10d685859592931c859cc5db6 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 4 Jul 2013 08:48:19 +0200 Subject: [PATCH 184/650] adjust smarty configurations --- public/include/smarty.inc.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/public/include/smarty.inc.php b/public/include/smarty.inc.php index 46a38750..e42e56e3 100644 --- a/public/include/smarty.inc.php +++ b/public/include/smarty.inc.php @@ -20,7 +20,10 @@ $smarty->template_dir = BASEPATH . 'templates/' . THEME . '/'; $smarty->compile_dir = BASEPATH . 'templates/compile/'; // Optional smarty caching, check Smarty documentation for details -$smarty->caching = $config['smarty']['cache']; -$smarty->cache_lifetime = $config['smarty']['cache_lifetime']; -$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"; +} ?> From 0e6edc562c89361ed6348cf23f91b7e7aedb51a4 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 2 Jul 2013 14:08:33 +0200 Subject: [PATCH 185/650] Add detailed smarty cache documentation to config Instead of just making it availble document the smarty cache feature. It might work for users, but it's advised to rely on the memcache instead. Fixes #309 --- public/include/config/global.inc.dist.php | 1 - 1 file changed, 1 deletion(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index da41eb90..b0aa2f21 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -341,7 +341,6 @@ $config['cookie']['path'] = '/'; $config['cookie']['name'] = 'POOLERCOOKIE'; $config['cookie']['domain'] = ''; - /** * Enable or disable the Smarty cache * From 3c426e913b609b927c1d79cdf03941f24e19ca45 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sat, 6 Jul 2013 18:10:23 +0200 Subject: [PATCH 186/650] Adding `{nocache}` flags for dynamic content This will update content instantly once the user changes it and not load a cached version from the smarty cache. Addresses #309 --- public/templates/mmcFE/account/edit/default.tpl | 10 +++++----- .../templates/mmcFE/account/notifications/default.tpl | 8 ++++---- public/templates/mmcFE/account/workers/default.tpl | 2 ++ public/templates/mmcFE/admin/news/default.tpl | 2 ++ public/templates/mmcFE/admin/news_edit/default.tpl | 6 +++--- public/templates/mmcFE/admin/settings/default.tpl | 4 ++-- public/templates/mmcFE/admin/user/default.tpl | 2 ++ 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/public/templates/mmcFE/account/edit/default.tpl b/public/templates/mmcFE/account/edit/default.tpl index 482c3579..c15b56d4 100644 --- a/public/templates/mmcFE/account/edit/default.tpl +++ b/public/templates/mmcFE/account/edit/default.tpl @@ -7,9 +7,9 @@ Username: {$GLOBAL.userdata.username} User Id: {$GLOBAL.userdata.id} API Key: {$GLOBAL.userdata.api_key} - E-Mail: - Payment Address: - Donation %: [donation amount in percent (example: 0.5)] + E-Mail: + Payment Address: + Donation %: [donation amount in percent (example: 0.5)] Automatic Payout Threshold: [{$GLOBAL.config.ap_threshold.min}-{$GLOBAL.config.ap_threshold.max} {$GLOBAL.config.currency}. Set to '0' for no auto payout] 4 digit PIN: [The 4 digit PIN you chose when registering] @@ -23,8 +23,8 @@ - - + +
      Account Balance:    {$GLOBAL.userdata.balance.confirmed|escape} {$GLOBAL.config.currency}
      Payout to:
      {$GLOBAL.userdata.coin_address|escape}
      Account Balance:    {nocache}{$GLOBAL.userdata.balance.confirmed|escape}{/nocache} {$GLOBAL.config.currency}
      Payout to:
      {nocache}{$GLOBAL.userdata.coin_address|escape}{/nocache}
      4 digit PIN:
      diff --git a/public/templates/mmcFE/account/notifications/default.tpl b/public/templates/mmcFE/account/notifications/default.tpl index 1d54729b..52140466 100644 --- a/public/templates/mmcFE/account/notifications/default.tpl +++ b/public/templates/mmcFE/account/notifications/default.tpl @@ -12,7 +12,7 @@ IDLE Worker - + @@ -20,7 +20,7 @@ New Blocks - + @@ -28,7 +28,7 @@ Auto Payout - + @@ -36,7 +36,7 @@ Manual Payout - + diff --git a/public/templates/mmcFE/account/workers/default.tpl b/public/templates/mmcFE/account/workers/default.tpl index c69d1139..f26e3109 100644 --- a/public/templates/mmcFE/account/workers/default.tpl +++ b/public/templates/mmcFE/account/workers/default.tpl @@ -15,6 +15,7 @@     + {nocache} {section worker $WORKERS} {assign var="username" value="."|escape|explode:$WORKERS[worker].username:2} @@ -29,6 +30,7 @@ {/section} + {/nocache} diff --git a/public/templates/mmcFE/admin/news/default.tpl b/public/templates/mmcFE/admin/news/default.tpl index 7bd8c198..8c3ac5d1 100644 --- a/public/templates/mmcFE/admin/news/default.tpl +++ b/public/templates/mmcFE/admin/news/default.tpl @@ -17,6 +17,7 @@ {include file="global/block_footer.tpl"} +{nocache} {section name=news loop=$NEWS} {include file="global/block_header.tpl" @@ -35,4 +36,5 @@ {include file="global/block_footer.tpl"} {/section} +{/nocache} {include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/admin/news_edit/default.tpl b/public/templates/mmcFE/admin/news_edit/default.tpl index 467d4a6c..22e945fb 100644 --- a/public/templates/mmcFE/admin/news_edit/default.tpl +++ b/public/templates/mmcFE/admin/news_edit/default.tpl @@ -10,7 +10,7 @@ Active - + @@ -18,13 +18,13 @@ Header - + Content - + diff --git a/public/templates/mmcFE/admin/settings/default.tpl b/public/templates/mmcFE/admin/settings/default.tpl index 3cd67f4e..a2ffbd5b 100644 --- a/public/templates/mmcFE/admin/settings/default.tpl +++ b/public/templates/mmcFE/admin/settings/default.tpl @@ -16,7 +16,7 @@ @@ -26,7 +26,7 @@ diff --git a/public/templates/mmcFE/admin/user/default.tpl b/public/templates/mmcFE/admin/user/default.tpl index 82955741..8c65c49f 100644 --- a/public/templates/mmcFE/admin/user/default.tpl +++ b/public/templates/mmcFE/admin/user/default.tpl @@ -44,6 +44,7 @@ +{nocache} {section name=user loop=$USERS|default} {$USERS[user].id} @@ -70,6 +71,7 @@ {/section} +{/nocache} From bffeea07c93ced2830ed4cc1ed0e89b5de947352 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sat, 6 Jul 2013 18:36:11 +0200 Subject: [PATCH 187/650] Adding cache detection to many pages This will allow pages to skip loading data from backends like the database or the wallet RPC server. If a cached page is detected and valid, all dynamic content generation will be skipped completely. Other pages that have not been adjusted in this commit will still fetch backend data all the time. This will ensure clients always see the most recent data, like worker information or account changes. This should fix #309 completely but needs some testing. --- .../include/pages/admin/transactions.inc.php | 16 ++- public/include/pages/admin/wallet.inc.php | 17 ++- public/include/pages/home.inc.php | 17 +-- public/include/pages/statistics.inc.php | 25 +++-- .../include/pages/statistics/blocks.inc.php | 17 ++- .../include/pages/statistics/graphs.inc.php | 19 ++-- public/include/pages/statistics/pool.inc.php | 101 +++++++++--------- public/include/smarty_globals.inc.php | 4 +- public/templates/mmcFE/master.tpl | 2 +- 9 files changed, 130 insertions(+), 88 deletions(-) diff --git a/public/include/pages/admin/transactions.inc.php b/public/include/pages/admin/transactions.inc.php index 00345903..b25d6f15 100644 --- a/public/include/pages/admin/transactions.inc.php +++ b/public/include/pages/admin/transactions.inc.php @@ -2,10 +2,20 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if ($user->isAuthenticated()) { + +// Check user to ensure they are admin +if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { + $debug->append('No cached version available, fetching from backend', 3); $aTransactions = $transaction->getAllTransactions(@$_REQUEST['start']); if (!$aTransactions) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any transaction', 'TYPE' => 'errormsg'); - $smarty->assign('TRANSACTIONS', $aTransactions); - $smarty->assign('CONTENT', 'default.tpl'); +} else { + $debug->append('Using cached page', 3); } +$smarty->assign('TRANSACTIONS', $aTransactions); +$smarty->assign('CONTENT', 'default.tpl'); ?> diff --git a/public/include/pages/admin/wallet.inc.php b/public/include/pages/admin/wallet.inc.php index 479ff919..d0642b14 100644 --- a/public/include/pages/admin/wallet.inc.php +++ b/public/include/pages/admin/wallet.inc.php @@ -9,15 +9,22 @@ 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', md5(serialize($_REQUEST)))) { + $debug->append('No cached version available, fetching from backend', 3); + if ($bitcoin->can_connect() === true){ + $dBalance = $bitcoin->query('getbalance'); + } else { + $dBalance = 0; + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to wallet RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + } + // Fetch locked balance from transactions + $dLockedBalance = $transaction->getLockedBalance(); } else { - $dBalance = 0; - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to wallet RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + $debug->append('Using cached page', 3); } $smarty->assign("BALANCE", $dBalance); -$smarty->assign("LOCKED", $transaction->getLockedBalance()); +$smarty->assign("LOCKED", $dLockedBalance); // Tempalte specifics $smarty->assign("CONTENT", "default.tpl"); diff --git a/public/include/pages/home.inc.php b/public/include/pages/home.inc.php index ea4bd8fb..ac7b458e 100644 --- a/public/include/pages/home.inc.php +++ b/public/include/pages/home.inc.php @@ -6,13 +6,18 @@ if (!defined('SECURITY')) die('Hacking attempt'); // Include markdown library use \Michelf\Markdown; -// 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']); +if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { + $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']); + } } +} else { + $debug->append('Using cached page', 3); } // Load news entries for Desktop site and unauthenticated users diff --git a/public/include/pages/statistics.inc.php b/public/include/pages/statistics.inc.php index 3307dd21..c5035f0c 100644 --- a/public/include/pages/statistics.inc.php +++ b/public/include/pages/statistics.inc.php @@ -4,18 +4,23 @@ if (!defined('SECURITY')) die('Hacking attempt'); -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']; - $iBlock = $bitcoin->query('getblockcount'); +if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { + $debug->append('No cached version available, fetching from backend', 3); + 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']; + $iBlock = $bitcoin->query('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 22eeadff..41e209cb 100644 --- a/public/include/pages/statistics/blocks.inc.php +++ b/public/include/pages/statistics/blocks.inc.php @@ -4,12 +4,19 @@ if (!defined('SECURITY')) die('Hacking attempt'); // Grab the last blocks found -$iLimit = 20; -$aBlocksFoundData = $statistics->getBlocksFound($iLimit); +if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { + $debug->append('No cached version available, fetching from backend', 3); + // Grab the last blocks found + $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); +} if ($config['website']['acl']['statistics']['blocks'] == 'public') { $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..4d31aadb 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', md5(serialize($_REQUEST)))) { + $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 27395cc1..8386cb07 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -1,58 +1,63 @@ can_connect() === true){ - $dDifficulty = $bitcoin->getdifficulty(); - if (is_array($dDifficulty) && array_key_exists('proof-of-work', $dDifficulty)) - $dDifficulty = $dDifficulty['proof-of-work']; - $iBlock = $bitcoin->getblockcount(); +if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { + $debug->append('No cached version available, fetching from backend', 3); + // Fetch data from wallet + if ($bitcoin->can_connect() === true){ + $dDifficulty = $bitcoin->getdifficulty(); + if (is_array($dDifficulty) && array_key_exists('proof-of-work', $dDifficulty)) + $dDifficulty = $dDifficulty['proof-of-work']; + $iBlock = $bitcoin->getblockcount(); + } 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']); + } 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); + count($aBlockData) > 0 ? $smarty->assign("LASTBLOCK", $aBlockData['height']) : $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 wallet 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); -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']); -} 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); -count($aBlockData) > 0 ? $smarty->assign("LASTBLOCK", $aBlockData['height']) : $smarty->assign("LASTBLOCK", 0); -$smarty->assign("DIFFICULTY", $dDifficulty); -$smarty->assign("REWARD", $config['reward']); - +// Public / private page detection if ($config['website']['acl']['statistics']['pool'] == 'public') { $smarty->assign("CONTENT", "authenticated.tpl"); } else if ($user->isAuthenticated() && $config['website']['acl']['statistics']['pool'] == 'private') { diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index e83b1ca3..cbd369d3 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -2,7 +2,7 @@ // Make sure we are called from index.php if (!defined('SECURITY')) - die('Hacking attempt'); + die('Hacking attempt'); // Globally available variables $debug->append('Global smarty variables', 3); @@ -109,7 +109,7 @@ if (@$_SESSION['USERDATA']['id']) { // 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' => 'warning'); + $_SESSION['POPUP'][] = array('CONTENT' => 'You have exceeded your accounts balance. Please transfer some ' . $config['currency'] . "!", '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'); } diff --git a/public/templates/mmcFE/master.tpl b/public/templates/mmcFE/master.tpl index f476f747..68777d1a 100644 --- a/public/templates/mmcFE/master.tpl +++ b/public/templates/mmcFE/master.tpl @@ -69,7 +69,7 @@
    - {include file="system/debugger.tpl"} + {nocache}{include file="system/debugger.tpl"}{/nocache}
    From e0275566ae3f8c2b83de98980ada6a5d5fb0442c Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 8 Jul 2013 14:16:45 +0200 Subject: [PATCH 188/650] disable smarty cache by default in dist --- public/include/config/global.inc.dist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index b0aa2f21..238654e7 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -371,6 +371,6 @@ $config['cookie']['domain'] = ''; * cache = 0, disabled * cache_lifetime = 30 seconds **/ -$config['smarty']['cache'] = 1; +$config['smarty']['cache'] = 0; $config['smarty']['cache_lifetime'] = 30; ?> From 65c6318b026a11845cc5b308dafd48f6fed83c41 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 9 Jul 2013 08:54:20 +0200 Subject: [PATCH 189/650] fixing empty variable when using cache --- public/include/pages/home.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/pages/home.inc.php b/public/include/pages/home.inc.php index ac7b458e..9431e219 100644 --- a/public/include/pages/home.inc.php +++ b/public/include/pages/home.inc.php @@ -16,11 +16,11 @@ if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { $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("NEWS", $aNews); $smarty->assign("CONTENT", "default.tpl"); ?> From 17829cfd4ab146e34371b4a946092b201a6acfb9 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 09:13:50 +0200 Subject: [PATCH 190/650] always assign default content --- public/include/pages/account/transactions.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/pages/account/transactions.inc.php b/public/include/pages/account/transactions.inc.php index db927ca7..7bcfb4f2 100644 --- a/public/include/pages/account/transactions.inc.php +++ b/public/include/pages/account/transactions.inc.php @@ -6,6 +6,6 @@ if ($user->isAuthenticated()) { $aTransactions = $transaction->getTransactions($_SESSION['USERDATA']['id']); if (!$aTransactions) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any transaction', 'TYPE' => 'errormsg'); $smarty->assign('TRANSACTIONS', $aTransactions); - $smarty->assign('CONTENT', 'default.tpl'); } +$smarty->assign('CONTENT', 'default.tpl'); ?> From 39cfdc78e03c541bf3c3fab089708dacbbb3aaa7 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 09:15:36 +0200 Subject: [PATCH 191/650] onliner security check --- public/include/smarty_globals.inc.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 82938ee2..a1e0eb19 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -1,8 +1,7 @@ append('Global smarty variables', 3); From 72d91ff6c3fb4ffc8c184ab83e9f6de2b99ace06 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 09:16:31 +0200 Subject: [PATCH 192/650] adding proper account balance warning back in --- public/include/smarty_globals.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index a1e0eb19..7219778c 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -109,7 +109,7 @@ if (@$_SESSION['USERDATA']['id']) { // Site-wide notifications, based on user events if ($aGlobal['userdata']['balance']['confirmed'] >= $config['ap_threshold']['max']) - $_SESSION['POPUP'][] = array('CONTENT' => 'You have exceeded your accounts balance. Please transfer some ' . $config['currency'] . "!", 'TYPE' => 'errormsg'); + $_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'); } From 25b12d204a4ec48503f20608f4886f380fa63668 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 09:19:30 +0200 Subject: [PATCH 193/650] Better description what type uses what for PPLNS Fixes #422 --- public/include/config/global.inc.dist.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 73325a5c..c8f31d6e 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -246,6 +246,11 @@ $config['fees'] = 0; * 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 From 5d568a462118e03d1c192d241e6d50a7dd081b2a Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 09:42:10 +0200 Subject: [PATCH 194/650] Adding CRONHOME detection to run-crons.sh This will ensure the cron will run without having to change the CRONHOME configuration. --- cronjobs/run-crons.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index e797cb44..ab1f482c 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -12,9 +12,6 @@ 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 pplns_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php archive_cleanup.php" @@ -30,6 +27,9 @@ VERBOSE="0" # # ################################################################ +# Find scripts path +CRONHOME=$( dirname $0 ) + # Change working director to CRONHOME if ! cd $CRONHOME 2>/dev/null; then echo "Unable to change to working directory \$CRONHOME: $CRONHOME" From dad727a8f0a19df73ba66471da6e384a7da7df6f Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 09:50:51 +0200 Subject: [PATCH 195/650] Fix CRONHOME detection if a symlink is used This will fix CRONHOME detection if `run-rcrons.sh` is a symlink in `/etc/cron.minutely`. Before symlinks would not return the proper path. --- cronjobs/run-crons.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index ab1f482c..0988310c 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -28,7 +28,11 @@ VERBOSE="0" ################################################################ # Find scripts path -CRONHOME=$( dirname $0 ) +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 From 1344f39f9617ad346f5485656b32a413065be745 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 11:43:48 +0200 Subject: [PATCH 196/650] Ensure we set workers to 0 If no workers are found false is returned. Smarty is not able to set a default on `false` values so we have to set it to 0 if the query failed. Fixes #418 --- public/include/classes/worker.class.php | 10 ++-------- public/include/smarty_globals.inc.php | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/public/include/classes/worker.class.php b/public/include/classes/worker.class.php index ec7abdc9..7643a6d1 100644 --- a/public/include/classes/worker.class.php +++ b/public/include/classes/worker.class.php @@ -135,15 +135,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(COUNT(DISTINCT username), 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; } diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index e83b1ca3..33c5fda7 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -24,7 +24,7 @@ if (@$_SESSION['AUTHENTICATED']) { $bitcoin->can_connect() === true ? $dNetworkHashrate = $bitcoin->query('getnetworkhashps') : $dNetworkHashrate = 0; // Fetch some data -$iCurrentActiveWorkers = $worker->getCountAllActiveWorkers(); +if (!$iCurrentActiveWorkers = $worker->getCountAllActiveWorkers()) $iCurrentActiveWorkers = 0; $iCurrentPoolHashrate = $statistics->getCurrentHashrate(); $iCurrentPoolShareRate = $statistics->getCurrentShareRate(); From 875572813b3ca2a50da4a1bcec1034135ce9ed2d Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 13:25:22 +0200 Subject: [PATCH 197/650] Adding last runtime to monitoring page Displays the date/time of the last successful run. Fixes #431 --- cronjobs/cron_end.inc.php | 2 +- public/include/pages/admin/monitoring.inc.php | 8 ++++++++ public/templates/mmcFE/admin/monitoring/default.tpl | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cronjobs/cron_end.inc.php b/cronjobs/cron_end.inc.php index 177e5296..56607c45 100644 --- a/cronjobs/cron_end.inc.php +++ b/cronjobs/cron_end.inc.php @@ -22,7 +22,7 @@ limitations under the License. $monitoring->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 . "_lastrun", "date", time()); // Mark cron as running for monitoring $monitoring->setStatus($cron_name . '_active', "yesno", 0); ?> diff --git a/public/include/pages/admin/monitoring.inc.php b/public/include/pages/admin/monitoring.inc.php index efc20a6c..5a952a99 100644 --- a/public/include/pages/admin/monitoring.inc.php +++ b/public/include/pages/admin/monitoring.inc.php @@ -15,30 +15,35 @@ $aCronStatus = array( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('statistics_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('statistics_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('statistics_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('statistics_lastrun') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('statistics_message') ), ), 'auto_payout' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('auto_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('auto_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('auto_payout_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('auto_payout_lastrun') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('auto_payout_message') ), ), 'archive_cleanup' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('archive_cleanup_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('archive_cleanup_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('archive_cleanup_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('archive_cleanup_lastrun') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('archive_cleanup_message') ), ), 'blockupdate' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('blockupdate_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('blockupdate_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('blockupdate_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('blockupdate_lastrun') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('blockupdate_message') ), ), 'findblock' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('findblock_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('findblock_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('findblock_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('findblock_lastrun') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('findblock_message') ), ) ); @@ -49,6 +54,7 @@ case 'pplns': array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pplns_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pplns_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pplns_payout_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pplns_payout_lastrun') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pplns_payout_message') ), ); break; @@ -57,6 +63,7 @@ case 'pps': array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pps_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pps_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pps_payout_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pps_payout_lastrun') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pps_payout_message') ), ); break; @@ -65,6 +72,7 @@ case 'prop': array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('proportional_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('proportional_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('proportional_payout_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('proportional_payout_lastrun') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('proportional_payout_message') ), ); break; diff --git a/public/templates/mmcFE/admin/monitoring/default.tpl b/public/templates/mmcFE/admin/monitoring/default.tpl index 6213a164..856766ac 100644 --- a/public/templates/mmcFE/admin/monitoring/default.tpl +++ b/public/templates/mmcFE/admin/monitoring/default.tpl @@ -5,6 +5,7 @@ Exit Code Active Runtime + Last Run Message @@ -29,6 +30,8 @@ {/if} {else if $event.STATUS.type == 'time'} {$event.STATUS.value|default:"0"|number_format:"2"} seconds + {else if $event.STATUS.type == 'date'} + {$event.STATUS.value|date_format:"%m/%d %H:%M:%S"} {else} {$event.STATUS.value|default:""} {/if} From 739bd9c7b67009d470c97e9c029e0214a451b87f Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 13:45:54 +0200 Subject: [PATCH 198/650] Properly cache on a per-user basis This will fix an issue with templates of other users being applied to different users logged in. Basically the first cached page would be displayed for all users. Created a new cache key for smarty to allow the user ID to be reference in the cache key. Hence each user has their own cached file which will be used. Improved caching by creating subdirectories for cached files. This way we won't run into a file limit per directory with a lot of cached files. This fixes #430 and the mentioned issue in that report. --- public/include/pages/admin/transactions.inc.php | 2 +- public/include/pages/admin/wallet.inc.php | 2 +- public/include/pages/home.inc.php | 2 +- public/include/pages/statistics.inc.php | 2 +- public/include/pages/statistics/blocks.inc.php | 2 +- public/include/pages/statistics/graphs.inc.php | 2 +- public/include/pages/statistics/pool.inc.php | 2 +- public/include/smarty.inc.php | 2 ++ public/index.php | 2 +- 9 files changed, 10 insertions(+), 8 deletions(-) diff --git a/public/include/pages/admin/transactions.inc.php b/public/include/pages/admin/transactions.inc.php index b25d6f15..fe991ba5 100644 --- a/public/include/pages/admin/transactions.inc.php +++ b/public/include/pages/admin/transactions.inc.php @@ -9,7 +9,7 @@ if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { die("404 Page not found"); } -if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { +if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { $debug->append('No cached version available, fetching from backend', 3); $aTransactions = $transaction->getAllTransactions(@$_REQUEST['start']); if (!$aTransactions) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any transaction', 'TYPE' => 'errormsg'); diff --git a/public/include/pages/admin/wallet.inc.php b/public/include/pages/admin/wallet.inc.php index d0642b14..fee0fd16 100644 --- a/public/include/pages/admin/wallet.inc.php +++ b/public/include/pages/admin/wallet.inc.php @@ -9,7 +9,7 @@ if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { die("404 Page not found"); } -if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { +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'); diff --git a/public/include/pages/home.inc.php b/public/include/pages/home.inc.php index 9431e219..349b197d 100644 --- a/public/include/pages/home.inc.php +++ b/public/include/pages/home.inc.php @@ -6,7 +6,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); // Include markdown library use \Michelf\Markdown; -if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { +if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { $debug->append('No cached version available, fetching from backend', 3); // Fetch active news to display $aNews = $news->getAllActive(); diff --git a/public/include/pages/statistics.inc.php b/public/include/pages/statistics.inc.php index c5035f0c..076ce66f 100644 --- a/public/include/pages/statistics.inc.php +++ b/public/include/pages/statistics.inc.php @@ -4,7 +4,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); -if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { +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->query('getdifficulty'); diff --git a/public/include/pages/statistics/blocks.inc.php b/public/include/pages/statistics/blocks.inc.php index 98d5ed04..3218a9f4 100644 --- a/public/include/pages/statistics/blocks.inc.php +++ b/public/include/pages/statistics/blocks.inc.php @@ -4,7 +4,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); // Grab the last blocks found -if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { +if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { $debug->append('No cached version available, fetching from backend', 3); // Grab the last blocks found $iLimit = 20; diff --git a/public/include/pages/statistics/graphs.inc.php b/public/include/pages/statistics/graphs.inc.php index 4d31aadb..575ce36d 100644 --- a/public/include/pages/statistics/graphs.inc.php +++ b/public/include/pages/statistics/graphs.inc.php @@ -3,7 +3,7 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { +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']); diff --git a/public/include/pages/statistics/pool.inc.php b/public/include/pages/statistics/pool.inc.php index 8386cb07..f3669625 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -3,7 +3,7 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if (!$smarty->isCached('master.tpl', md5(serialize($_REQUEST)))) { +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){ diff --git a/public/include/smarty.inc.php b/public/include/smarty.inc.php index e42e56e3..ac05814e 100644 --- a/public/include/smarty.inc.php +++ b/public/include/smarty.inc.php @@ -25,5 +25,7 @@ if ($config['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; + $smarty_cache_key = md5(serialize($_REQUEST . @$_SESSION['USERDATA']['id'])); } ?> diff --git a/public/index.php b/public/index.php index dbb28f9a..36360172 100644 --- a/public/index.php +++ b/public/index.php @@ -82,7 +82,7 @@ $smarty->assign('DebuggerInfo', $debug->getDebugInfo()); // Display our page if (!@$supress_master) - $smarty->display("master.tpl", md5(serialize($_REQUEST))); + $smarty->display("master.tpl", $smarty_cache_key); // Unset any temporary values here unset($_SESSION['POPUP']); From dfde017267a953e1f609a88c356e0e4698ec049c Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 14:26:53 +0200 Subject: [PATCH 199/650] Get rid of Orphan transaction types This fixes #432 and puts orphans on the same system as unconfirmed transactions. --- cronjobs/blockupdate.php | 2 +- public/include/classes/transaction.class.php | 54 ++++++++----------- .../mmcFE/account/transactions/default.tpl | 29 +++------- .../mmcFE/admin/transactions/default.tpl | 25 ++------- .../templates/mmcFE/global/sidebar_pplns.tpl | 1 + .../templates/mmcFE/global/sidebar_prop.tpl | 1 + 6 files changed, 38 insertions(+), 74 deletions(-) diff --git a/cronjobs/blockupdate.php b/cronjobs/blockupdate.php index 67d106b7..e11063bb 100755 --- a/cronjobs/blockupdate.php +++ b/cronjobs/blockupdate.php @@ -41,7 +41,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $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)) { + if ($block->setConfirmations($aBlock['id'], -1)) { $log->logInfo(" Block marked as orphan"); } else { $log->logError(" Block became orphaned but unable to update database entries"); diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 2d65657b..d01a6d18 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -48,35 +48,6 @@ class Transaction { 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 @@ -227,7 +198,8 @@ class Transaction { $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 + ROUND(IFNULL(t4.credit, 0) - IFNULL(t5.other, 0), 8) AS unconfirmed, + ROUND(IFNULL(t6.credit, 0) - IFNULL(t7.other, 0), 8) AS orphaned FROM ( SELECT sum(t.amount) AS credit @@ -274,10 +246,28 @@ class Transaction { t.type IN ('Donation','Fee') AND b.confirmations < ? ) AND t.account_id = ? - ) AS t5 + ) AS t5, + ( + 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 = -1 + AND t.account_id = ? + ) AS t6, + ( + 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 = -1 + ) + AND t.account_id = ? + ) AS t7 "); 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); + $stmt->bind_param("iiiiiiiiiii", $this->config['confirmations'], $account_id, $account_id, $this->config['confirmations'], $account_id, $this->config['confirmations'], $account_id, $this->config['confirmations'], $account_id, $account_id, $account_id); if (!$stmt->execute()) { $this->debug->append("Unable to execute statement: " . $stmt->error); $this->setErrorMessage("Fetching balance failed"); diff --git a/public/templates/mmcFE/account/transactions/default.tpl b/public/templates/mmcFE/account/transactions/default.tpl index 160b12e4..4bbe7b4b 100644 --- a/public/templates/mmcFE/account/transactions/default.tpl +++ b/public/templates/mmcFE/account/transactions/default.tpl @@ -17,15 +17,9 @@ {assign var=has_confirmed value=false} {section transaction $TRANSACTIONS} {if ( - (($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus')and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) - or ($TRANSACTIONS[transaction].type == 'Donation' and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) - or ($TRANSACTIONS[transaction].type == 'Fee' and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) - or $TRANSACTIONS[transaction].type == 'Credit_PPS' - or $TRANSACTIONS[transaction].type == 'Fee_PPS' - or $TRANSACTIONS[transaction].type == 'Donation_PPS' - or $TRANSACTIONS[transaction].type == 'Debit_AP' - or $TRANSACTIONS[transaction].type == 'Debit_MP' - or $TRANSACTIONS[transaction].type == 'TXFee' + ( ( $TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee' ) and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations ) + or $TRANSACTIONS[transaction].type == 'Credit_PPS' or $TRANSACTIONS[transaction].type == 'Fee_PPS' or $TRANSACTIONS[transaction].type == 'Donation_PPS' + or $TRANSACTIONS[transaction].type == 'Debit_AP' or $TRANSACTIONS[transaction].type == 'Debit_MP' or $TRANSACTIONS[transaction].type == 'TXFee' )} {assign var=has_credits value=true} @@ -67,11 +61,9 @@ {assign var=has_unconfirmed value=false} {section transaction $TRANSACTIONS} - {if ( - ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations - or ($TRANSACTIONS[transaction].type == 'Donation' and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) - or ($TRANSACTIONS[transaction].type == 'Fee' and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) - )} + {if + (($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) + } {assign var=has_unconfirmed value=true} {$TRANSACTIONS[transaction].id} @@ -117,12 +109,7 @@ {assign var=has_orphaned value=false} {section transaction $TRANSACTIONS} - {if ( - $TRANSACTIONS[transaction].type == 'Orphan_Credit' - or $TRANSACTIONS[transaction].type == 'Orphan_Donation' - or $TRANSACTIONS[transaction].type == 'Orphan_Fee' - or $TRANSACTIONS[transaction].type == 'Orphan_Bonus' - )} + {if ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Fee' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Bonus') and $TRANSACTIONS[transaction].confirmations == -1} {$TRANSACTIONS[transaction].id} {$TRANSACTIONS[transaction].timestamp} @@ -131,7 +118,7 @@ {if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if} {$TRANSACTIONS[transaction].amount|number_format:"8"} - {if $TRANSACTIONS[transaction].type == 'Orphan_Credit' or $TRANSACTIONS[transaction].type == 'Orphan_Bonus'} + {if $TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus'} {assign var="orphan_credits" value="`$orphan_credits|default:"0"+$TRANSACTIONS[transaction].amount`"} {else} {assign var="orphan_debits" value="`$orphan_debits|default:"0"+$TRANSACTIONS[transaction].amount`"} diff --git a/public/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl index d611f6d6..aba15f12 100644 --- a/public/templates/mmcFE/admin/transactions/default.tpl +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -21,15 +21,9 @@ {assign var=confirmed value=0} {section transaction $TRANSACTIONS} {if ( - (($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus')and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) - or ($TRANSACTIONS[transaction].type == 'Donation' and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) - or ($TRANSACTIONS[transaction].type == 'Fee' and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) - or $TRANSACTIONS[transaction].type == 'Credit_PPS' - or $TRANSACTIONS[transaction].type == 'Fee_PPS' - or $TRANSACTIONS[transaction].type == 'Donation_PPS' - or $TRANSACTIONS[transaction].type == 'Debit_AP' - or $TRANSACTIONS[transaction].type == 'Debit_MP' - or $TRANSACTIONS[transaction].type == 'TXFee' + ( ( $TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee' ) and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations ) + or $TRANSACTIONS[transaction].type == 'Credit_PPS' or $TRANSACTIONS[transaction].type == 'Fee_PPS' or $TRANSACTIONS[transaction].type == 'Donation_PPS' + or $TRANSACTIONS[transaction].type == 'Debit_AP' or $TRANSACTIONS[transaction].type == 'Debit_MP' or $TRANSACTIONS[transaction].type == 'TXFee' )} {assign var=confirmed value=1} @@ -74,11 +68,7 @@ {assign var=unconfirmed value=0} {section transaction $TRANSACTIONS} - {if ( - ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations - or ($TRANSACTIONS[transaction].type == 'Donation' and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) - or ($TRANSACTIONS[transaction].type == 'Fee' and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) - )} + {if ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations} {assign var=unconfirmed value=1} {$TRANSACTIONS[transaction].id} @@ -118,12 +108,7 @@ {assign var=orphaned value=0} {section transaction $TRANSACTIONS} - {if ( - $TRANSACTIONS[transaction].type == 'Orphan_Credit' - or $TRANSACTIONS[transaction].type == 'Orphan_Donation' - or $TRANSACTIONS[transaction].type == 'Orphan_Fee' - or $TRANSACTIONS[transaction].type == 'Orphan_Bonus' - )} + {if ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Fee' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Bonus') and $TRANSACTIONS[transaction].confirmations == -1} {assign var=orphaned value=1} {$TRANSACTIONS[transaction].id} diff --git a/public/templates/mmcFE/global/sidebar_pplns.tpl b/public/templates/mmcFE/global/sidebar_pplns.tpl index deb0c1cc..79ffec8a 100644 --- a/public/templates/mmcFE/global/sidebar_pplns.tpl +++ b/public/templates/mmcFE/global/sidebar_pplns.tpl @@ -67,6 +67,7 @@ {$GLOBAL.config.currency} Account Balance Confirmed{$GLOBAL.userdata.balance.confirmed|default:"0"} Unconfirmed{$GLOBAL.userdata.balance.unconfirmed|default:"0"} + Orphaned{$GLOBAL.userdata.balance.orphaned|default:"0"}
    diff --git a/public/templates/mmcFE/global/sidebar_prop.tpl b/public/templates/mmcFE/global/sidebar_prop.tpl index 09159318..c19c429d 100644 --- a/public/templates/mmcFE/global/sidebar_prop.tpl +++ b/public/templates/mmcFE/global/sidebar_prop.tpl @@ -62,6 +62,7 @@ {$GLOBAL.config.currency} Account Balance Confirmed{$GLOBAL.userdata.balance.confirmed|default:"0"} Unconfirmed{$GLOBAL.userdata.balance.unconfirmed|default:"0"} + Orphaned{$GLOBAL.userdata.balance.orphaned|default:"0"}
    From 5b504226754e48418cae671647f2511f331a162a Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 15:49:08 +0200 Subject: [PATCH 200/650] Fixing XSS for user registration --- public/include/classes/user.class.php | 7 ++++++- public/templates/mmcFE/admin/user/default.tpl | 4 ++-- public/templates/mmcFE/global/userinfo.tpl | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 68616e3e..53b86d6f 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -442,6 +442,10 @@ class User { **/ public function register($username, $password1, $password2, $pin, $email1='', $email2='') { $this->debug->append("STA " . __METHOD__, 4); + if (strlen($username > 40)) { + $this->setErrorMessage('Username exceeding character limit'); + return false; + } if ($this->getEmail($email1)) { $this->setErrorMessage( 'This e-mail address is already taken' ); return false; @@ -482,8 +486,9 @@ class User { $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('sssss', $username, $password_hash, $email1, $pin_hash, $apikey_hash)) { + if ($this->checkStmt($stmt) && $stmt->bind_param('sssss', $username_clean, $password_hash, $email1, $pin_hash, $apikey_hash)) { if (!$stmt->execute()) { $this->setErrorMessage( 'Unable to register' ); if ($stmt->sqlstate == '23000') $this->setErrorMessage( 'Username or email already registered' ); diff --git a/public/templates/mmcFE/admin/user/default.tpl b/public/templates/mmcFE/admin/user/default.tpl index 8c65c49f..fef10bfa 100644 --- a/public/templates/mmcFE/admin/user/default.tpl +++ b/public/templates/mmcFE/admin/user/default.tpl @@ -48,8 +48,8 @@ {section name=user loop=$USERS|default} {$USERS[user].id} - {$USERS[user].username} - {$USERS[user].email} + {$USERS[user].username|escape} + {$USERS[user].email|escape} {$USERS[user].shares} {$USERS[user].hashrate} {$USERS[user].payout.est_donation|number_format:"8"} diff --git a/public/templates/mmcFE/global/userinfo.tpl b/public/templates/mmcFE/global/userinfo.tpl index 92105bdc..d9745394 100644 --- a/public/templates/mmcFE/global/userinfo.tpl +++ b/public/templates/mmcFE/global/userinfo.tpl @@ -1,5 +1,5 @@ {if $GLOBAL.userdata.username|default} -

    Welcome, {$smarty.session.USERDATA.username} Active Account: {$GLOBAL.fees}% Pool Fee (You are donating {$GLOBAL.userdata.donate_percent}% of your earnings)

    +

    Welcome, {$smarty.session.USERDATA.username|escape} Active Account: {$GLOBAL.fees|escape}% Pool Fee (You are donating {$GLOBAL.userdata.donate_percent|escape}% of your earnings)

    {else}

    Welcome guest, please register to user this pool.

    {/if} From 08359c0d19a82e14c6f0c1e2a9867131dba32cf4 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 15:53:38 +0200 Subject: [PATCH 201/650] Further escaping of user inputs --- public/templates/mmcFE/statistics/blocks/default.tpl | 2 +- public/templates/mmcFE/statistics/blocks/small_table.tpl | 2 +- .../templates/mmcFE/statistics/pool/contributors_hashrate.tpl | 4 ++-- .../templates/mmcFE/statistics/pool/contributors_shares.tpl | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/templates/mmcFE/statistics/blocks/default.tpl b/public/templates/mmcFE/statistics/blocks/default.tpl index f404673c..0856a269 100644 --- a/public/templates/mmcFE/statistics/blocks/default.tpl +++ b/public/templates/mmcFE/statistics/blocks/default.tpl @@ -57,7 +57,7 @@ target and network difficulty and assuming a zero variance scenario. {else if $BLOCKSFOUND[block].confirmations == -1} Orphan {else}{$GLOBAL.confirmations - $BLOCKSFOUND[block].confirmations} left{/if} - {$BLOCKSFOUND[block].finder|default:"unknown"} + {$BLOCKSFOUND[block].finder|default:"unknown"|escape} {$BLOCKSFOUND[block].time|date_format:"%d/%m %H:%M:%S"} {$BLOCKSFOUND[block].difficulty|number_format:"2"} {$BLOCKSFOUND[block].amount|number_format:"2"} diff --git a/public/templates/mmcFE/statistics/blocks/small_table.tpl b/public/templates/mmcFE/statistics/blocks/small_table.tpl index 731d57ef..2b0f8aac 100644 --- a/public/templates/mmcFE/statistics/blocks/small_table.tpl +++ b/public/templates/mmcFE/statistics/blocks/small_table.tpl @@ -14,7 +14,7 @@ {section block $BLOCKSFOUND} {$BLOCKSFOUND[block].height} - {$BLOCKSFOUND[block].finder|default:"unknown"} + {$BLOCKSFOUND[block].finder|default:"unknown"|escape} {$BLOCKSFOUND[block].time|date_format:"%d/%m %H:%M:%S"} {$BLOCKSFOUND[block].shares|number_format} diff --git a/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl b/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl index a2a6ed58..b6168c36 100644 --- a/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl +++ b/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl @@ -17,7 +17,7 @@ {math assign="estday" equation="round(reward / ( diff * pow(2,32) / ( hashrate * 1000 ) / 3600 / 24), 3)" diff=$DIFFICULTY reward=$REWARD hashrate=$CONTRIBHASHES[contrib].hashrate} {$rank++} - {$CONTRIBHASHES[contrib].account} + {$CONTRIBHASHES[contrib].account|escape} {$CONTRIBHASHES[contrib].hashrate|number_format} {$estday|number_format:"3"} {if $GLOBAL.config.price.currency}{($estday * $GLOBAL.price)|default:"n/a"|number_format:"2"}{/if} @@ -27,7 +27,7 @@ {if $GLOBAL.userdata.hashrate > 0}{math assign="myestday" equation="round(reward / ( diff * pow(2,32) / ( hashrate * 1000 ) / 3600 / 24), 3)" diff=$DIFFICULTY reward=$REWARD hashrate=$GLOBAL.userdata.hashrate}{/if} n/a - {$GLOBAL.userdata.username} + {$GLOBAL.userdata.username|escape} {$GLOBAL.userdata.hashrate} {$myestday|number_format:"3"|default:"n/a"} {if $GLOBAL.config.price.currency}{($myestday * $GLOBAL.price)|default:"n/a"|number_format:"2"}{/if} diff --git a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl index 2a482209..232e76b2 100644 --- a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl +++ b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl @@ -14,14 +14,14 @@ {section hashrate $CONTRIBSHARES} {$rank++} - {$CONTRIBSHARES[hashrate].account} + {$CONTRIBSHARES[hashrate].account|escape} {$CONTRIBSHARES[hashrate].shares|number_format} {/section} {if $listed != 1 && $GLOBAL.userdata.username|default:""} n/a - {$GLOBAL.userdata.username} + {$GLOBAL.userdata.username|escape} {$GLOBAL.userdata.shares.valid|number_format} {/if} From 7466689b507159c82cbee796c48c26f1ee58f954 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 15:55:32 +0200 Subject: [PATCH 202/650] further escapes on templates --- public/templates/mmcFE/account/edit/default.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mmcFE/account/edit/default.tpl b/public/templates/mmcFE/account/edit/default.tpl index c15b56d4..fd445286 100644 --- a/public/templates/mmcFE/account/edit/default.tpl +++ b/public/templates/mmcFE/account/edit/default.tpl @@ -4,7 +4,7 @@ - + From 5c0d9921346b2aa23a352a960b58a907bbf84a29 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 15:58:30 +0200 Subject: [PATCH 203/650] fixing smarty cache key generation --- public/include/smarty.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/smarty.inc.php b/public/include/smarty.inc.php index ac05814e..f95180d7 100644 --- a/public/include/smarty.inc.php +++ b/public/include/smarty.inc.php @@ -26,6 +26,6 @@ if ($config['smarty']['cache']) { $smarty->cache_lifetime = $config['smarty']['cache_lifetime']; $smarty->cache_dir = BASEPATH . "templates/cache"; $smarty->use_sub_dirs = true; - $smarty_cache_key = md5(serialize($_REQUEST . @$_SESSION['USERDATA']['id'])); + $smarty_cache_key = md5(serialize($_REQUEST) . serialize(@$_SESSION['USERDATA']['id'])); } ?> From 450cc4d24df975aee4aac80f72cd7cf40a9d7b1f Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 16:07:51 +0200 Subject: [PATCH 204/650] Command-line switch for PHP Binary and Verbosity This will fix #429 --- cronjobs/run-crons.sh | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index 0988310c..de30645a 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -15,9 +15,6 @@ PIDFILE='/tmp/mmcfe-ng-cron.pid' # List of cruns to execute CRONS="findblock.php proportional_payout.php pplns_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php archive_cleanup.php" -# Additional arguments to pass to cronjobs -CRONARGS="-v" - # Output additional runtime information VERBOSE="0" @@ -27,6 +24,20 @@ VERBOSE="0" # # ################################################################ +# Overwrite some settings via command line arguments +while getopts "hvp:" opt; do + case "$opt" in + h|\?) + echo "Usage: $0 [-v] [-p PHP_BINARY]"; + exit 0 + ;; + v) VERBOSE=1 + ;; + p) PHP_BIN=$3; shift; + ;; + esac +done + # Find scripts path if [[ -L $0 ]]; then CRONHOME=$( dirname $( readlink $0 ) ) @@ -70,8 +81,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 From 65047d3f09cc9ed6645eb9d42842c98967a1450e Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 16:09:21 +0200 Subject: [PATCH 205/650] wrong argument option --- cronjobs/run-crons.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index de30645a..1f55bd77 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -33,7 +33,7 @@ while getopts "hvp:" opt; do ;; v) VERBOSE=1 ;; - p) PHP_BIN=$3; shift; + p) PHP_BIN=$2; shift; ;; esac done From 09c00877758ce6dc21fcf2c66d4932ce056ac6be Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 11 Jul 2013 16:50:24 +0200 Subject: [PATCH 206/650] Add tickerupdate to cron monitoring Fixes #439 --- cronjobs/tickerupdate.php | 1 + public/include/pages/admin/monitoring.inc.php | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/cronjobs/tickerupdate.php b/cronjobs/tickerupdate.php index 4b36adb2..875be2b2 100755 --- a/cronjobs/tickerupdate.php +++ b/cronjobs/tickerupdate.php @@ -33,4 +33,5 @@ if ($price = $tools->getPrice()) { $log->logFatal("failed to fetch API data: " . $tools->getError()); } +require_once('cron_end.inc.php'); ?> diff --git a/public/include/pages/admin/monitoring.inc.php b/public/include/pages/admin/monitoring.inc.php index 5a952a99..0d7dd671 100644 --- a/public/include/pages/admin/monitoring.inc.php +++ b/public/include/pages/admin/monitoring.inc.php @@ -45,6 +45,13 @@ $aCronStatus = array( array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('findblock_runtime') ), array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('findblock_lastrun') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('findblock_message') ), + ), + 'tickerupdate' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('tickerupdate_status') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('tickerupdate_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('tickerupdate_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('tickerupdate_lastrun') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('tickerupdate_message') ), ) ); // Payout system specifics From 15e89ad4d3cb7a800f11d692d476003b7896e6bb Mon Sep 17 00:00:00 2001 From: Ilya Stromberg Date: Thu, 11 Jul 2013 19:11:03 +0400 Subject: [PATCH 207/650] (#409) Do not use Memcached if it switched off via config --- public/include/classes/statscache.class.php | 24 +++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/public/include/classes/statscache.class.php b/public/include/classes/statscache.class.php index e84386da..c3e9ea38 100644 --- a/public/include/classes/statscache.class.php +++ b/public/include/classes/statscache.class.php @@ -9,12 +9,16 @@ 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; + 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(); } } /** @@ -26,7 +30,7 @@ class StatsCache extends Memcached { 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); + return $this->cache->set($this->config['memcache']['keyprefix'] . $key, $value, $expiration); } /** @@ -36,7 +40,7 @@ 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)) { + if ($data = $this->cache->get($this->config['memcache']['keyprefix'].$key)) { $this->debug->append("Found key in cache", 3); return $data; } else { @@ -55,7 +59,15 @@ class StatsCache extends Memcached { if ($this->config['memcache']['enabled']) $this->set($key, $data, $expiration); 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); From 8f4b4ed9701611c027eb71aeca6f2a7cd74abe05 Mon Sep 17 00:00:00 2001 From: Ilya Stromberg Date: Thu, 11 Jul 2013 19:35:23 +0400 Subject: [PATCH 208/650] Fix code style --- public/include/classes/statscache.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/include/classes/statscache.class.php b/public/include/classes/statscache.class.php index c3e9ea38..e64d5d02 100644 --- a/public/include/classes/statscache.class.php +++ b/public/include/classes/statscache.class.php @@ -17,8 +17,9 @@ class StatsCache { $this->debug = $debug; if (! $config['memcache']['enabled'] ) { $this->debug->append("Not storing any values in memcache"); + } else { + $this->cache = new Memcached(); } - else { $this->cache = new Memcached(); } } /** From 521bcc80220ce68db186ecfc4acdf641c45477aa Mon Sep 17 00:00:00 2001 From: Ilya Stromberg Date: Thu, 11 Jul 2013 20:19:35 +0400 Subject: [PATCH 209/650] Fix built-in documentation --- public/include/config/global.inc.dist.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 1cf894bf..b564d6a9 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -315,10 +315,12 @@ $config['confirmations'] = 120; /** * Memcache configuration * - * Even though you can disable the memcache for debugging purposes, the memcache - * library is still required for mmcfe-ng to work. You should not disable this in - * a live environment since a lot of data is cached for the website to increase load - * times! + * 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 From 638b8387c36e6edafbde258595c74d011a680c13 Mon Sep 17 00:00:00 2001 From: Iain Kay Date: Thu, 11 Jul 2013 19:17:49 +0000 Subject: [PATCH 210/650] Updated global.inc.php to reflect the new values required for cookie configuration and documented each of the options. --- public/include/config/global.inc.dist.php | 37 ++++++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 1cf894bf..33aa1151 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -349,16 +349,43 @@ $config['memcache']['splay'] = 15; /** * Cookie configiration * - * For multiple installations of this cookie change the cookie name + * You can configure the cookie behaviour to secure your cookies more than the PHP defaults + * + * For multiple installations of mmcfe-ng on the same domain you must change the cookie + * path or change the cookie name to avoid conflicts. + * + * Description + * duration: the amount of time, in seconds, that a cookie should persist in the users + * browser. 0 = until closed; 1440 = 24 minutes. + * + * 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're + * using SSL this will stop a user accidently accessing the site without SSL + * and exposing their session cookie. * * Default: - * path = '/' - * name = 'POOLERCOOKIE' - * domain = '' + * duration = '1440' + * domain = '' + * path = '/' + * name = 'POOLERCOOKIE' + * httponly = true + * secure = false **/ +$config['cookie']['duration'] = '1440'; +$config['cookie']['domain'] = ''; $config['cookie']['path'] = '/'; $config['cookie']['name'] = 'POOLERCOOKIE'; -$config['cookie']['domain'] = ''; +$config['cookie']['httponly'] = true; +$config['cookie']['secure'] = false; /** * Enable or disable the Smarty cache From d2bbc366d16b83baa9c9f713355bdc1a32c27451 Mon Sep 17 00:00:00 2001 From: Iain Kay Date: Thu, 11 Jul 2013 19:26:09 +0000 Subject: [PATCH 211/650] Changed the Cookie Explanation in global.inc.php to be more in line with the rest of the structure. --- public/include/config/global.inc.dist.php | 31 +++++++++++++---------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 33aa1151..a83f47be 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -354,23 +354,28 @@ $config['memcache']['splay'] = 15; * For multiple installations of mmcfe-ng on the same domain you must change the cookie * path or change the cookie name to avoid conflicts. * - * Description - * duration: the amount of time, in seconds, that a cookie should persist in the users - * browser. 0 = until closed; 1440 = 24 minutes. + * Explanation: + * duration: + * the amount of time, in seconds, that a cookie should persist in the users browser. + * 0 = until closed; 1440 = 24 minutes. * - * domain: the only domain name that may access this cookie in the browser + * 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. + * 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. + * 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're - * using SSL this will stop a user accidently accessing the site without SSL - * and exposing their session cookie. + * 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' From 9f4789c707591f21e09c5b9e51aec93ee7d0c986 Mon Sep 17 00:00:00 2001 From: Iain Kay Date: Thu, 11 Jul 2013 19:29:24 +0000 Subject: [PATCH 212/650] In order to read the cookie configuration from include/config/globa.inc.php the session must begin after this has been included. --- public/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/index.php b/public/index.php index dbb28f9a..ad58b9e0 100644 --- a/public/index.php +++ b/public/index.php @@ -24,13 +24,13 @@ define("BASEPATH", "./"); // Our security check define("SECURITY", 1); +// Include our configuration (holding defines for the requires) +if (!include_once(BASEPATH . 'include/config/global.inc.php')) die('Unable to load site configuration'); + // 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'); - // Load Classes, they name defines the $ variable used // We include all needed files here, even though our templates could load them themself require_once(INCLUDE_DIR . '/autoloader.inc.php'); From aac202da2b353eec3b302901ad8e464a7c86ada0 Mon Sep 17 00:00:00 2001 From: Iain Kay Date: Thu, 11 Jul 2013 19:34:58 +0000 Subject: [PATCH 213/650] Pull cookie session params from include/config/global.inc.php before session_start() --- public/index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/public/index.php b/public/index.php index ad58b9e0..2f51e67f 100644 --- a/public/index.php +++ b/public/index.php @@ -28,6 +28,7 @@ define("SECURITY", 1); if (!include_once(BASEPATH . 'include/config/global.inc.php')) die('Unable to load site configuration'); // Start a session +session_set_cookie_params($config['cookie']['duration'], $config['cookie']['path'], $config['cookie']['domain'], $config['cookie']['secure'], $config['cookie']['httponly']); session_start(); $session_id = session_id(); From dfbaf621de80c4715ce020e6c85a240166f0e418 Mon Sep 17 00:00:00 2001 From: Iain Kay Date: Thu, 11 Jul 2013 19:41:50 +0000 Subject: [PATCH 214/650] When destroying a users session on the server we now also remove all session data immediately, rather than relying on garbage collection, and we destroy the cookie on the users browser. --- public/include/classes/user.class.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 68616e3e..86250001 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -387,7 +387,16 @@ class User { **/ public function logoutUser($redirect="index.php") { $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: $redirect"); From a635d2163c82301f0b473245ba6e69b61474ba8f Mon Sep 17 00:00:00 2001 From: Iain Kay Date: Thu, 11 Jul 2013 19:56:10 +0000 Subject: [PATCH 215/650] Added note about php.ini session.gc_maxlifetime value - Important to stop garbage collection removing cookies that should be valid. --- public/include/config/global.inc.dist.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index a83f47be..28a2cd8c 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -357,7 +357,8 @@ $config['memcache']['splay'] = 15; * Explanation: * duration: * the amount of time, in seconds, that a cookie should persist in the users browser. - * 0 = until closed; 1440 = 24 minutes. + * 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 From 3e2608fcef42a3e7b61563e720207a4b7ca54bc1 Mon Sep 17 00:00:00 2001 From: Iain Kay Date: Thu, 11 Jul 2013 20:30:39 +0000 Subject: [PATCH 216/650] Removed unnecessary cookie name variable that was not used. --- public/include/config/global.inc.dist.php | 5 +---- public/index.php | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 28a2cd8c..d13632c5 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -351,8 +351,7 @@ $config['memcache']['splay'] = 15; * * You can configure the cookie behaviour to secure your cookies more than the PHP defaults * - * For multiple installations of mmcfe-ng on the same domain you must change the cookie - * path or change the cookie name to avoid conflicts. + * For multiple installations of mmcfe-ng on the same domain you must change the cookie path. * * Explanation: * duration: @@ -382,14 +381,12 @@ $config['memcache']['splay'] = 15; * duration = '1440' * domain = '' * path = '/' - * name = 'POOLERCOOKIE' * httponly = true * secure = false **/ $config['cookie']['duration'] = '1440'; $config['cookie']['domain'] = ''; $config['cookie']['path'] = '/'; -$config['cookie']['name'] = 'POOLERCOOKIE'; $config['cookie']['httponly'] = true; $config['cookie']['secure'] = false; diff --git a/public/index.php b/public/index.php index 2f51e67f..95d3427c 100644 --- a/public/index.php +++ b/public/index.php @@ -28,7 +28,7 @@ define("SECURITY", 1); if (!include_once(BASEPATH . 'include/config/global.inc.php')) die('Unable to load site configuration'); // Start a session -session_set_cookie_params($config['cookie']['duration'], $config['cookie']['path'], $config['cookie']['domain'], $config['cookie']['secure'], $config['cookie']['httponly']); +session_set_cookie_params(time()+$config['cookie']['duration'], $config['cookie']['path'], $config['cookie']['domain'], $config['cookie']['secure'], $config['cookie']['httponly']); session_start(); $session_id = session_id(); From 4a693e1bd9440b5f77f51c7086398c38c3eb3d4e Mon Sep 17 00:00:00 2001 From: Iain Kay Date: Thu, 11 Jul 2013 20:40:14 +0000 Subject: [PATCH 217/650] Fix bug in sessions where the duration did not increase as user actively browsed site. --- public/index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/public/index.php b/public/index.php index 95d3427c..fa67432c 100644 --- a/public/index.php +++ b/public/index.php @@ -30,6 +30,7 @@ if (!include_once(BASEPATH . 'include/config/global.inc.php')) die('Unable to lo // 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 From 3df40b5bb71e913f0fbe4f7b95dba863ed661281 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 12 Jul 2013 09:48:31 +0200 Subject: [PATCH 218/650] removing newlines --- cronjobs/auto_payout.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cronjobs/auto_payout.php b/cronjobs/auto_payout.php index c55cc913..548e5de8 100755 --- a/cronjobs/auto_payout.php +++ b/cronjobs/auto_payout.php @@ -23,7 +23,7 @@ limitations under the License. require_once('shared.inc.php'); if ($bitcoin->can_connect() !== true) { - $log->logFatal(" unable to connect to RPC server, exiting\n"); + $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); @@ -34,7 +34,7 @@ if ($bitcoin->can_connect() !== true) { $users = $user->getAllAutoPayout(); // Quick summary -$log->logInfo(" found " . count($users) . " queued payout(s)\n"); +$log->logInfo(" found " . count($users) . " queued payout(s)"); // Go through users and run transactions if (! empty($users)) { @@ -77,7 +77,7 @@ if (! empty($users)) { } } } else { - $log->logDebug(" no user has configured their AP > 0\n"); + $log->logDebug(" no user has configured their AP > 0"); } // Cron cleanup and monitoring From 8ec1d2cab39e7d4f75871a42965c508aef039451 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 12 Jul 2013 10:33:42 +0200 Subject: [PATCH 219/650] Adding anonymous account support * Added anonymous flag to accounts table * Added checkbox for anonymous flag in edit account page * Updated user class to support new flag * Updated statistics class to support anonymous and donations * Updated all templates showing usernames to show anonymous instead * Added new SQL `ALTER TABLE` file for upgrading the table Fixes #419 once merged. --- public/include/classes/statistics.class.php | 23 ++++++++++++++----- public/include/classes/transaction.class.php | 1 + public/include/classes/user.class.php | 8 +++---- public/include/pages/account/edit.inc.php | 2 +- .../templates/mmcFE/about/donors/default.tpl | 2 +- .../templates/mmcFE/account/edit/default.tpl | 4 ++++ public/templates/mmcFE/global/userinfo.tpl | 2 +- .../mmcFE/statistics/blocks/default.tpl | 2 +- .../mmcFE/statistics/blocks/small_table.tpl | 2 +- .../statistics/pool/contributors_hashrate.tpl | 2 +- .../statistics/pool/contributors_shares.tpl | 2 +- sql/003_accounts_anonymous.sql | 1 + 12 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 sql/003_accounts_anonymous.sql diff --git a/public/include/classes/statistics.class.php b/public/include/classes/statistics.class.php index 3e375249..e2d1848a 100644 --- a/public/include/classes/statistics.class.php +++ b/public/include/classes/statistics.class.php @@ -61,7 +61,10 @@ 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 FROM " . $this->block->getTableName() . " AS b LEFT JOIN " . $this->user->getTableName() . " AS a ON b.account_id = a.id @@ -327,9 +330,13 @@ class Statistics { case 'shares': $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, + COUNT(s.id) 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 @@ -343,14 +350,18 @@ class Statistics { case 'hashes': $stmt = $this->mysqli->prepare(" SELECT - IFNULL(ROUND(COUNT(id) * POW(2," . $this->config['difficulty'] . ")/600/1000, 2), 0) AS hashrate, - SUBSTRING_INDEX( username, '.', 1 ) AS account + a.donate_percent AS donate_percent, + a.is_anonymous AS is_anonymous, + IFNULL(ROUND(COUNT(t1.id) * POW(2," . $this->config['difficulty'] . ")/600/1000, 2), 0) AS hashrate, + SUBSTRING_INDEX( t1.username, '.', 1 ) AS account FROM ( SELECT id, username FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) AND our_result = 'Y' UNION SELECT id, 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()) diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index d01a6d18..30867cc6 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -127,6 +127,7 @@ class Transaction { 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 diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index a363ff58..ac4fabdf 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -283,7 +283,7 @@ 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; @@ -317,8 +317,8 @@ class User { $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'); @@ -421,7 +421,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, IFNULL(donate_percent, '0') as donate_percent, coin_address, ap_threshold FROM $this->table WHERE id = ? LIMIT 0,1"); diff --git a/public/include/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index 1ced549b..9ab38e49 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -61,7 +61,7 @@ if ($user->isAuthenticated()) { 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/templates/mmcFE/about/donors/default.tpl b/public/templates/mmcFE/about/donors/default.tpl index 2d5efaf3..8d5205ac 100644 --- a/public/templates/mmcFE/about/donors/default.tpl +++ b/public/templates/mmcFE/about/donors/default.tpl @@ -12,7 +12,7 @@ {section name=donor loop=$DONORS} - + diff --git a/public/templates/mmcFE/account/edit/default.tpl b/public/templates/mmcFE/account/edit/default.tpl index fd445286..fcf29c7c 100644 --- a/public/templates/mmcFE/account/edit/default.tpl +++ b/public/templates/mmcFE/account/edit/default.tpl @@ -11,6 +11,10 @@ +
    Username: {$GLOBAL.userdata.username}
    Username: {$GLOBAL.userdata.username|escape}
    User Id: {$GLOBAL.userdata.id}
    API Key: {$GLOBAL.userdata.api_key}
    E-Mail:
    {$DONORS[donor].username}{if $DONORS[donor].is_anonymous|default:"0" == 1}anonymous{else}{$DONORS[donor].username}{/if} {$DONORS[donor].donate_percent} {$DONORS[donor].donation|number_format:"2"}
    Payment Address:
    Donation %: [donation amount in percent (example: 0.5)]
    Automatic Payout Threshold: [{$GLOBAL.config.ap_threshold.min}-{$GLOBAL.config.ap_threshold.max} {$GLOBAL.config.currency}. Set to '0' for no auto payout]
    Anonymous Account: + + +
    4 digit PIN: [The 4 digit PIN you chose when registering]
    diff --git a/public/templates/mmcFE/global/userinfo.tpl b/public/templates/mmcFE/global/userinfo.tpl index d9745394..45e9ad92 100644 --- a/public/templates/mmcFE/global/userinfo.tpl +++ b/public/templates/mmcFE/global/userinfo.tpl @@ -1,5 +1,5 @@ {if $GLOBAL.userdata.username|default} -

    Welcome, {$smarty.session.USERDATA.username|escape} Active Account: {$GLOBAL.fees|escape}% Pool Fee (You are donating {$GLOBAL.userdata.donate_percent|escape}% of your earnings)

    +

    Welcome, {$smarty.session.USERDATA.username|escape} {if $GLOBAL.userdata.is_anonymous}Anonymous{else}Active{/if} Account: {$GLOBAL.fees|escape}% Pool Fee (You are donating {$GLOBAL.userdata.donate_percent|escape}% of your earnings)

    {else}

    Welcome guest, please register to user this pool.

    {/if} diff --git a/public/templates/mmcFE/statistics/blocks/default.tpl b/public/templates/mmcFE/statistics/blocks/default.tpl index 0856a269..2871cd01 100644 --- a/public/templates/mmcFE/statistics/blocks/default.tpl +++ b/public/templates/mmcFE/statistics/blocks/default.tpl @@ -57,7 +57,7 @@ target and network difficulty and assuming a zero variance scenario. {else if $BLOCKSFOUND[block].confirmations == -1} Orphan {else}{$GLOBAL.confirmations - $BLOCKSFOUND[block].confirmations} left{/if} - {$BLOCKSFOUND[block].finder|default:"unknown"|escape} + {if $BLOCKSFOUND[block].is_anonymous|default:"0" == 1}anonymous{else}{$BLOCKSFOUND[block].finder|default:"unknown"|escape}{/if} {$BLOCKSFOUND[block].time|date_format:"%d/%m %H:%M:%S"} {$BLOCKSFOUND[block].difficulty|number_format:"2"} {$BLOCKSFOUND[block].amount|number_format:"2"} diff --git a/public/templates/mmcFE/statistics/blocks/small_table.tpl b/public/templates/mmcFE/statistics/blocks/small_table.tpl index 2b0f8aac..010b3557 100644 --- a/public/templates/mmcFE/statistics/blocks/small_table.tpl +++ b/public/templates/mmcFE/statistics/blocks/small_table.tpl @@ -14,7 +14,7 @@ {section block $BLOCKSFOUND} {$BLOCKSFOUND[block].height} - {$BLOCKSFOUND[block].finder|default:"unknown"|escape} + {if $BLOCKSFOUND[block].is_anonymous|default:"0" == 1}anonymous{else}{$BLOCKSFOUND[block].finder|default:"unknown"|escape}{/if} {$BLOCKSFOUND[block].time|date_format:"%d/%m %H:%M:%S"} {$BLOCKSFOUND[block].shares|number_format} diff --git a/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl b/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl index b6168c36..a7f3cd9f 100644 --- a/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl +++ b/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl @@ -17,7 +17,7 @@ {math assign="estday" equation="round(reward / ( diff * pow(2,32) / ( hashrate * 1000 ) / 3600 / 24), 3)" diff=$DIFFICULTY reward=$REWARD hashrate=$CONTRIBHASHES[contrib].hashrate} {$rank++} - {$CONTRIBHASHES[contrib].account|escape} + {if $CONTRIBHASHES[contrib].is_anonymous|default:"0" == 1}anonymous{else}{$CONTRIBHASHES[contrib].account|escape}{/if} {$CONTRIBHASHES[contrib].hashrate|number_format} {$estday|number_format:"3"} {if $GLOBAL.config.price.currency}{($estday * $GLOBAL.price)|default:"n/a"|number_format:"2"}{/if} diff --git a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl index 232e76b2..3efb4a8e 100644 --- a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl +++ b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl @@ -14,7 +14,7 @@ {section hashrate $CONTRIBSHARES} {$rank++} - {$CONTRIBSHARES[hashrate].account|escape} + {if $CONTRIBHASHES[hashrate].is_anonymous|default:"0" == 1}anonymous{else}{$CONTRIBSHARES[hashrate].account|escape}{/if} {$CONTRIBSHARES[hashrate].shares|number_format} {/section} diff --git a/sql/003_accounts_anonymous.sql b/sql/003_accounts_anonymous.sql new file mode 100644 index 00000000..167ad5aa --- /dev/null +++ b/sql/003_accounts_anonymous.sql @@ -0,0 +1 @@ +ALTER TABLE `accounts` ADD `is_anonymous` BOOLEAN NOT NULL DEFAULT FALSE AFTER `is_admin` ; From 015da23d8e81ec5325a84b5e787fe3d8a0aa0a16 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 12 Jul 2013 19:42:26 +0200 Subject: [PATCH 220/650] disable SQL intensive admin query cache --- cronjobs/statistics.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cronjobs/statistics.php b/cronjobs/statistics.php index 2a145692..3d54fd78 100755 --- a/cronjobs/statistics.php +++ b/cronjobs/statistics.php @@ -42,11 +42,13 @@ $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"); +*/ // Per user share statistics based on all shares submitted $start = microtime(true); From b64ef5a48905a578a05c29547e271cbf6f944ba4 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 12 Jul 2013 19:42:43 +0200 Subject: [PATCH 221/650] Use INFO as default logging level for crons --- cronjobs/shared.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cronjobs/shared.inc.php b/cronjobs/shared.inc.php index 14bed27e..33113c00 100644 --- a/cronjobs/shared.inc.php +++ b/cronjobs/shared.inc.php @@ -39,7 +39,7 @@ require_once(BASEPATH . 'include/config/global.inc.php'); require_once(INCLUDE_DIR . '/autoloader.inc.php'); // Load 3rd party logging library for running crons -$log = new KLogger ( 'logs/' . $cron_name . '.txt' , KLogger::DEBUG ); +$log = new KLogger ( 'logs/' . $cron_name . '.txt' , KLogger::INFO ); $log->LogDebug('Starting ' . $cron_name); // Load the start time for later runtime calculations for monitoring From 0775eaf8c1aef5b40e9bac34e7546395191d12d3 Mon Sep 17 00:00:00 2001 From: typ Date: Sat, 13 Jul 2013 16:41:51 +0200 Subject: [PATCH 222/650] add check for non alpa/-/_ chars --- public/include/classes/user.class.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index a363ff58..cd514966 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -455,6 +455,10 @@ class User { $this->setErrorMessage('Username exceeding character limit'); return false; } + if (!preg_match('/[^a-zA-Z0-9_\-]/', $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; From ed5e320ff60230fa23c48de21856398444c7309e Mon Sep 17 00:00:00 2001 From: typ Date: Sun, 14 Jul 2013 02:43:44 +0200 Subject: [PATCH 223/650] fucked up... --- public/include/classes/user.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index cd514966..a3a01ef5 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -455,7 +455,7 @@ class User { $this->setErrorMessage('Username exceeding character limit'); return false; } - if (!preg_match('/[^a-zA-Z0-9_\-]/', $username)) { + if (preg_match('/[^a-zA-Z0-9_\-]/', $username)) { $this->setErrorMessage('Username may only contain alphanumeric characters'); return false; } From 253d6e8a47d9ae9bbbb3190f5fa00a23c7c74bde Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 14 Jul 2013 21:08:03 +0200 Subject: [PATCH 224/650] Fixing username regexp during registration Fixes wrong regext of #453 --- public/include/classes/user.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index c87c8fbb..bdd87d1e 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -455,7 +455,7 @@ class User { $this->setErrorMessage('Username exceeding character limit'); return false; } - if (preg_match('/[^a-zA-Z0-9_\-]/', $username)) { + if (preg_match('/[^a-z_\-0-9]/i', $username)) { $this->setErrorMessage('Username may only contain alphanumeric characters'); return false; } From d5c14b9b4428b31bbecdada34aeca066eaae0578 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 10:40:23 +0200 Subject: [PATCH 225/650] Update 002_monitoring.sql --- sql/002_monitoring.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/002_monitoring.sql b/sql/002_monitoring.sql index e5a21682..99d44c2f 100644 --- a/sql/002_monitoring.sql +++ b/sql/002_monitoring.sql @@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS `monitoring` ( `value` varchar(25) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf32 COMMENT='Monitoring events from cronjobs'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Monitoring events from cronjobs'; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; From 29d5d36a7e31858a4ab2751110ccebd6383b79ee Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 14 Jul 2013 21:06:26 +0200 Subject: [PATCH 226/650] WiP for one time tokens * Added token type class * Storing Token Type as ID not varchar * Added new system to user class and fixed issues with it * Started on mail verification process in user class * Updated autoloader * Updated change password template Addresses #330 --- public/include/autoloader.inc.php | 2 + public/include/classes/base.class.php | 3 + public/include/classes/token.class.php | 60 +++++++++++++++++ public/include/classes/tokentype.class.php | 21 ++++++ public/include/classes/user.class.php | 64 ++++++++++--------- public/include/pages/password/change.inc.php | 4 +- public/include/pages/password/reset.inc.php | 2 +- .../mmcFE/password/change/default.tpl | 2 +- 8 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 public/include/classes/token.class.php create mode 100644 public/include/classes/tokentype.class.php diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index 281b39c6..69a306bf 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -22,6 +22,8 @@ require_once(INCLUDE_DIR . '/database.inc.php'); require_once(INCLUDE_DIR . '/smarty.inc.php'); // Load classes that need the above as dependencies require_once(CLASS_DIR . '/base.class.php'); +require_once(CLASS_DIR . '/tokentype.class.php'); +require_once(CLASS_DIR . '/token.class.php'); require_once(CLASS_DIR . '/block.class.php'); require_once(CLASS_DIR . '/setting.class.php'); require_once(CLASS_DIR . '/monitoring.class.php'); diff --git a/public/include/classes/base.class.php b/public/include/classes/base.class.php index 6b3c2389..316b245e 100644 --- a/public/include/classes/base.class.php +++ b/public/include/classes/base.class.php @@ -24,6 +24,9 @@ class Base { public function setConfig($config) { $this->config = $config; } + public function setTokenType($tokentype) { + $this->tokentype = $tokentype; + } public function setErrorMessage($msg) { $this->sError = $msg; } diff --git a/public/include/classes/token.class.php b/public/include/classes/token.class.php new file mode 100644 index 00000000..fa472849 --- /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 Insert ID 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 $stmt->insert_id; + $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; + } +} + +$token = new Token(); +$token->setDebug($debug); +$token->setMysql($mysqli); +$token->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/user.class.php b/public/include/classes/user.class.php index bdd87d1e..36b409c5 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -9,11 +9,11 @@ class User { private $userID = false; private $table = 'accounts'; private $user = array(); - private $tableAccountBalance = 'accountBalance'; - public function __construct($debug, $mysqli, $salt, $config) { + public function __construct($debug, $mysqli, $token, $salt, $config) { $this->debug = $debug; $this->mysqli = $mysqli; + $this->token = $token; $this->salt = $salt; $this->config = $config; $this->debug->append("Instantiated User class", 2); @@ -485,13 +485,13 @@ class User { } 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 (?, ?, ?, ?, ?) + INSERT INTO $this->table (username, pass, email, pin, api_key, is_locked) + VALUES (?, ?, ?, ?, ?, ?) "); } else { $stmt = $this->mysqli->prepare(" - INSERT INTO $this->table (username, pass, email, pin, api_key, is_admin) - VALUES (?, ?, ?, ?, ?, 1) + INSERT INTO $this->table (username, pass, email, pin, api_key, is_admin, is_locked) + VALUES (?, ?, ?, ?, ?, 1, 0) "); } @@ -501,14 +501,19 @@ class User { $apikey_hash = $this->getHash($username); $username_clean = strip_tags($username); - if ($this->checkStmt($stmt) && $stmt->bind_param('sssss', $username_clean, $password_hash, $email1, $pin_hash, $apikey_hash)) { - if (!$stmt->execute()) { - $this->setErrorMessage( 'Unable to register' ); - if ($stmt->sqlstate == '23000') $this->setErrorMessage( 'Username or email already registered' ); - return false; + // + $this->config['confirm_email'] ? $is_locked = 1 : $is_locked = 0; + + if ($this->checkStmt($stmt) && $stmt->bind_param('sssssi', $username_clean, $password_hash, $email1, $pin_hash, $apikey_hash, $is_locked) && $stmt->execute()) { + if ($this->config['confirm_email']) { + $this->token->createToken('confirm_email', $stmt->insert_id); + } else { + return true; } - $stmt->close(); - return true; + } else { + $this->setErrorMessage( 'Unable to register' ); + if ($stmt->sqlstate == '23000') $this->setErrorMessage( 'Username or email already registered' ); + return false; } return false; } @@ -520,9 +525,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 ($token = $this->token->getToken($token)) { if ($new1 !== $new2) { $this->setErrorMessage( 'New passwords do not match' ); return false; @@ -532,14 +537,20 @@ class User { return false; } $new_hash = $this->getHash($new1); - $stmt = $this->mysqli->prepare("UPDATE $this->table SET pass = ?, token = NULL WHERE id = ? AND token = ?"); - if ($this->checkStmt($stmt) && $stmt->bind_param('sis', $new_hash, $id, $token) && $stmt->execute() && $stmt->affected_rows === 1) { - return true; + $stmt = $this->mysqli->prepare("UPDATE $this->table SET pass = ? WHERE id = ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('si', $new_hash, $token['account_id']) && $stmt->execute() && $stmt->affected_rows === 1) { + if ($this->token->deleteToken($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('Unable find user for your token'); } + $this->debug->append('Failed to update password:' . $this->mysqli->error); return false; } @@ -549,7 +560,7 @@ class User { * @param smarty object Smarty object for mail templating * @return bool **/ - public function resetPassword($username, $smarty) { + public function initResetPassword($username, $smarty) { $this->debug->append("STA " . __METHOD__, 4); // Fetch the users mail address if (empty($username)) { @@ -560,16 +571,11 @@ class User { $this->setErrorMessage("Unable to find a mail address for user $username"); return false; } - if (!$this->setUserToken($this->getUserId($username))) { + if (!$token = $this->token->getToken($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('TOKEN', $token['token']); $smarty->assign('USERNAME', $username); $smarty->assign('SUBJECT', 'Password Reset Request'); $smarty->assign('WEBSITENAME', $this->config['website']['name']); @@ -608,4 +614,4 @@ class User { } // Make our class available automatically -$user = new User($debug, $mysqli, SALT, $config); +$user = new User($debug, $mysqli, $token, SALT, $config); diff --git a/public/include/pages/password/change.inc.php b/public/include/pages/password/change.inc.php index 8b5f4064..b8115c69 100644 --- a/public/include/pages/password/change.inc.php +++ b/public/include/pages/password/change.inc.php @@ -4,8 +4,8 @@ if (!defined('SECURITY')) die('Hacking attempt'); -if ($_POST['do'] == 'useToken') { - if ($user->useToken($_POST['token'], $_POST['newPassword'], $_POST['newPassword2'])) { +if ($_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'); 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/templates/mmcFE/password/change/default.tpl b/public/templates/mmcFE/password/change/default.tpl index 0d254677..ae3aab55 100644 --- a/public/templates/mmcFE/password/change/default.tpl +++ b/public/templates/mmcFE/password/change/default.tpl @@ -3,7 +3,7 @@ - + From 0ede05a6fd732e7efb801a513bbfabd96c1eebc1 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 14 Jul 2013 22:17:54 +0200 Subject: [PATCH 227/650] Adding email verification * Adding mail verification during account registration * Added new dist file option for mail verification * Added account confirmation page using tokens * Added mail class into user class for password resets * Moved password reset template * Adjusted account registration page * Adjusted user class for email confirmation Also fixed a bug with smarty_cache_key not being used properly if smarty is disabled. Key still needs to be available even if caching is disabled Addresses #330 and prepare the ticket for invitation only system. --- public/include/autoloader.inc.php | 2 +- public/include/classes/token.class.php | 12 ++-- public/include/classes/user.class.php | 64 +++++++++++-------- public/include/config/global.inc.dist.php | 17 +++++ public/include/pages/account/confirm.inc.php | 17 +++++ public/include/pages/password/change.inc.php | 2 +- .../include/pages/register/register.inc.php | 4 +- public/include/smarty.inc.php | 2 +- .../mail/{body.tpl => password/reset.tpl} | 4 +- .../templates/mail/register/confirm_email.tpl | 10 +++ .../mmcFE/account/confirm/default.tpl | 1 + sql/004_tokens.sql | 47 ++++++++++++++ 12 files changed, 143 insertions(+), 39 deletions(-) create mode 100644 public/include/pages/account/confirm.inc.php rename public/templates/mail/{body.tpl => password/reset.tpl} (80%) create mode 100644 public/templates/mail/register/confirm_email.tpl create mode 100644 public/templates/mmcFE/account/confirm/default.tpl create mode 100644 sql/004_tokens.sql diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index 69a306bf..bb765523 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -22,6 +22,7 @@ require_once(INCLUDE_DIR . '/database.inc.php'); require_once(INCLUDE_DIR . '/smarty.inc.php'); // Load classes that need the above as dependencies require_once(CLASS_DIR . '/base.class.php'); +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 . '/block.class.php'); @@ -32,7 +33,6 @@ 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 . '/transaction.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(INCLUDE_DIR . '/lib/Michelf/Markdown.php'); diff --git a/public/include/classes/token.class.php b/public/include/classes/token.class.php index fa472849..c65da13c 100644 --- a/public/include/classes/token.class.php +++ b/public/include/classes/token.class.php @@ -22,7 +22,7 @@ class Token Extends Base { * Insert a new token * @param name string Name of the variable * @param value string Variable value - * @return mixed Insert ID on success, false on failure + * @return mixed Token string on success, false on failure **/ public function createToken($strType, $account_id=NULL) { $strToken = hash('sha256', $account_id.$strType.microtime()); @@ -35,7 +35,7 @@ class Token Extends Base { VALUES (?, ?, ?) "); if ($stmt && $stmt->bind_param('sii', $strToken, $iToken_id, $account_id) && $stmt->execute()) - return $stmt->insert_id; + return $strToken; $this->setErrorMessage('Unable to create new token'); $this->debug->append('Failed to create new token in database: ' . $this->mysqli->error); return false; @@ -54,7 +54,7 @@ class Token Extends Base { } } -$token = new Token(); -$token->setDebug($debug); -$token->setMysql($mysqli); -$token->setTokenType($tokentype); +$oToken = new Token(); +$oToken->setDebug($debug); +$oToken->setMysql($mysqli); +$oToken->setTokenType($tokentype); diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 36b409c5..9927ac30 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -10,16 +10,21 @@ class User { private $table = 'accounts'; private $user = array(); - public function __construct($debug, $mysqli, $token, $salt, $config) { + public function __construct($debug, $mysqli, $salt, $config) { $this->debug = $debug; $this->mysqli = $mysqli; - $this->token = $token; $this->salt = $salt; $this->config = $config; $this->debug->append("Instantiated User class", 2); } // get and set methods + public function setMail($mail) { + $this->mail = $mail; + } + public function setToken($token) { + $this->token= $token; + } private function setErrorMessage($msg) { $this->sError = $msg; } @@ -484,14 +489,16 @@ class User { return false; } if ($this->mysqli->query("SELECT id FROM $this->table LIMIT 1")->num_rows > 0) { + $this->config['accounts']['confirm_email'] ? $is_locked = 1 : $is_locked = 0; $stmt = $this->mysqli->prepare(" INSERT INTO $this->table (username, pass, email, pin, api_key, is_locked) VALUES (?, ?, ?, ?, ?, ?) "); } else { + $is_locked = 0; $stmt = $this->mysqli->prepare(" INSERT INTO $this->table (username, pass, email, pin, api_key, is_admin, is_locked) - VALUES (?, ?, ?, ?, ?, 1, 0) + VALUES (?, ?, ?, ?, ?, 1, ?) "); } @@ -501,17 +508,29 @@ class User { $apikey_hash = $this->getHash($username); $username_clean = strip_tags($username); - // - $this->config['confirm_email'] ? $is_locked = 1 : $is_locked = 0; - if ($this->checkStmt($stmt) && $stmt->bind_param('sssssi', $username_clean, $password_hash, $email1, $pin_hash, $apikey_hash, $is_locked) && $stmt->execute()) { - if ($this->config['confirm_email']) { - $this->token->createToken('confirm_email', $stmt->insert_id); + if ($this->config['accounts']['confirm_email']) { + 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'); + 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; } @@ -548,7 +567,7 @@ class User { $this->setErrorMessage('Unable to set new password'); } } else { - $this->setErrorMessage('Unable find user for your token'); + $this->setErrorMessage('Invalid token'); } $this->debug->append('Failed to update password:' . $this->mysqli->error); return false; @@ -557,35 +576,26 @@ class User { /** * 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 initResetPassword($username, $smarty) { + public function initResetPassword($username) { $this->debug->append("STA " . __METHOD__, 4); // Fetch the users mail address if (empty($username)) { $this->serErrorMessage("Username must not be empty"); return false; } - if (!$email = $this->getUserEmail($username)) { + if (!$aData['email'] = $this->getUserEmail($username)) { $this->setErrorMessage("Unable to find a mail address for user $username"); return false; } - if (!$token = $this->token->getToken($this->token->createToken('password_reset', $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; } - $smarty->assign('TOKEN', $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"); @@ -614,4 +624,6 @@ class User { } // Make our class available automatically -$user = new User($debug, $mysqli, $token, SALT, $config); +$user = new User($debug, $mysqli, SALT, $config); +$user->setMail($mail); +$user->setToken($oToken); diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 25667017..4c7595cc 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -121,6 +121,23 @@ $config['website']['theme'] = 'mmcFE'; $config['website']['mobile'] = true; $config['website']['mobile_theme'] = 'mobile'; +/** + * Account specific settings + * + * Explanation + * You can change some defaults on how accounts are created or registered + * By default, all newly created accounts will require an email verificaiton. + * Only after acitivating an account the user will be able to login + * + * Options: + * confirm_email : Send confirmation mail to user after registration + * + * Defaults: + * confirm_email : true + * + **/ +$config['accounts']['confirm_email'] = true; + /** * Some basic access restrictions on some pages * 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/password/change.inc.php b/public/include/pages/password/change.inc.php index b8115c69..b45b3ee4 100644 --- a/public/include/pages/password/change.inc.php +++ b/public/include/pages/password/change.inc.php @@ -11,7 +11,7 @@ if ($_POST['do'] == 'resetPassword') { $_SESSION['POPUP'][] = array('CONTENT' => $user->getError(), 'TYPE' => 'errormsg'); } } - // 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 01a27e10..526ef247 100644 --- a/public/include/pages/register/register.inc.php +++ b/public/include/pages/register/register.inc.php @@ -20,7 +20,7 @@ if($config['recaptcha']['enabled'] && $_POST["recaptcha_response_field"] && $_PO if ($setting->getValue('lock_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']) && !$setting->getValue('lock_registration')) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Account created, please login'); + $config['accounts']['confirm_email'] ? $_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'); } @@ -37,7 +37,7 @@ if($config['recaptcha']['enabled'] && $_POST["recaptcha_response_field"] && $_PO if ($setting->getValue('lock_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']) && !$setting->getValue('lock_registration')) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Account created, please login'); + $config['accounts']['confirm_email'] ? $_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'); } diff --git a/public/include/smarty.inc.php b/public/include/smarty.inc.php index f95180d7..dfd1f4f6 100644 --- a/public/include/smarty.inc.php +++ b/public/include/smarty.inc.php @@ -18,6 +18,7 @@ $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 if ($config['smarty']['cache']) { @@ -26,6 +27,5 @@ if ($config['smarty']['cache']) { $smarty->cache_lifetime = $config['smarty']['cache_lifetime']; $smarty->cache_dir = BASEPATH . "templates/cache"; $smarty->use_sub_dirs = true; - $smarty_cache_key = md5(serialize($_REQUEST) . serialize(@$_SESSION['USERDATA']['id'])); } ?> diff --git a/public/templates/mail/body.tpl b/public/templates/mail/password/reset.tpl similarity index 80% rename from public/templates/mail/body.tpl rename to public/templates/mail/password/reset.tpl index a80b579a..fe488dca 100644 --- a/public/templates/mail/body.tpl +++ b/public/templates/mail/password/reset.tpl @@ -1,8 +1,8 @@ -

    Hello {$USERNAME},


    +

    Hello {$DATA.username},


    You have requested a password reset through our online form. In order to complete the request please follow this link:

    -

    http://{$smarty.server.SERVER_NAME}{$smarty.server.PHP_SELF}?page=password&action=change&token={$TOKEN}

    +

    http://{$smarty.server.SERVER_NAME}{$smarty.server.PHP_SELF}?page=password&action=change&token={$DATA.token}

    You will be asked to change your password. You can then use this new password to login to your account.

    Cheers,

    Website Administration

    diff --git a/public/templates/mail/register/confirm_email.tpl b/public/templates/mail/register/confirm_email.tpl new file mode 100644 index 00000000..67ea72c5 --- /dev/null +++ b/public/templates/mail/register/confirm_email.tpl @@ -0,0 +1,10 @@ + + +

    Hello {$DATA.username},


    +

    You have create a new account. In order to complete the registration process please follow this link:

    +

    http://{$smarty.server.SERVER_NAME}{$smarty.server.PHP_SELF}?page=account&action=confirm&token={$DATA.token}

    +

    +

    Cheers,

    +

    Website Administration

    + + diff --git a/public/templates/mmcFE/account/confirm/default.tpl b/public/templates/mmcFE/account/confirm/default.tpl new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/public/templates/mmcFE/account/confirm/default.tpl @@ -0,0 +1 @@ + diff --git a/sql/004_tokens.sql b/sql/004_tokens.sql new file mode 100644 index 00000000..439c3ab5 --- /dev/null +++ b/sql/004_tokens.sql @@ -0,0 +1,47 @@ +SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; + + +CREATE TABLE IF NOT EXISTS `tokens` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `account_id` int(11) NOT NULL, + `token` varchar(65) NOT NULL, + `type` tinyint(4) NOT NULL, + `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `token` (`token`), + KEY `account_id` (`account_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; + + +CREATE TABLE IF NOT EXISTS `token_types` ( + `id` tinyint(4) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(25) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ; + +INSERT INTO `token_types` (`id`, `name`) VALUES +(1, 'password_reset'), +(2, 'confirm_email'); + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; + +ALTER TABLE `accounts` DROP `token`; From be9a8d3fda49d79e5aa8a85cca6b4627b5688b6e Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 09:05:44 +0200 Subject: [PATCH 228/650] Go through activiation even for admin accounts Fixes an issue with accounts being locked after trying to activate it. Addresses #330 --- public/include/classes/user.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 9927ac30..ef640098 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -421,7 +421,7 @@ class User { * @param userID int User ID * return data array Database fields as used in SELECT **/ - public function getUserData($userID) { + public function getUserData($userID) e $this->debug->append("STA " . __METHOD__, 4); $this->debug->append("Fetching user information for user id: $userID"); $stmt = $this->mysqli->prepare(" @@ -495,7 +495,6 @@ class User { VALUES (?, ?, ?, ?, ?, ?) "); } else { - $is_locked = 0; $stmt = $this->mysqli->prepare(" INSERT INTO $this->table (username, pass, email, pin, api_key, is_admin, is_locked) VALUES (?, ?, ?, ?, ?, 1, ?) From 7cc1e2543c71221c000429c1328185796fecda10 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 09:06:54 +0200 Subject: [PATCH 229/650] fixing syntax error --- public/include/classes/user.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index ef640098..52d19c44 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -421,7 +421,7 @@ class User { * @param userID int User ID * return data array Database fields as used in SELECT **/ - public function getUserData($userID) e + public function getUserData($userID) { $this->debug->append("STA " . __METHOD__, 4); $this->debug->append("Fetching user information for user id: $userID"); $stmt = $this->mysqli->prepare(" From bd32dfa9f8f6f3a9d7108d7185ea73e525948011 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 12:25:16 +0200 Subject: [PATCH 230/650] Adding invitation system to mmcfe-ng core This will allow users to send invitations to other people via email. Each account will still need to confirm the email address if the option is enabled. Addresses #330, will need to allow pool operators to enable this feature even with registrations turned off. --- public/include/autoloader.inc.php | 1 + public/include/classes/base.class.php | 6 + public/include/classes/invitation.class.php | 106 ++++++++++++++++++ public/include/classes/user.class.php | 23 +++- public/include/config/global.inc.dist.php | 17 ++- .../include/pages/account/invitations.inc.php | 25 +++++ .../include/pages/register/register.inc.php | 8 +- public/include/smarty_globals.inc.php | 1 + public/templates/mail/invitations/body.tpl | 11 ++ .../mmcFE/account/invitations/default.tpl | 43 +++++++ public/templates/mmcFE/global/navigation.tpl | 1 + public/templates/mmcFE/register/default.tpl | 3 + ..._tokens.sql => 004_tokens_invitations.sql} | 29 ++++- 13 files changed, 261 insertions(+), 13 deletions(-) create mode 100644 public/include/classes/invitation.class.php create mode 100644 public/include/pages/account/invitations.inc.php create mode 100644 public/templates/mail/invitations/body.tpl create mode 100644 public/templates/mmcFE/account/invitations/default.tpl rename sql/{004_tokens.sql => 004_tokens_invitations.sql} (60%) diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index bb765523..a5b21621 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -29,6 +29,7 @@ require_once(CLASS_DIR . '/block.class.php'); require_once(CLASS_DIR . '/setting.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'); diff --git a/public/include/classes/base.class.php b/public/include/classes/base.class.php index 316b245e..836ee3b3 100644 --- a/public/include/classes/base.class.php +++ b/public/include/classes/base.class.php @@ -15,6 +15,9 @@ class Base { public function setMysql($mysqli) { $this->mysqli = $mysqli; } + public function setMail($mail) { + $this->mail = $mail; + } public function setSmarty($smarty) { $this->smarty = $smarty; } @@ -24,6 +27,9 @@ class Base { public function setConfig($config) { $this->config = $config; } + public function setToken($token) { + $this->token = $token; + } public function setTokenType($tokentype) { $this->tokentype = $tokentype; } diff --git a/public/include/classes/invitation.class.php b/public/include/classes/invitation.class.php new file mode 100644 index 00000000..ef640420 --- /dev/null +++ b/public/include/classes/invitation.class.php @@ -0,0 +1,106 @@ +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; + } + + 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; + } + public function getByEmail($strEmail) { + $this->debug->append("STA " . __METHOD__, 4); + return $this->getSingle($strEmail, 'id', 'email', 's'); + } + + public function getByTokenId($token_id) { + $this->debug->append("STA " . __METHOD__, 4); + return $this->getSingle($token_id, 'id', 'token_id'); + } + 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); + } + 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; + } + 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; + } +} + +$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/user.class.php b/public/include/classes/user.class.php index 52d19c44..64427486 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -454,7 +454,7 @@ 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'); @@ -488,8 +488,25 @@ class User { $this->setErrorMessage( 'Invalid PIN' ); return false; } + if (isset($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; + } + 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->config['accounts']['confirm_email'] ? $is_locked = 1 : $is_locked = 0; + $this->config['accounts']['confirm_email']['enabled'] ? $is_locked = 1 : $is_locked = 0; $stmt = $this->mysqli->prepare(" INSERT INTO $this->table (username, pass, email, pin, api_key, is_locked) VALUES (?, ?, ?, ?, ?, ?) @@ -508,7 +525,7 @@ class User { $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->config['accounts']['confirm_email']) { + if ($this->config['accounts']['confirm_email']['enabled']) { if ($token = $this->token->createToken('confirm_email', $stmt->insert_id)) { $aData['username'] = $username_clean; $aData['token'] = $token; diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 4c7595cc..deeba5b9 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -129,14 +129,27 @@ $config['website']['mobile_theme'] = 'mobile'; * By default, all newly created accounts will require an email verificaiton. * Only after acitivating an account the user will be able to login * + * 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. They are listed on the accounts + * page. + * + * You can limit the number of registrations send per account via configuration + * variable. + * * Options: * confirm_email : Send confirmation mail to user after registration + * invitations : Enable or disable the invitation system + * count : Maximum invitations a user is able to send * * Defaults: * confirm_email : true - * + * invitations : true + * count : 5 **/ -$config['accounts']['confirm_email'] = true; +$config['accounts']['confirm_email']['enabled'] = true; +$config['accounts']['invitations']['enabled'] = true; +$config['accounts']['invitations']['count'] = 5; /** * Some basic access restrictions on some pages diff --git a/public/include/pages/account/invitations.inc.php b/public/include/pages/account/invitations.inc.php new file mode 100644 index 00000000..cd8d644a --- /dev/null +++ b/public/include/pages/account/invitations.inc.php @@ -0,0 +1,25 @@ +isAuthenticated()) { + if ($config['accounts']['invitations']['enabled']) { + 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/register/register.inc.php b/public/include/pages/register/register.inc.php index 526ef247..9a099816 100644 --- a/public/include/pages/register/register.inc.php +++ b/public/include/pages/register/register.inc.php @@ -19,8 +19,8 @@ if($config['recaptcha']['enabled'] && $_POST["recaptcha_response_field"] && $_PO $smarty->assign("RECAPTCHA", recaptcha_get_html($config['recaptcha']['public_key'])); if ($setting->getValue('lock_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']) && !$setting->getValue('lock_registration')) { - $config['accounts']['confirm_email'] ? $_SESSION['POPUP'][] = array('CONTENT' => 'Please check your mailbox to activate this account') : $_SESSION['POPUP'][] = array('CONTENT' => 'Account created, please login'); + } else if ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $_POST['token']) && !$setting->getValue('lock_registration')) { + $config['accounts']['confirm_email']['enabled'] ? $_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'); } @@ -36,8 +36,8 @@ if($config['recaptcha']['enabled'] && $_POST["recaptcha_response_field"] && $_PO } else { if ($setting->getValue('lock_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']) && !$setting->getValue('lock_registration')) { - $config['accounts']['confirm_email'] ? $_SESSION['POPUP'][] = array('CONTENT' => 'Please check your mailbox to activate this account') : $_SESSION['POPUP'][] = array('CONTENT' => 'Account created, please login'); + } else if ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $_POST['token']) && !$setting->getValue('lock_registration')) { + $config['accounts']['confirm_email']['enabled'] ? $_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'); } diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index a3a47e66..d0d5dbbf 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -48,6 +48,7 @@ $aGlobal = array( 'chaininfo' => $config['chaininfo'], 'config' => array( 'website' => array( 'title' => $config['website']['title'], 'acl' => $config['website']['acl'] ), + 'accounts' => $config['accounts'], 'price' => array( 'currency' => $config['price']['currency'] ), 'targetdiff' => $config['difficulty'], 'currency' => $config['currency'], diff --git a/public/templates/mail/invitations/body.tpl b/public/templates/mail/invitations/body.tpl new file mode 100644 index 00000000..353b82e0 --- /dev/null +++ b/public/templates/mail/invitations/body.tpl @@ -0,0 +1,11 @@ + + +

    Hello valued miner,


    +

    {$DATA.username} invited you to participate on this pool: +

    http://{$smarty.server.SERVER_NAME}{$smarty.server.PHP_SELF}?page=register&token={$DATA.token}

    +{if $DATA.message}

    Personal message:

    {$DATA.message}

    {/if} +

    +

    Cheers,

    +

    Website Administration

    + + diff --git a/public/templates/mmcFE/account/invitations/default.tpl b/public/templates/mmcFE/account/invitations/default.tpl new file mode 100644 index 00000000..b8d6743a --- /dev/null +++ b/public/templates/mmcFE/account/invitations/default.tpl @@ -0,0 +1,43 @@ +{include file="global/block_header.tpl" ALIGN="left" BLOCK_HEADER="Invitations"} +
    + + + +
    New Password:
    New Password Repeat:
    + + + + + + + + + + + +
    E-Mail
    Message
    + +
    + +{include file="global/block_footer.tpl"} + +{include file="global/block_header.tpl" ALIGN="right" BLOCK_HEADER="Past Invitations"} + + + + + + + + + +{section name=invite loop=$INVITATIONS} + + + + + +{/section} + +
    E-MailSentActivated
    {$INVITATIONS[invite].email}{$INVITATIONS[invite].time|date_format:"%d/%m/%Y %H:%M:%S"}
    +{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 42c89e9a..4d3d3621 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -7,6 +7,7 @@
  • My Workers
  • Transactions
  • Notifications
  • + {if $GLOBAL.config.accounts.invitations}
  • Invitations
  • {/if} {/if} diff --git a/public/templates/mmcFE/register/default.tpl b/public/templates/mmcFE/register/default.tpl index 3316dfa9..7253a243 100644 --- a/public/templates/mmcFE/register/default.tpl +++ b/public/templates/mmcFE/register/default.tpl @@ -1,6 +1,9 @@ {include file="global/block_header.tpl" BLOCK_HEADER="Join our pool" BLOCK_STYLE="clear:none;"}
    +{if $smarty.request.token|default:""} + +{/if} diff --git a/sql/004_tokens.sql b/sql/004_tokens_invitations.sql similarity index 60% rename from sql/004_tokens.sql rename to sql/004_tokens_invitations.sql index 439c3ab5..3ed5a8dc 100644 --- a/sql/004_tokens.sql +++ b/sql/004_tokens_invitations.sql @@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS `tokens` ( /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; SET time_zone = "+00:00"; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; @@ -34,14 +34,35 @@ CREATE TABLE IF NOT EXISTS `token_types` ( `id` tinyint(4) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(25) NOT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ; +) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=4 ; INSERT INTO `token_types` (`id`, `name`) VALUES (1, 'password_reset'), -(2, 'confirm_email'); +(2, 'confirm_email'), +(3, 'invitation'); /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; -ALTER TABLE `accounts` DROP `token`; +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; + + +CREATE TABLE IF NOT EXISTS `invitations` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `account_id` int(11) unsigned NOT NULL, + `email` varchar(50) CHARACTER SET utf8 NOT NULL, + `token_id` int(11) NOT NULL, + `is_activated` tinyint(1) NOT NULL DEFAULT '0', + `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; From 7f4f5cd343c8481ab825948b12061563eb562d28 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 12:43:29 +0200 Subject: [PATCH 231/650] Make invitations configurable via admin panel Invitations can now be configured through admin panel settings. By default, invitations are enabled. Invitation system is also available if registrations are disabled. To completely remove the ability of new users to sign up, disable both registration and invitations. Fixes #330 --- public/include/config/global.inc.dist.php | 7 +-- .../include/pages/account/invitations.inc.php | 2 +- public/include/pages/admin/settings.inc.php | 1 + public/include/pages/register.inc.php | 2 +- .../include/pages/register/register.inc.php | 46 +++++++++---------- public/include/smarty_globals.inc.php | 1 + .../mmcFE/admin/settings/default.tpl | 10 ++++ public/templates/mmcFE/global/navigation.tpl | 2 +- 8 files changed, 40 insertions(+), 31 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index deeba5b9..e66197fe 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -131,24 +131,21 @@ $config['website']['mobile_theme'] = 'mobile'; * * 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. They are listed on the accounts - * page. + * 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: * confirm_email : Send confirmation mail to user after registration - * invitations : Enable or disable the invitation system * count : Maximum invitations a user is able to send * * Defaults: * confirm_email : true - * invitations : true * count : 5 **/ $config['accounts']['confirm_email']['enabled'] = true; -$config['accounts']['invitations']['enabled'] = true; $config['accounts']['invitations']['count'] = 5; /** diff --git a/public/include/pages/account/invitations.inc.php b/public/include/pages/account/invitations.inc.php index cd8d644a..b12e2d4e 100644 --- a/public/include/pages/account/invitations.inc.php +++ b/public/include/pages/account/invitations.inc.php @@ -4,7 +4,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); if ($user->isAuthenticated()) { - if ($config['accounts']['invitations']['enabled']) { + 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') { diff --git a/public/include/pages/admin/settings.inc.php b/public/include/pages/admin/settings.inc.php index dab1f096..9bc7ef3b 100644 --- a/public/include/pages/admin/settings.inc.php +++ b/public/include/pages/admin/settings.inc.php @@ -19,6 +19,7 @@ if (@$_REQUEST['do'] == 'save' && !empty($_REQUEST['data'])) { // Fetch settings to propagate to template $smarty->assign("MAINTENANCE", $setting->getValue('maintenance')); $smarty->assign("LOCKREGISTRATION", $setting->getValue('lock_registration')); +$smarty->assign("DISABLEINVITATIONS", $setting->getValue('disable_invitations')); // Tempalte specifics $smarty->assign("CONTENT", "default.tpl"); diff --git a/public/include/pages/register.inc.php b/public/include/pages/register.inc.php index d47c67ed..9b338b51 100644 --- a/public/include/pages/register.inc.php +++ b/public/include/pages/register.inc.php @@ -3,7 +3,7 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if ($setting->getValue('lock_registration')) { +if ($setting->getValue('lock_registration') && !$config['accounts']['invitations']['enabled']) { $_SESSION['POPUP'][] = array('CONTENT' => 'Account registration is currently disabled. Please try again later.', 'TYPE' => 'errormsg'); $smarty->assign("CONTENT", "disabled.tpl"); } else { diff --git a/public/include/pages/register/register.inc.php b/public/include/pages/register/register.inc.php index 9a099816..9e438853 100644 --- a/public/include/pages/register/register.inc.php +++ b/public/include/pages/register/register.inc.php @@ -13,33 +13,33 @@ if ($config['recaptcha']['enabled']) { ); } -// 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 ($setting->getValue('lock_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'], $_POST['token']) && !$setting->getValue('lock_registration')) { +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 { + // 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 ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $_POST['token'])) { + $config['accounts']['confirm_email']['enabled'] ? $_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 ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $_POST['token'])) { $config['accounts']['confirm_email']['enabled'] ? $_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 ($setting->getValue('lock_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'], $_POST['token']) && !$setting->getValue('lock_registration')) { - $config['accounts']['confirm_email']['enabled'] ? $_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'); } } diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index d0d5dbbf..5f6ca959 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -49,6 +49,7 @@ $aGlobal = array( 'config' => array( 'website' => array( 'title' => $config['website']['title'], 'acl' => $config['website']['acl'] ), 'accounts' => $config['accounts'], + 'disable_invitations' => $setting->getValue('disable_invitations'), 'price' => array( 'currency' => $config['price']['currency'] ), 'targetdiff' => $config['difficulty'], 'currency' => $config['currency'], diff --git a/public/templates/mmcFE/admin/settings/default.tpl b/public/templates/mmcFE/admin/settings/default.tpl index a2ffbd5b..154c39d1 100644 --- a/public/templates/mmcFE/admin/settings/default.tpl +++ b/public/templates/mmcFE/admin/settings/default.tpl @@ -30,6 +30,16 @@ + + + + +
    Disable Invitations + +
    diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 4d3d3621..79d36af8 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -7,7 +7,7 @@
  • My Workers
  • Transactions
  • Notifications
  • - {if $GLOBAL.config.accounts.invitations}
  • Invitations
  • {/if} + {if !$GLOBAL.config.disable_invitations}
  • Invitations
  • {/if} {/if} From bf3cd25326eb054767de77f96f6ba8768205ade4 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 12:52:55 +0200 Subject: [PATCH 232/650] removing unused token methods --- public/include/classes/user.class.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 64427486..8c108dbd 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -49,9 +49,6 @@ class User { 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'); } @@ -61,9 +58,6 @@ class User { public function getUserFailed($id) { return $this->getSingle($id, 'failed_logins', 'id'); } - public function getIdFromToken($token) { - return $this->getSingle($token, 'id', 'token', 's'); - } public function isLocked($id) { return $this->getUserLocked($id); } @@ -78,10 +72,6 @@ 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' => setHash($id.time())); - return $this->updateSingle($id, $field); - } public function setUserFailed($id, $value) { $field = array( 'name' => 'failed_logins', 'type' => 'i', 'value' => $value); return $this->updateSingle($id, $field); From 8f720625585d25d3f6dcd2cb71d93f1eef987d2d Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 13:44:22 +0200 Subject: [PATCH 233/650] Fixing issue with registration disabled and invitations This will fix an issue with certain combinations of registration and/or invitations being enabled or disabled. Addresses #330 --- public/include/pages/register.inc.php | 5 ++++- public/include/pages/register/register.inc.php | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/include/pages/register.inc.php b/public/include/pages/register.inc.php index 9b338b51..2866a4b6 100644 --- a/public/include/pages/register.inc.php +++ b/public/include/pages/register.inc.php @@ -3,9 +3,12 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if ($setting->getValue('lock_registration') && !$config['accounts']['invitations']['enabled']) { +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('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 . '/lib/recaptchalib.php'); diff --git a/public/include/pages/register/register.inc.php b/public/include/pages/register/register.inc.php index 9e438853..560961f6 100644 --- a/public/include/pages/register/register.inc.php +++ b/public/include/pages/register/register.inc.php @@ -15,6 +15,8 @@ if ($config['recaptcha']['enabled']) { 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('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($config['recaptcha']['enabled'] && $_POST["recaptcha_response_field"] && $_POST["recaptcha_response_field"]!=''){ From 0f00f7d322826ecfc2f64e77f377f9031cec1275 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 13:46:20 +0200 Subject: [PATCH 234/650] Another fix for registration issues Addresses #330 --- public/include/pages/register.inc.php | 2 +- public/include/pages/register/register.inc.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/include/pages/register.inc.php b/public/include/pages/register.inc.php index 2866a4b6..01b71b18 100644 --- a/public/include/pages/register.inc.php +++ b/public/include/pages/register.inc.php @@ -6,7 +6,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); 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('disable_invitations') && !isset($_GET['token'])) { +} 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 { diff --git a/public/include/pages/register/register.inc.php b/public/include/pages/register/register.inc.php index 560961f6..ab43ba34 100644 --- a/public/include/pages/register/register.inc.php +++ b/public/include/pages/register/register.inc.php @@ -15,7 +15,7 @@ if ($config['recaptcha']['enabled']) { 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('disable_invitations') && !isset($_POST['token'])) { +} 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 From 525c0ab0090a67273b455ed846a942a100ec07a5 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 14:28:54 +0200 Subject: [PATCH 235/650] Fixing PHP Warning on unsert token on register Addresses #330 and cleans up PHP Log --- public/include/classes/user.class.php | 2 +- public/include/pages/register/register.inc.php | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 8c108dbd..6d7a6d27 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -478,7 +478,7 @@ class User { $this->setErrorMessage( 'Invalid PIN' ); return false; } - if (isset($strToken)) { + if (isset($strToken) && !empty($strToken)) { $aToken = $this->token->getToken($strToken); // Circle dependency, so we create our own object here $invitation = new Invitation(); diff --git a/public/include/pages/register/register.inc.php b/public/include/pages/register/register.inc.php index ab43ba34..ca165e3b 100644 --- a/public/include/pages/register/register.inc.php +++ b/public/include/pages/register/register.inc.php @@ -22,7 +22,8 @@ if ($setting->getValue('disable_invitations') && $setting->getValue('lock_regist 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 ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $_POST['token'])) { + isset($_POST['token']) ? $token = $_POST['token'] : $token = ''; + if ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $token)) { $config['accounts']['confirm_email']['enabled'] ? $_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'); @@ -37,7 +38,8 @@ if ($setting->getValue('disable_invitations') && $setting->getValue('lock_regist $_SESSION['POPUP'][] = array('CONTENT' => 'Empty Captcha, please try again.', 'TYPE' => 'errormsg'); // Captcha disabled } else { - if ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $_POST['token'])) { + isset($_POST['token']) ? $token = $_POST['token'] : $token = ''; + if ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $token)) { $config['accounts']['confirm_email']['enabled'] ? $_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'); From 501f369b4e570d64776be3342edee783560b2e88 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 15:31:18 +0200 Subject: [PATCH 236/650] Further fixes to PHP warnings Addresses #330 and further cleans up the PHP log. --- public/include/classes/user.class.php | 6 +++--- public/include/pages/password/change.inc.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 6d7a6d27..9d448a40 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -552,7 +552,7 @@ class User { **/ public function resetPassword($token, $new1, $new2) { $this->debug->append("STA " . __METHOD__, 4); - if ($token = $this->token->getToken($token)) { + if ($aToken = $this->token->getToken($token)) { if ($new1 !== $new2) { $this->setErrorMessage( 'New passwords do not match' ); return false; @@ -563,8 +563,8 @@ class User { } $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, $token['account_id']) && $stmt->execute() && $stmt->affected_rows === 1) { - if ($this->token->deleteToken($token)) { + 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'); diff --git a/public/include/pages/password/change.inc.php b/public/include/pages/password/change.inc.php index b45b3ee4..919632bd 100644 --- a/public/include/pages/password/change.inc.php +++ b/public/include/pages/password/change.inc.php @@ -4,7 +4,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); -if ($_POST['do'] == 'resetPassword') { +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 { From 7b929ed3e4b97ec92a5f84c5f42eaae6697358c6 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 16:16:40 +0200 Subject: [PATCH 237/650] Fixing PHP Warning on resetting failed login count Addresses #330 and further cleans PHP Log --- public/include/pages/account/reset_failed.inc.php | 3 ++- public/templates/mmcFE/global/empty.tpl | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 public/templates/mmcFE/global/empty.tpl diff --git a/public/include/pages/account/reset_failed.inc.php b/public/include/pages/account/reset_failed.inc.php index bce9b418..39541dc2 100644 --- a/public/include/pages/account/reset_failed.inc.php +++ b/public/include/pages/account/reset_failed.inc.php @@ -8,5 +8,6 @@ if ($user->isAuthenticated()) { $user->setUserFailed($_SESSION['USERDATA']['id'], 0); header("Location: " . $_SERVER['HTTP_REFERER']); } - +// Somehow we still need to load this empty template +$smarty->assign("CONTENT", "../../global/empty.tpl"); ?> diff --git a/public/templates/mmcFE/global/empty.tpl b/public/templates/mmcFE/global/empty.tpl new file mode 100644 index 00000000..e69de29b From 41ec58ea168a4b8aa6eb2a939901df8c3032051a Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 15 Jul 2013 16:28:22 +0200 Subject: [PATCH 238/650] Adding inline docuemtation to invitation class Adding proper inline documentation to invitation class. --- public/include/classes/invitation.class.php | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/public/include/classes/invitation.class.php b/public/include/classes/invitation.class.php index ef640420..822dfef3 100644 --- a/public/include/classes/invitation.class.php +++ b/public/include/classes/invitation.class.php @@ -21,6 +21,11 @@ class Invitation extends Base { 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 = ?"); @@ -30,15 +35,34 @@ class Invitation extends Base { $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'); @@ -47,6 +71,14 @@ class Invitation extends Base { $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 ( ?, ?, ?)"); @@ -54,6 +86,13 @@ class Invitation extends Base { 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 @@ -95,6 +134,7 @@ class Invitation extends Base { } } +// Instantiate class $invitation = new invitation(); $invitation->setDebug($debug); $invitation->setMysql($mysqli); From b7ffbd0bfd798bfa872cd189ee16b133bb1bf447 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 16 Jul 2013 16:02:52 +0200 Subject: [PATCH 239/650] fixing issue for first created admin user --- public/include/classes/user.class.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 9d448a40..4f9f5cd4 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -497,11 +497,14 @@ class User { } if ($this->mysqli->query("SELECT id FROM $this->table LIMIT 1")->num_rows > 0) { $this->config['accounts']['confirm_email']['enabled'] ? $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, ?) @@ -515,7 +518,7 @@ class User { $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->config['accounts']['confirm_email']['enabled']) { + if ($this->config['accounts']['confirm_email']['enabled'] && $is_admin != 1) { if ($token = $this->token->createToken('confirm_email', $stmt->insert_id)) { $aData['username'] = $username_clean; $aData['token'] = $token; From 271c7760534ab9a0445517336cd3b76b3381772c Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 16 Jul 2013 21:16:53 +0200 Subject: [PATCH 240/650] Fixing issue with wrong require Fixes #469 --- cronjobs/pps_payout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cronjobs/pps_payout.php b/cronjobs/pps_payout.php index bb48e62e..5e6f673a 100755 --- a/cronjobs/pps_payout.php +++ b/cronjobs/pps_payout.php @@ -148,5 +148,5 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { } } -require_once('cron.inc.php'); +require_once('cron_end.inc.php'); ?> From 60b4bba4898d178b9d19fb5ffde80c80c333f69c Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 16 Jul 2013 21:49:12 +0200 Subject: [PATCH 241/650] Fixing wrong round share calculations on PPLNS This addresses #468, might be the actual fix already. --- cronjobs/pplns_payout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php index 6bb4234a..f7cdb6b2 100755 --- a/cronjobs/pplns_payout.php +++ b/cronjobs/pplns_payout.php @@ -65,7 +65,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { if ($iRoundShares >= $pplns_target) { $log->logDebug("Matching or exceeding PPLNS target of $pplns_target with $iRoundShares"); - $aAccountShares = $share->getSharesForAccounts($aBlock['share_id'] - $pplns_target + 1, $aBlock['share_id']); + $aAccountShares = $share->getSharesForAccounts($aBlock['share_id'] - $pplns_target, $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); From a12499ecd9782fc524931eb7e3b48f914efc053e Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 16 Jul 2013 21:55:52 +0200 Subject: [PATCH 242/650] Fixing negative time since last block This will fix #466 if approved by reporter. --- public/include/pages/statistics/pool.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/public/include/pages/statistics/pool.inc.php b/public/include/pages/statistics/pool.inc.php index f3669625..256d4edf 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -38,6 +38,7 @@ if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { $now = new DateTime( "now" ); if (!empty($aBlockData)) { $dTimeSinceLast = ($now->getTimestamp() - $aBlockData['time']); + if ($dTimeSinceLast < 0) $dTimeSinceLast = 0; } else { $dTimeSinceLast = 0; } From 39eb7f5714a4f48dfdd4a27fc61d6eaa47fff405 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 17 Jul 2013 09:18:18 +0200 Subject: [PATCH 243/650] Fixing option argument in run-crons for PHP binary Fixes #470 --- cronjobs/run-crons.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index 1f55bd77..fcc66f70 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -31,10 +31,12 @@ while getopts "hvp:" opt; do echo "Usage: $0 [-v] [-p PHP_BINARY]"; exit 0 ;; - v) VERBOSE=1 - ;; - p) PHP_BIN=$2; shift; - ;; + v) VERBOSE=1 ;; + p) PHP_BIN=$OPTARG ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + exit 1 + ;; esac done From d492b532b7d71df3c64bba0651ec719d86daf3c7 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 17 Jul 2013 09:43:02 +0200 Subject: [PATCH 244/650] Adding ability to disable the sites API functions Addresses #467 and will fix upon merge. --- public/include/autoloader.inc.php | 1 + public/include/classes/api.class.php | 23 ++++++++++++++ public/include/config/global.inc.dist.php | 31 ++++++++++--------- public/include/pages/api.inc.php | 6 ++-- .../include/pages/api/getblockcount.inc.php | 6 ++-- .../include/pages/api/getblocksfound.inc.php | 6 ++-- .../pages/api/getcurrentworkers.inc.php | 6 ++-- .../include/pages/api/getdifficulty.inc.php | 6 ++-- .../pages/api/getestimatedtime.inc.php | 6 ++-- .../include/pages/api/getpoolhashrate.inc.php | 6 ++-- .../pages/api/getpoolsharerate.inc.php | 6 ++-- .../include/pages/api/getpoolstatus.inc.php | 3 ++ .../pages/api/gettimesincelastblock.inc.php | 6 ++-- .../include/pages/api/getuserstatus.inc.php | 6 ++-- .../include/pages/api/getuserworkers.inc.php | 6 ++-- public/include/pages/api/public.inc.php | 6 ++-- 16 files changed, 91 insertions(+), 39 deletions(-) create mode 100644 public/include/classes/api.class.php diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index a5b21621..929e2f85 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -22,6 +22,7 @@ require_once(INCLUDE_DIR . '/database.inc.php'); require_once(INCLUDE_DIR . '/smarty.inc.php'); // Load classes that need the above as dependencies require_once(CLASS_DIR . '/base.class.php'); +require_once(CLASS_DIR . '/api.class.php'); require_once(CLASS_DIR . '/mail.class.php'); require_once(CLASS_DIR . '/tokentype.class.php'); require_once(CLASS_DIR . '/token.class.php'); diff --git a/public/include/classes/api.class.php b/public/include/classes/api.class.php new file mode 100644 index 00000000..36374162 --- /dev/null +++ b/public/include/classes/api.class.php @@ -0,0 +1,23 @@ +config['website']['api']['disabled']) { + return true; + } else { + if ($error == true) { + header('HTTP/1.1 501 Not implemented'); + die('501 Not implemented'); + } + } + } +} + +$api = new Api(); +$api->setConfig($config); diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index e66197fe..173a47b7 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -96,22 +96,24 @@ $config['ap_threshold']['max'] = 250; * Website specific configuration settings * * Explanation: - * title : Website title used in master template - * name : The pool name, displayed in the header and mails - * slogan : A special slogan, also displayed in the header below name - * email : `From` addresses used in notifications - * theme : Theme used for desktop browsers - * mobile : Enable/Disable mobile theme support - * mobile_theme : Theme used for mobile browsers + * title : Website title used in master template + * name : The pool name, displayed in the header and mails + * slogan : A special slogan, also displayed in the header below name + * email : `From` addresses used in notifications + * theme : Theme used for desktop browsers + * mobile : Enable/Disable mobile theme support + * mobile_theme : Theme used for mobile browsers + * api disabled : Disable the sites API functions * * Defaults: - * title = `The Pool - Mining Evolved` - * name = `The Pool` - * slogan = `Resistance is futile` - * email = `test@example.com` - * theme = `mmcFE` - * mobile = true - * mobile_theme = `mobile` + * title = `The Pool - Mining Evolved` + * name = `The Pool` + * slogan = `Resistance is futile` + * email = `test@example.com` + * theme = `mmcFE` + * mobile = true + * mobile_theme = `mobile` + * api disbabled = false **/ $config['website']['title'] = 'The Pool - Mining Evolved'; $config['website']['name'] = 'The Pool'; @@ -120,6 +122,7 @@ $config['website']['email'] = 'test@example.com'; $config['website']['theme'] = 'mmcFE'; $config['website']['mobile'] = true; $config['website']['mobile_theme'] = 'mobile'; +$config['website']['api']['disabled'] = false; /** * Account specific settings 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..6764f436 100644 --- a/public/include/pages/api/getblockcount.inc.php +++ b/public/include/pages/api/getblockcount.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getblocksfound.inc.php b/public/include/pages/api/getblocksfound.inc.php index 00883dad..b238bdc3 100644 --- a/public/include/pages/api/getblocksfound.inc.php +++ b/public/include/pages/api/getblocksfound.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getcurrentworkers.inc.php b/public/include/pages/api/getcurrentworkers.inc.php index 4e26cc1c..48864316 100644 --- a/public/include/pages/api/getcurrentworkers.inc.php +++ b/public/include/pages/api/getcurrentworkers.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getdifficulty.inc.php b/public/include/pages/api/getdifficulty.inc.php index 0e9cb6f9..e84d4045 100644 --- a/public/include/pages/api/getdifficulty.inc.php +++ b/public/include/pages/api/getdifficulty.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getestimatedtime.inc.php b/public/include/pages/api/getestimatedtime.inc.php index a48393fa..f06e28f3 100644 --- a/public/include/pages/api/getestimatedtime.inc.php +++ b/public/include/pages/api/getestimatedtime.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getpoolhashrate.inc.php b/public/include/pages/api/getpoolhashrate.inc.php index 6f6763ec..5546d321 100644 --- a/public/include/pages/api/getpoolhashrate.inc.php +++ b/public/include/pages/api/getpoolhashrate.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getpoolsharerate.inc.php b/public/include/pages/api/getpoolsharerate.inc.php index 8e9117f1..a87859f6 100644 --- a/public/include/pages/api/getpoolsharerate.inc.php +++ b/public/include/pages/api/getpoolsharerate.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getpoolstatus.inc.php b/public/include/pages/api/getpoolstatus.inc.php index ece9f557..4cf0d5ef 100644 --- a/public/include/pages/api/getpoolstatus.inc.php +++ b/public/include/pages/api/getpoolstatus.inc.php @@ -3,6 +3,9 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); +// Check if the API is activated +$api->isActive(); + // Check user token $user_id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/gettimesincelastblock.inc.php b/public/include/pages/api/gettimesincelastblock.inc.php index 532da6bd..14575364 100644 --- a/public/include/pages/api/gettimesincelastblock.inc.php +++ b/public/include/pages/api/gettimesincelastblock.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getuserstatus.inc.php b/public/include/pages/api/getuserstatus.inc.php index ac8f6654..afdd59fc 100644 --- a/public/include/pages/api/getuserstatus.inc.php +++ b/public/include/pages/api/getuserstatus.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $user_id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getuserworkers.inc.php b/public/include/pages/api/getuserworkers.inc.php index 9aaca562..06eb3411 100644 --- a/public/include/pages/api/getuserworkers.inc.php +++ b/public/include/pages/api/getuserworkers.inc.php @@ -1,8 +1,10 @@ isActive(); // Check user token $user_id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/public.inc.php b/public/include/pages/api/public.inc.php index 162e9134..5a98367a 100644 --- a/public/include/pages/api/public.inc.php +++ b/public/include/pages/api/public.inc.php @@ -1,10 +1,10 @@ isActive(); // Fetch last block information $aLastBlock = $block->getLast(); From 8393053641b4a557748d33d2e1bebd08cf7c887a Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 09:04:59 +0200 Subject: [PATCH 245/650] Disabel API references if API disabled This will turn off displaying any API references and links when API was turned off in the configuration. Fixes #467 --- public/include/smarty_globals.inc.php | 2 +- public/templates/mmcFE/account/edit/default.tpl | 2 +- public/templates/mmcFE/global/navigation.tpl | 2 +- public/templates/mmcFE/statistics/default.tpl | 2 +- public/templates/mmcFE/statistics/pool/authenticated.tpl | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 5f6ca959..fe1868d6 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -47,7 +47,7 @@ $aGlobal = array( 'blockexplorer' => $config['blockexplorer'], 'chaininfo' => $config['chaininfo'], 'config' => array( - 'website' => array( 'title' => $config['website']['title'], 'acl' => $config['website']['acl'] ), + 'website' => $config['website'], 'accounts' => $config['accounts'], 'disable_invitations' => $setting->getValue('disable_invitations'), 'price' => array( 'currency' => $config['price']['currency'] ), diff --git a/public/templates/mmcFE/account/edit/default.tpl b/public/templates/mmcFE/account/edit/default.tpl index fcf29c7c..be3ed355 100644 --- a/public/templates/mmcFE/account/edit/default.tpl +++ b/public/templates/mmcFE/account/edit/default.tpl @@ -6,7 +6,7 @@ - + {if !$GLOBAL.config.website.api.disabled}{/if} diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 79d36af8..14a002ef 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -47,7 +47,7 @@
  • About
  • diff --git a/public/templates/mmcFE/statistics/default.tpl b/public/templates/mmcFE/statistics/default.tpl index a8e9a4c2..59603d94 100644 --- a/public/templates/mmcFE/statistics/default.tpl +++ b/public/templates/mmcFE/statistics/default.tpl @@ -19,5 +19,5 @@
    Username: {$GLOBAL.userdata.username|escape}
    User Id: {$GLOBAL.userdata.id}
    API Key: {$GLOBAL.userdata.api_key}
    API Key: {$GLOBAL.userdata.api_key}
    E-Mail:
    Payment Address:
    Donation %: [donation amount in percent (example: 0.5)]
    -
  • These stats are also available in JSON format HERE
  • +{if !$GLOBAL.config.website.api.disabled}
  • These stats are also available in JSON format HERE
  • {/if} {include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/statistics/pool/authenticated.tpl b/public/templates/mmcFE/statistics/pool/authenticated.tpl index 9e41b6d3..8d35df1a 100644 --- a/public/templates/mmcFE/statistics/pool/authenticated.tpl +++ b/public/templates/mmcFE/statistics/pool/authenticated.tpl @@ -56,7 +56,7 @@ -
  • These stats are also available in JSON format HERE
  • +{if !$GLOBAL.config.website.api.disabled}
  • These stats are also available in JSON format HERE
  • {/if} {include file="global/block_footer.tpl"} From 72ed08c92cb7a7fd2b122409ebba0bb13bff7a0c Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 09:30:44 +0200 Subject: [PATCH 246/650] Adding tooltip help for anonymous account flag Fixes #459 --- public/templates/mmcFE/account/edit/default.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mmcFE/account/edit/default.tpl b/public/templates/mmcFE/account/edit/default.tpl index fcf29c7c..98452cb6 100644 --- a/public/templates/mmcFE/account/edit/default.tpl +++ b/public/templates/mmcFE/account/edit/default.tpl @@ -11,7 +11,7 @@ Payment Address: Donation %: [donation amount in percent (example: 0.5)] Automatic Payout Threshold: [{$GLOBAL.config.ap_threshold.min}-{$GLOBAL.config.ap_threshold.max} {$GLOBAL.config.currency}. Set to '0' for no auto payout] - Anonymous Account: + Anonymous Account : From 73ec70738186cf7b1e0ef62972a408b8785ceb72 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 09:43:16 +0200 Subject: [PATCH 247/650] Adds blockhash to blockexplorer URL * Changed blockexplorer URL to use Blockhash instead of height * Added calls to find current networks blocks blockhash * Propagated changes onto temmplate * Added new dist configuration for new blockexplorer URL Fixes #446 --- public/include/config/global.inc.dist.php | 7 ++++--- public/include/pages/statistics/pool.inc.php | 10 +++++++++- .../templates/mmcFE/statistics/pool/authenticated.tpl | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index e66197fe..22fe69cd 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -243,9 +243,10 @@ $config['payout_system'] = 'prop'; $config['archive']['maxrounds'] = 10; $config['archive']['maxage'] = 60 * 24; -// URL prefix for block searches, used for block links, default: `http://explorer.litecoin.net/search?q=` -// If empty, the block link to the block information page will be removed -$config['blockexplorer'] = 'http://explorer.litecoin.net/search?q='; +// URL prefix for block searches, used for block links, default: `http://explorer.litecoin.net/block/` +// The Blockhash is appended on the templates to this URL +// If this config is empty, the block link to the block information page will be removed +$config['blockexplorer'] = 'http://explorer.litecoin.net/block/'; // Link to blockchain information, used for difficulty link, default: `http://allchains.info` // If empty, the difficulty link to the chain information will be removed diff --git a/public/include/pages/statistics/pool.inc.php b/public/include/pages/statistics/pool.inc.php index 256d4edf..9aafadcf 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -11,6 +11,8 @@ if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { if (is_array($dDifficulty) && array_key_exists('proof-of-work', $dDifficulty)) $dDifficulty = $dDifficulty['proof-of-work']; $iBlock = $bitcoin->getblockcount(); + $sBlockHash = $bitcoin->query('getblockhash', $iBlock); + var_dump($sBlockHash); } else { $dDifficulty = 1; $iBlock = 0; @@ -51,7 +53,13 @@ if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { $smarty->assign("CONTRIBSHARES", $aContributorsShares); $smarty->assign("CONTRIBHASHES", $aContributorsHashes); $smarty->assign("CURRENTBLOCK", $iBlock); - count($aBlockData) > 0 ? $smarty->assign("LASTBLOCK", $aBlockData['height']) : $smarty->assign("LASTBLOCK", 0); + $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 { diff --git a/public/templates/mmcFE/statistics/pool/authenticated.tpl b/public/templates/mmcFE/statistics/pool/authenticated.tpl index 9e41b6d3..7834b9b5 100644 --- a/public/templates/mmcFE/statistics/pool/authenticated.tpl +++ b/public/templates/mmcFE/statistics/pool/authenticated.tpl @@ -22,7 +22,7 @@ {if $GLOBAL.blockexplorer} Next Network Block - {$CURRENTBLOCK + 1}    (Current: {$CURRENTBLOCK}) + {$CURRENTBLOCK + 1}    (Current: {$CURRENTBLOCK}) {else} @@ -32,7 +32,7 @@ {/if} Last Block Found - {if $GLOBAL.blockexplorer}{$LASTBLOCK|default:"0"}{else}{$LASTBLOCK|default:"0"}{/if} + {if $GLOBAL.blockexplorer}{$LASTBLOCK|default:"0"}{else}{$LASTBLOCK|default:"0"}{/if} Current Difficulty From 6d379e8e58bccc199c78042a1c21aa1ad653c231 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 09:45:20 +0200 Subject: [PATCH 248/650] Removing debug output --- public/include/pages/statistics/pool.inc.php | 1 - 1 file changed, 1 deletion(-) diff --git a/public/include/pages/statistics/pool.inc.php b/public/include/pages/statistics/pool.inc.php index 9aafadcf..b8872293 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -12,7 +12,6 @@ if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { $dDifficulty = $dDifficulty['proof-of-work']; $iBlock = $bitcoin->getblockcount(); $sBlockHash = $bitcoin->query('getblockhash', $iBlock); - var_dump($sBlockHash); } else { $dDifficulty = 1; $iBlock = 0; From b57f6b59a1f69bf4338d82a395b438d342b08070 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 10:49:16 +0200 Subject: [PATCH 249/650] Disable API on mobile theme if disabled --- public/templates/mobile/statistics/default.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/mobile/statistics/default.tpl b/public/templates/mobile/statistics/default.tpl index 994e60a3..7b03653d 100644 --- a/public/templates/mobile/statistics/default.tpl +++ b/public/templates/mobile/statistics/default.tpl @@ -18,4 +18,4 @@ -
  • These stats are also available in JSON format HERE
  • +{if !$GLOBAL.config.website.api.disabled}
  • These stats are also available in JSON format HERE
  • {/if} From a655abddbf378761cdcbd97b9c4feb21efe85874 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 11:05:09 +0200 Subject: [PATCH 250/650] fixing mobile template --- public/templates/mobile/statistics/pool/authenticated.tpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/templates/mobile/statistics/pool/authenticated.tpl b/public/templates/mobile/statistics/pool/authenticated.tpl index 0889347c..a7025779 100644 --- a/public/templates/mobile/statistics/pool/authenticated.tpl +++ b/public/templates/mobile/statistics/pool/authenticated.tpl @@ -27,7 +27,7 @@ {if $GLOBAL.blockexplorer} Next Network Block - {$CURRENTBLOCK + 1}    (Current: {$CURRENTBLOCK}) + {$CURRENTBLOCK + 1}    (Current: {$CURRENTBLOCK}) {else} @@ -37,7 +37,7 @@ {/if} Last Block Found - {if $GLOBAL.blockexplorer}{$LASTBLOCK|default:"0"}{else}{$LASTBLOCK|default:"0"}{/if} + {if $GLOBAL.blockexplorer}{$LASTBLOCK|default:"0"}{else}{$LASTBLOCK|default:"0"}{/if} {if $GLOBAL.chaininfo} From 7cf3ff3d883b56cca22c59079588eba2a61b576f Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 13:55:05 +0200 Subject: [PATCH 251/650] Fix anonymous detection in share contributors list Fixes #484 --- .../mmcFE/statistics/pool/contributors_shares.tpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl index 3efb4a8e..6dd36eb8 100644 --- a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl +++ b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl @@ -11,11 +11,11 @@ {assign var=rank value=1} {assign var=listed value=0} -{section hashrate $CONTRIBSHARES} - +{section shares $CONTRIBSHARES} + {$rank++} - {if $CONTRIBHASHES[hashrate].is_anonymous|default:"0" == 1}anonymous{else}{$CONTRIBSHARES[hashrate].account|escape}{/if} - {$CONTRIBSHARES[hashrate].shares|number_format} + {if $CONTRIBSHARES[shares].is_anonymous|default:"0" == 1}anonymous{else}{$CONTRIBSHARES[shares].account|escape}{/if} + {$CONTRIBSHARES[shares].shares|number_format} {/section} {if $listed != 1 && $GLOBAL.userdata.username|default:""} From 3d20d041d7d3aefe75f4a0d75e1704889b2b393b Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 13:58:36 +0200 Subject: [PATCH 252/650] Update POOLS.md Added new pool --- POOLS.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/POOLS.md b/POOLS.md index 88b986b4..9268a9d4 100644 --- a/POOLS.md +++ b/POOLS.md @@ -27,7 +27,7 @@ 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 | | @@ -48,7 +48,7 @@ running more or less without any issues (related to `mmcfe-ng` that is ;-)). He the most powerful pool! | Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | -| -------- | ---- | ------------- | ----------------- | ----- | +| -------- | ---- | ------------- | ------------------: | ----- | | http://www.ejpool.info | Litecoin | 155 MHash | 120 | | ### Obigal @@ -65,6 +65,12 @@ Small Time Miners are running various stratum only pools for different coins. ### 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 | From 6641cf7f5c137d315688786a563f01f64d04a6ce Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 15:41:52 +0200 Subject: [PATCH 253/650] Adding notifications and start/end times Fixes #486 --- cronjobs/cron_end.inc.php | 2 +- cronjobs/notifications.php | 2 ++ cronjobs/shared.inc.php | 1 + public/include/pages/admin/monitoring.inc.php | 35 ++++++++++++++----- .../mmcFE/admin/monitoring/default.tpl | 3 +- 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/cronjobs/cron_end.inc.php b/cronjobs/cron_end.inc.php index 56607c45..03a548d2 100644 --- a/cronjobs/cron_end.inc.php +++ b/cronjobs/cron_end.inc.php @@ -22,7 +22,7 @@ limitations under the License. $monitoring->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 . "_lastrun", "date", time()); +$monitoring->setStatus($cron_name . "_endtime", "date", time()); // Mark cron as running for monitoring $monitoring->setStatus($cron_name . '_active', "yesno", 0); ?> diff --git a/cronjobs/notifications.php b/cronjobs/notifications.php index a60e4657..effcf002 100755 --- a/cronjobs/notifications.php +++ b/cronjobs/notifications.php @@ -65,4 +65,6 @@ if (!empty($aNotifications)) { } else { $log->logDebug(" no active IDLE worker notifications\n"); } + +require_once('cron_end.inc.php'); ?> diff --git a/cronjobs/shared.inc.php b/cronjobs/shared.inc.php index 33113c00..f66be4f4 100644 --- a/cronjobs/shared.inc.php +++ b/cronjobs/shared.inc.php @@ -48,4 +48,5 @@ $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/public/include/pages/admin/monitoring.inc.php b/public/include/pages/admin/monitoring.inc.php index 0d7dd671..a82dd78a 100644 --- a/public/include/pages/admin/monitoring.inc.php +++ b/public/include/pages/admin/monitoring.inc.php @@ -15,42 +15,56 @@ $aCronStatus = array( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('statistics_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('statistics_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('statistics_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('statistics_lastrun') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('statistics_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('statistics_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('statistics_message') ), ), 'auto_payout' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('auto_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('auto_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('auto_payout_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('auto_payout_lastrun') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('auto_payout_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('auto_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('auto_payout_message') ), ), 'archive_cleanup' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('archive_cleanup_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('archive_cleanup_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('archive_cleanup_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('archive_cleanup_lastrun') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('archive_cleanup_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('archive_cleanup_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('archive_cleanup_message') ), ), 'blockupdate' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('blockupdate_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('blockupdate_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('blockupdate_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('blockupdate_lastrun') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('blockupdate_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('blockupdate_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('blockupdate_message') ), ), 'findblock' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('findblock_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('findblock_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('findblock_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('findblock_lastrun') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('findblock_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('findblock_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('findblock_message') ), ), + 'notifications' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('notifications_status') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('notifications_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('notifications_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('notifications_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('notifications_endtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('notifications_message') ), + ), 'tickerupdate' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('tickerupdate_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('tickerupdate_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('tickerupdate_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('tickerupdate_lastrun') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('tickerupdate_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('tickerupdate_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('tickerupdate_message') ), ) ); @@ -61,7 +75,8 @@ case 'pplns': array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pplns_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pplns_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pplns_payout_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pplns_payout_lastrun') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pplns_payout_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pplns_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pplns_payout_message') ), ); break; @@ -70,7 +85,8 @@ case 'pps': array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pps_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pps_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pps_payout_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pps_payout_lastrun') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pps_payout_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pps_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pps_payout_message') ), ); break; @@ -79,7 +95,8 @@ case 'prop': array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('proportional_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('proportional_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('proportional_payout_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('proportional_payout_lastrun') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('proportional_payout_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('proportional_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('proportional_payout_message') ), ); break; diff --git a/public/templates/mmcFE/admin/monitoring/default.tpl b/public/templates/mmcFE/admin/monitoring/default.tpl index 856766ac..f7b099b1 100644 --- a/public/templates/mmcFE/admin/monitoring/default.tpl +++ b/public/templates/mmcFE/admin/monitoring/default.tpl @@ -5,7 +5,8 @@ Exit Code Active Runtime - Last Run + Start Time + End Time Message From 43923fe4e94acd7127774b9234658214639c636d Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 19 Jul 2013 09:49:48 +0200 Subject: [PATCH 254/650] only fetch blockhash for valid blocks --- public/include/pages/statistics/pool.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/pages/statistics/pool.inc.php b/public/include/pages/statistics/pool.inc.php index b8872293..2c1ca533 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -11,7 +11,7 @@ if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { if (is_array($dDifficulty) && array_key_exists('proof-of-work', $dDifficulty)) $dDifficulty = $dDifficulty['proof-of-work']; $iBlock = $bitcoin->getblockcount(); - $sBlockHash = $bitcoin->query('getblockhash', $iBlock); + is_int($iBlock) && $iBlock > 0 ? $sBlockHash = $bitcoin->query('getblockhash', $iBlock) : $sBlockHash = ''; } else { $dDifficulty = 1; $iBlock = 0; From 7fce187ab091fb57ce84a952e913919372ddc2e1 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 19 Jul 2013 11:12:28 +0200 Subject: [PATCH 255/650] Admin Panel Setting to disable/enable AP/MP This will allow admins to temporarily or permanantly enable and disable auto and manual payout processing. Fixes #488 --- cronjobs/auto_payout.php | 10 +++++++++- public/include/pages/account/edit.inc.php | 2 ++ public/include/pages/admin/settings.inc.php | 2 ++ public/include/smarty_globals.inc.php | 1 + .../templates/mmcFE/account/edit/default.tpl | 2 ++ .../mmcFE/admin/settings/default.tpl | 20 +++++++++++++++++++ 6 files changed, 36 insertions(+), 1 deletion(-) diff --git a/cronjobs/auto_payout.php b/cronjobs/auto_payout.php index 548e5de8..bf87f41c 100755 --- a/cronjobs/auto_payout.php +++ b/cronjobs/auto_payout.php @@ -22,6 +22,14 @@ limitations under the License. // Include all settings and classes require_once('shared.inc.php'); +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); + exit(0); +} + if ($bitcoin->can_connect() !== true) { $log->logFatal(" unable to connect to RPC server, exiting"); $monitoring->setStatus($cron_name . "_active", "yesno", 0); @@ -34,7 +42,7 @@ if ($bitcoin->can_connect() !== true) { $users = $user->getAllAutoPayout(); // Quick summary -$log->logInfo(" found " . count($users) . " queued payout(s)"); +if (count($users) > 0) $log->logInfo(" found " . count($users) . " queued payout(s)"); // Go through users and run transactions if (! empty($users)) { diff --git a/public/include/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index 9ab38e49..503542fc 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -12,6 +12,8 @@ if ($user->isAuthenticated()) { case 'cashOut': if ($setting->getValue('manual_payout_active') == 1) { $_SESSION['POPUP'][] = array('CONTENT' => 'A manual payout is in progress. Please try again later.', 'TYPE' => 'errormsg'); + } else 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; diff --git a/public/include/pages/admin/settings.inc.php b/public/include/pages/admin/settings.inc.php index 9bc7ef3b..5999b00d 100644 --- a/public/include/pages/admin/settings.inc.php +++ b/public/include/pages/admin/settings.inc.php @@ -20,6 +20,8 @@ if (@$_REQUEST['do'] == 'save' && !empty($_REQUEST['data'])) { $smarty->assign("MAINTENANCE", $setting->getValue('maintenance')); $smarty->assign("LOCKREGISTRATION", $setting->getValue('lock_registration')); $smarty->assign("DISABLEINVITATIONS", $setting->getValue('disable_invitations')); +$smarty->assign("DISABLEAP", $setting->getValue('disable_ap')); +$smarty->assign("DISABLEMP", $setting->getValue('disable_mp')); // Tempalte specifics $smarty->assign("CONTENT", "default.tpl"); diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index fe1868d6..5f47bd65 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -46,6 +46,7 @@ $aGlobal = array( 'price' => $setting->getValue('price'), 'blockexplorer' => $config['blockexplorer'], 'chaininfo' => $config['chaininfo'], + 'disable_mp' => $setting->getValue('disable_mp'), 'config' => array( 'website' => $config['website'], 'accounts' => $config['accounts'], diff --git a/public/templates/mmcFE/account/edit/default.tpl b/public/templates/mmcFE/account/edit/default.tpl index c3b28f42..71221f12 100644 --- a/public/templates/mmcFE/account/edit/default.tpl +++ b/public/templates/mmcFE/account/edit/default.tpl @@ -20,6 +20,7 @@ {include file="global/block_footer.tpl"} +{if !$GLOBAL.disable_mp} {include file="global/block_header.tpl" BLOCK_HEADER="Cash Out"}
    • Please note: a {$GLOBAL.config.txfee} {$GLOBAL.config.currency} transaction will apply when processing "On-Demand" manual payments
    @@ -33,6 +34,7 @@
    {include file="global/block_footer.tpl"} +{/if} {include file="global/block_header.tpl" BLOCK_HEADER="Change Password"}
    • Note: You will be redirected to login on successful completion of a password change
    diff --git a/public/templates/mmcFE/admin/settings/default.tpl b/public/templates/mmcFE/admin/settings/default.tpl index 154c39d1..117dd8cd 100644 --- a/public/templates/mmcFE/admin/settings/default.tpl +++ b/public/templates/mmcFE/admin/settings/default.tpl @@ -40,6 +40,26 @@ + + Disable Auto Payout + + + + + + + Disable Manual Payout + + + + + From 440ca027a2fba59347929c5ccd11af3cb3840d46 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 21 Jul 2013 08:12:40 +0200 Subject: [PATCH 256/650] Fixing PPLNS target calculation on blockavg This will fix #492 with PPLNS targets not taking the blocks in proper order. --- public/include/classes/block.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index 711ed0b5..c08f89d6 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -109,7 +109,7 @@ class Block { * @return data float Float value of average shares **/ public function getAvgBlockShares($limit=10) { - $stmt = $this->mysqli->prepare("SELECT AVG(shares) AS average FROM $this->table LIMIT ?"); + $stmt = $this->mysqli->prepare("SELECT AVG(shares) AS average FROM $this->table ORDER BY height DESC LIMIT ?"); if ($this->checkStmt($stmt) && $stmt->bind_param('i', $limit) && $stmt->execute() && $result = $stmt->get_result()) return (float)$result->fetch_object()->average; return false; From 7d801a561c8daf5cb706250c3c4cdc8a0ce6cb26 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 21 Jul 2013 08:20:53 +0200 Subject: [PATCH 257/650] Fixing Orphan showing as unconfirmed * Fixes orphaned transactions showing as unconfirmed too * Fixes transaction tables to show orphaned credits in green, not red Fixes #490 --- public/include/classes/transaction.class.php | 4 ++-- public/templates/mmcFE/account/transactions/default.tpl | 2 +- public/templates/mmcFE/admin/transactions/default.tpl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 30867cc6..229c8581 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -235,7 +235,7 @@ class Transaction { 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 < ? + t.type IN ('Credit','Bonus') AND b.confirmations < ? AND b.confirmations > 0 AND t.account_id = ? ) AS t4, ( @@ -244,7 +244,7 @@ class Transaction { LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id WHERE ( - t.type IN ('Donation','Fee') AND b.confirmations < ? + t.type IN ('Donation','Fee') AND b.confirmations < ? AND b.confirmations > 0 ) AND t.account_id = ? ) AS t5, diff --git a/public/templates/mmcFE/account/transactions/default.tpl b/public/templates/mmcFE/account/transactions/default.tpl index 4bbe7b4b..52602088 100644 --- a/public/templates/mmcFE/account/transactions/default.tpl +++ b/public/templates/mmcFE/account/transactions/default.tpl @@ -116,7 +116,7 @@ {$TRANSACTIONS[transaction].type} {$TRANSACTIONS[transaction].coin_address} {if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if} - {$TRANSACTIONS[transaction].amount|number_format:"8"} + {$TRANSACTIONS[transaction].amount|number_format:"8"} {if $TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus'} {assign var="orphan_credits" value="`$orphan_credits|default:"0"+$TRANSACTIONS[transaction].amount`"} diff --git a/public/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl index aba15f12..ecf0dc70 100644 --- a/public/templates/mmcFE/admin/transactions/default.tpl +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -117,7 +117,7 @@ {$TRANSACTIONS[transaction].type} {$TRANSACTIONS[transaction].coin_address} {if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if} - {$TRANSACTIONS[transaction].amount|number_format:"8"} + {$TRANSACTIONS[transaction].amount|number_format:"8"} {/if} {/section} From c94c1be7be64d4d2416741361f9d95810f63475a Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 21 Jul 2013 08:35:57 +0200 Subject: [PATCH 258/650] Using proper SQL query by @CaptainAK Proposed fix did not work, using proper Query now. Thank @CaptainAK for the fix! Fixes #492 --- public/include/classes/block.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index c08f89d6..8ce985d1 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -108,8 +108,8 @@ class Block { * @param limit int Maximum blocks to check * @return data float Float value of average shares **/ - public function getAvgBlockShares($limit=10) { - $stmt = $this->mysqli->prepare("SELECT AVG(shares) AS average FROM $this->table ORDER BY height DESC LIMIT ?"); + public function getAvgBlockShares($limit=1) { + $stmt = $this->mysqli->prepare("SELECT AVG(x.shares) AS average FROM (SELECT shares 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; From 36a74b0bbf206b7b0f5c9bfed48192ecf3934a79 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 21 Jul 2013 17:25:09 +0200 Subject: [PATCH 259/650] Fix transaction table, adjust transaction class * Ensure we also check newly added blocks for unconfirmed tx * Only list orphaned transactions in the orphaned tab Fixes #490 --- public/include/classes/transaction.class.php | 4 ++-- public/templates/mmcFE/account/transactions/default.tpl | 2 +- public/templates/mmcFE/admin/transactions/default.tpl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 229c8581..2cbe4312 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -235,7 +235,7 @@ class Transaction { 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 b.confirmations > 0 + t.type IN ('Credit','Bonus') AND b.confirmations < ? AND b.confirmations >= 0 AND t.account_id = ? ) AS t4, ( @@ -244,7 +244,7 @@ class Transaction { LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id WHERE ( - t.type IN ('Donation','Fee') AND b.confirmations < ? AND b.confirmations > 0 + t.type IN ('Donation','Fee') AND b.confirmations < ? AND b.confirmations >= 0 ) AND t.account_id = ? ) AS t5, diff --git a/public/templates/mmcFE/account/transactions/default.tpl b/public/templates/mmcFE/account/transactions/default.tpl index 52602088..a7337bd6 100644 --- a/public/templates/mmcFE/account/transactions/default.tpl +++ b/public/templates/mmcFE/account/transactions/default.tpl @@ -62,7 +62,7 @@ {assign var=has_unconfirmed value=false} {section transaction $TRANSACTIONS} {if - (($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) + (($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations and $TRANSACTIONS[transaction].confirmations >= 0) } {assign var=has_unconfirmed value=true} diff --git a/public/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl index ecf0dc70..c493de8b 100644 --- a/public/templates/mmcFE/admin/transactions/default.tpl +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -68,7 +68,7 @@ {assign var=unconfirmed value=0} {section transaction $TRANSACTIONS} - {if ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations} + {if ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations and $TRANSACTIONS[transaction].confirmations >= 0} {assign var=unconfirmed value=1} {$TRANSACTIONS[transaction].id} From bb313bee3605b321e77a2153d15c265e293cbe07 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Sun, 21 Jul 2013 17:31:32 +0200 Subject: [PATCH 260/650] Update README.md Adding smarty template reference --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a7787bb2..95a26d3f 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Features The following feature have been implemented so far: +* Fully re-written GUI with [Smarty][2] templates * Mobile WebUI * Reward Systems * Propotional @@ -142,3 +143,4 @@ limitations under the License. [1]: https://github.com/TheSerapher/php-mmcfe-ng/issues "Issue" + [2]: http://www.smarty.net/docs/en/ "Smarty" From ee2c90525cda600710304a7559d1795e334e13e9 Mon Sep 17 00:00:00 2001 From: obigal Date: Sun, 21 Jul 2013 14:14:12 -0400 Subject: [PATCH 261/650] Cryptsy api support --- public/include/classes/tools.class.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/include/classes/tools.class.php b/public/include/classes/tools.class.php index b160711d..dcfa1959 100644 --- a/public/include/classes/tools.class.php +++ b/public/include/classes/tools.class.php @@ -48,6 +48,8 @@ class Tools extends Base { 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; @@ -73,6 +75,9 @@ class Tools extends Base { 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']); From 93e36a82593002329cb12eceea20bb6a50c3efcc Mon Sep 17 00:00:00 2001 From: obigal Date: Sun, 21 Jul 2013 14:15:34 -0400 Subject: [PATCH 262/650] Cryptsy api support --- public/include/config/global.inc.dist.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 0634101d..359f74da 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -73,6 +73,11 @@ $config['wallet']['password'] = 'testnet'; * url = `http://www.coinchoose.com` * target = `/api.php` * currency = `BTC` + * + * Optional (cryptsy.com): + * url = `https://www.cryptsy.com` + * target = `/api.php?method=marketdata` + * currency = `BTC` **/ $config['price']['url'] = 'https://btc-e.com'; $config['price']['target'] = '/api/2/ltc_usd/ticker'; From f51cc36b09f8630804f8e4e3ae50ce98f36f5dca Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 22 Jul 2013 14:14:51 +0200 Subject: [PATCH 263/650] set end time of cron even when disabled --- cronjobs/auto_payout.php | 1 + 1 file changed, 1 insertion(+) diff --git a/cronjobs/auto_payout.php b/cronjobs/auto_payout.php index bf87f41c..043782e0 100755 --- a/cronjobs/auto_payout.php +++ b/cronjobs/auto_payout.php @@ -27,6 +27,7 @@ if ($setting->getValue('disable_ap') == 1) { $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); } From ee5e2c46c6b2876a48c544e77396a04ed045e9c0 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Fri, 19 Jul 2013 12:35:52 +0200 Subject: [PATCH 264/650] Adding manual payout cron This will avoid double payouts via the website. Payouts will be requested by users and processed by a cron. If, for whatever reason, users do add two requests (it is checked if a payout exists) they would only have one successful payout until their account balance is back up to a save value to trigger the payout. This should fix any issues with manual payouts being exploited through the website. Will require some testing by others to ensure things work as expected. --- cronjobs/manual_payout.php | 82 +++++++++++++++++++ cronjobs/run-crons.sh | 2 +- public/include/autoloader.inc.php | 1 + public/include/classes/notification.class.php | 2 +- public/include/classes/payout.class.php | 65 +++++++++++++++ public/include/pages/account/edit.inc.php | 47 ++--------- public/include/pages/admin/monitoring.inc.php | 8 ++ 7 files changed, 165 insertions(+), 42 deletions(-) create mode 100755 cronjobs/manual_payout.php create mode 100644 public/include/classes/payout.class.php diff --git a/cronjobs/manual_payout.php b/cronjobs/manual_payout.php new file mode 100755 index 00000000..db4cb2be --- /dev/null +++ b/cronjobs/manual_payout.php @@ -0,0 +1,82 @@ +#!/usr/bin/php +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); +} + +// var_dump($oPayout->createPayout(1.12, 1)); +$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 { + $bitcoin->validateaddress($aData['coin_address']); + } catch (BitcoinClientException $e) { + $log->logError('Failed to verify this users coin address, skipping payout'); + continue; + } + try { + $bitcoin->sendtoaddress($aData['coin_address'], $dBalance); + } 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'])) { + // 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/run-crons.sh b/cronjobs/run-crons.sh index fcc66f70..2781b8d0 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -13,7 +13,7 @@ PHP_BIN=$( which php ) PIDFILE='/tmp/mmcfe-ng-cron.pid' # List of cruns to execute -CRONS="findblock.php proportional_payout.php pplns_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php archive_cleanup.php" +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" diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index 929e2f85..8f8ea6dc 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -26,6 +26,7 @@ require_once(CLASS_DIR . '/api.class.php'); 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'); require_once(CLASS_DIR . '/setting.class.php'); require_once(CLASS_DIR . '/monitoring.class.php'); diff --git a/public/include/classes/notification.class.php b/public/include/classes/notification.class.php index 910f5674..d128087a 100644 --- a/public/include/classes/notification.class.php +++ b/public/include/classes/notification.class.php @@ -178,12 +178,12 @@ class Notification extends Mail { $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)) { - $this->setErrorMessage('Error sending mail notification'); return true; } } else { $this->setErrorMessage('User disabled ' . $strType . ' notifications'); } + $this->setErrorMessage('Error sending mail notification'); return false; } } diff --git a/public/include/classes/payout.class.php b/public/include/classes/payout.class.php new file mode 100644 index 00000000..832679b1 --- /dev/null +++ b/public/include/classes/payout.class.php @@ -0,0 +1,65 @@ +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/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index 503542fc..23d02794 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -10,55 +10,22 @@ 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'); - } else if ($setting->getValue('disable_mp') == 1) { + 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 { - $auto_payout = $monitoring->getStatus('auto_payout_active'); - if ($auto_payout['value'] == 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 wallet 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'] . ' ' . $config['currency'] . ' to cover transaction fees', 'TYPE' => 'errormsg'); } - $setting->setValue('manual_payout_active', 0); } break; diff --git a/public/include/pages/admin/monitoring.inc.php b/public/include/pages/admin/monitoring.inc.php index a82dd78a..6848a9da 100644 --- a/public/include/pages/admin/monitoring.inc.php +++ b/public/include/pages/admin/monitoring.inc.php @@ -27,6 +27,14 @@ $aCronStatus = array( array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('auto_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('auto_payout_message') ), ), + 'manual_payout' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('manual_payout_status') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('manual_payout_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('manual_payout_runtime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('manual_payout_starttime') ), + array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('manual_payout_endtime') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('manual_payout_message') ), + ), 'archive_cleanup' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('archive_cleanup_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('archive_cleanup_active') ), From 4b6582ac35a87ccf1ce230651aa0085e67dc17cf Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 22 Jul 2013 16:42:23 +0200 Subject: [PATCH 265/650] More colorful monitoring output * Added colorized start/end times * Added Success/Error image for Active status * Added color to runtime status --- public/include/pages/admin/monitoring.inc.php | 44 +++++++++---------- .../mmcFE/admin/monitoring/default.tpl | 26 ++++++++--- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/public/include/pages/admin/monitoring.inc.php b/public/include/pages/admin/monitoring.inc.php index 6848a9da..627223d8 100644 --- a/public/include/pages/admin/monitoring.inc.php +++ b/public/include/pages/admin/monitoring.inc.php @@ -15,64 +15,64 @@ $aCronStatus = array( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('statistics_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('statistics_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('statistics_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('statistics_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('statistics_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('statistics_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('statistics_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('statistics_message') ), ), 'auto_payout' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('auto_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('auto_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('auto_payout_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('auto_payout_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('auto_payout_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('auto_payout_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('auto_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('auto_payout_message') ), ), 'manual_payout' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('manual_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('manual_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('manual_payout_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('manual_payout_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('manual_payout_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('manual_payout_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('manual_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('manual_payout_message') ), ), 'archive_cleanup' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('archive_cleanup_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('archive_cleanup_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('archive_cleanup_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('archive_cleanup_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('archive_cleanup_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('archive_cleanup_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('archive_cleanup_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('archive_cleanup_message') ), ), 'blockupdate' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('blockupdate_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('blockupdate_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('blockupdate_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('blockupdate_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('blockupdate_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('blockupdate_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('blockupdate_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('blockupdate_message') ), ), 'findblock' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('findblock_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('findblock_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('findblock_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('findblock_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('findblock_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('findblock_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('findblock_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('findblock_message') ), ), 'notifications' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('notifications_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('notifications_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('notifications_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('notifications_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('notifications_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('notifications_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('notifications_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('notifications_message') ), ), 'tickerupdate' => array ( array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('tickerupdate_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('tickerupdate_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('tickerupdate_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('tickerupdate_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('tickerupdate_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('tickerupdate_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('tickerupdate_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('tickerupdate_message') ), ) ); @@ -83,8 +83,8 @@ case 'pplns': array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pplns_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pplns_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pplns_payout_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pplns_payout_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pplns_payout_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('pplns_payout_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('pplns_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pplns_payout_message') ), ); break; @@ -93,8 +93,8 @@ case 'pps': array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pps_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pps_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pps_payout_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pps_payout_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('pps_payout_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('pps_payout_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('pps_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pps_payout_message') ), ); break; @@ -103,8 +103,8 @@ case 'prop': array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('proportional_payout_status') ), array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('proportional_payout_active') ), array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('proportional_payout_runtime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('proportional_payout_starttime') ), - array( 'NAME' => 'Last Run', 'STATUS' => $monitoring->getStatus('proportional_payout_endtime') ), + array( 'NAME' => 'Start Time', 'STATUS' => $monitoring->getStatus('proportional_payout_starttime') ), + array( 'NAME' => 'End Time', 'STATUS' => $monitoring->getStatus('proportional_payout_endtime') ), array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('proportional_payout_message') ), ); break; diff --git a/public/templates/mmcFE/admin/monitoring/default.tpl b/public/templates/mmcFE/admin/monitoring/default.tpl index f7b099b1..4c272c35 100644 --- a/public/templates/mmcFE/admin/monitoring/default.tpl +++ b/public/templates/mmcFE/admin/monitoring/default.tpl @@ -24,15 +24,27 @@ {else if $event.STATUS.type == 'message'} {$event.STATUS.value} {else if $event.STATUS.type == 'yesno'} - {if $event.STATUS.value == 1} - Yes - {else} - No - {/if} + {else if $event.STATUS.type == 'time'} - {$event.STATUS.value|default:"0"|number_format:"2"} seconds + {if $event.STATUS.value > 60} + + {else if $event.STATUS.value > 120} + + {else} + + {/if} + {$event.STATUS.value|default:"0"|number_format:"2"} seconds + {else if $event.STATUS.type == 'date'} - {$event.STATUS.value|date_format:"%m/%d %H:%M:%S"} + {if $event.STATUS.value < $smarty.now - 120} + + {else if $event.STATUS.value < $smarty.now - 180} + + {else} + + {/if} + {$event.STATUS.value|date_format:"%m/%d %H:%M:%S"} + {else} {$event.STATUS.value|default:""} {/if} From 2c3eac1f670a460d3054057b108eb2076f03bcae Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Mon, 22 Jul 2013 16:47:50 +0200 Subject: [PATCH 266/650] proper coloring for start/end times --- public/templates/mmcFE/admin/monitoring/default.tpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/templates/mmcFE/admin/monitoring/default.tpl b/public/templates/mmcFE/admin/monitoring/default.tpl index 4c272c35..07831059 100644 --- a/public/templates/mmcFE/admin/monitoring/default.tpl +++ b/public/templates/mmcFE/admin/monitoring/default.tpl @@ -36,10 +36,10 @@ {$event.STATUS.value|default:"0"|number_format:"2"} seconds {else if $event.STATUS.type == 'date'} - {if $event.STATUS.value < $smarty.now - 120} - - {else if $event.STATUS.value < $smarty.now - 180} + {if ($smarty.now - 180) > $event.STATUS.value} + {else if ($smarty.now - 120) > $event.STATUS.value} + {else} {/if} From 1d6cbd44a6cd6b8b1368c95639b9332fdfe7a668 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 23 Jul 2013 08:56:29 +0200 Subject: [PATCH 267/650] Adding new admin transaction view * Added transaction filters * Added proper paging support * Removed the tabs that caused confusion * Added transaction status column Fixes #404 --- public/include/classes/transaction.class.php | 73 +++++++- .../include/pages/admin/transactions.inc.php | 8 +- .../mmcFE/admin/transactions/default.tpl | 164 +++++++----------- 3 files changed, 138 insertions(+), 107 deletions(-) diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 2cbe4312..0d7bb511 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -81,12 +81,13 @@ class Transaction { /** * Fetch all transactions for all users + * Optionally apply a filter * @param none * @return mixed array or false **/ - public function getAllTransactions($start=0) { + public function getAllTransactions($start=0,$filter=NULL,$limit=30) { $this->debug->append("STA " . __METHOD__, 4); - $stmt = $this->mysqli->prepare(" + $sql = " SELECT t.id AS id, a.username as username, @@ -96,17 +97,77 @@ class Transaction { t.timestamp AS timestamp, b.height AS height, b.confirmations AS confirmations - FROM transactions AS t + 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 + LEFT JOIN " . $this->user->getTableName() . " AS a ON t.account_id = a.id"; + if (is_array($filter)) { + $aFilter = array(); + foreach ($filter as $key => $value) { + if (!empty($value)) { + switch ($key) { + case 'type': + $aFilter[] = "t.type = '$value'"; + break; + case 'status': + switch ($value) { + case 'Confirmed': + $aFilter[] = "b.confirmations >= " . $this->config['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('$value')"; + break; + case 'address': + $aFilter[] = "t.coin_address = '$value'"; + break; + } + } + } + if (!empty($aFilter)) { + $sql .= " WHERE " . implode(' AND ', $aFilter); + } + } + $sql .= " ORDER BY id DESC - LIMIT ?,30"); - if ($this->checkStmt($stmt) && $stmt->bind_param('i', $start) && $stmt->execute() && $result = $stmt->get_result()) + LIMIT ?,? + "; + $stmt = $this->mysqli->prepare($sql); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $start, $limit) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); $this->debug->append('Unable to fetch transactions'); return false; } + /** + * Count the amount of transactions in the table + **/ + public function getCountAllTransactions($filter=NULL) { + $stmt = $this->mysqli->prepare("SELECT COUNT(id) AS total FROM $this->table"); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->total; + $this->debug->append('Failed to fetch transaction count: ' . $this->mysqli->error); + return false; + } + 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; + } + $this->debug->append('Failed to fetch transaction types: ' . $this->mysqli->error); + return false; + } + private function checkStmt($bState) { if ($bState ===! true) { $this->debug->append("Failed to prepare statement: " . $this->mysqli->error); diff --git a/public/include/pages/admin/transactions.inc.php b/public/include/pages/admin/transactions.inc.php index fe991ba5..2c1126ba 100644 --- a/public/include/pages/admin/transactions.inc.php +++ b/public/include/pages/admin/transactions.inc.php @@ -11,11 +11,17 @@ if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { $debug->append('No cached version available, fetching from backend', 3); - $aTransactions = $transaction->getAllTransactions(@$_REQUEST['start']); + $aTransactions = $transaction->getAllTransactions(@$_REQUEST['start'], @$_REQUEST['filter'], 5); + $iCountTransactions = $transaction->getCountAllTransactions(); + $aTransactionTypes = $transaction->getTypes(); if (!$aTransactions) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any transaction', 'TYPE' => 'errormsg'); } else { $debug->append('Using cached page', 3); } + $smarty->assign('TRANSACTIONS', $aTransactions); +$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/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl index c493de8b..b9833bad 100644 --- a/public/templates/mmcFE/admin/transactions/default.tpl +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -1,47 +1,91 @@ -{include file="global/block_header.tpl" BLOCK_HEADER="Transaction Log" BUTTONS=array(Confirmed,Unconfirmed,Orphan)} +{include file="global/block_header.tpl" ALIGN="left" BLOCK_STYLE="width: 23%" BLOCK_HEADER="Transaction Filter"} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +{if $COUNTTRANSACTIONS / 5 > 1} + {if $smarty.request.start > 0} + + {else} + + {/if} +{/if} + +{if $COUNTTRANSACTIONS / 5 > 1} + {if $COUNTTRANSACTIONS - $smarty.request.start - 5 > 0} + + {else} + + {/if} +{/if} +
    Type{html_options name="filter[type]" options=$TRANSACTIONTYPES selected=$smarty.request.filter.type}
    Status{html_options name="filter[status]" options=$TXSTATUS selected=$smarty.request.filter.status}
    Account
    Address
    +{include file="global/block_footer.tpl"} + +{include file="global/block_header.tpl" ALIGN="right" BLOCK_STYLE="width: 75%" BLOCK_HEADER="Transaction History"} +
    - - -
    -
    -
    - +
    + - {assign var=confirmed value=0} {section transaction $TRANSACTIONS} - {if ( - ( ( $TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee' ) and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations ) - or $TRANSACTIONS[transaction].type == 'Credit_PPS' or $TRANSACTIONS[transaction].type == 'Fee_PPS' or $TRANSACTIONS[transaction].type == 'Donation_PPS' - or $TRANSACTIONS[transaction].type == 'Debit_AP' or $TRANSACTIONS[transaction].type == 'Debit_MP' or $TRANSACTIONS[transaction].type == 'TXFee' - )} - {assign var=confirmed value=1} + - {/if} {/section} - {if $confirmed != 1} - - - - {/if}
    TX # Account Date TX TypeStatus Payment Address Block # Amount
    {$TRANSACTIONS[transaction].id} {$TRANSACTIONS[transaction].username} {$TRANSACTIONS[transaction].timestamp} {$TRANSACTIONS[transaction].type} + {if $TRANSACTIONS[transaction].type == 'Credit_PPS' OR + $TRANSACTIONS[transaction].type == 'Debit_MP' OR + $TRANSACTIONS[transaction].type == 'Debit_AP' OR + $TRANSACTIONS[transaction].confirmations > $GLOBAL.confirmations + }Confirmed + {else if $TRANSACTIONS[transaction].confirmations == -1}Orphaned + {else}Unconfirmed{/if} + ({$TRANSACTIONS[transaction].confirmations|default:"n/a"}) + {$TRANSACTIONS[transaction].coin_address} {if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if} {$TRANSACTIONS[transaction].amount|number_format:"8"}
    No confirmed transactions

    @@ -51,84 +95,4 @@

    -
    -
    - - - - - - - - - - - - - - {assign var=unconfirmed value=0} -{section transaction $TRANSACTIONS} - {if ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Fee') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations and $TRANSACTIONS[transaction].confirmations >= 0} - {assign var=unconfirmed value=1} - - - - - - - - - - {/if} -{/section} - {if $unconfirmed != 1} - - - - {/if} - -
    TX #AccountDateTX TypePayment AddressBlock #Amount
    {$TRANSACTIONS[transaction].id}{$TRANSACTIONS[transaction].username}{$TRANSACTIONS[transaction].timestamp}{$TRANSACTIONS[transaction].type}{$TRANSACTIONS[transaction].coin_address}{if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if}{$TRANSACTIONS[transaction].amount|number_format:"8"}
    No unconfirmed transactions
    -

    Listed are your estimated rewards and donations/fees for all blocks awaiting {$GLOBAL.confirmations} confirmations.

    -
    -
    -
    -
    - - - - - - - - - - - - - - {assign var=orphaned value=0} -{section transaction $TRANSACTIONS} - {if ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Fee' or $TRANSACTIONS[transaction].type == 'Donation' or $TRANSACTIONS[transaction].type == 'Bonus') and $TRANSACTIONS[transaction].confirmations == -1} - {assign var=orphaned value=1} - - - - - - - - - - {/if} -{/section} - {if $orphaned != 1} - - - - {/if} - -
    TX #AccountDateTX TypePayment AddressBlock #Amount
    {$TRANSACTIONS[transaction].id}{$TRANSACTIONS[transaction].username}{$TRANSACTIONS[transaction].timestamp}{$TRANSACTIONS[transaction].type}{$TRANSACTIONS[transaction].coin_address}{if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if}{$TRANSACTIONS[transaction].amount|number_format:"8"}
    No orphan transactions
    -

    Listed are your orphaned transactions for blocks not part of the main blockchain.

    -
    -
    {include file="global/block_footer.tpl"} From 1a5d216b7a1c820212121a553570137a818673e0 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 23 Jul 2013 09:08:57 +0200 Subject: [PATCH 268/650] Allow adjusting the tx row limit --- public/include/pages/admin/transactions.inc.php | 12 +++++++----- .../templates/mmcFE/admin/transactions/default.tpl | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/public/include/pages/admin/transactions.inc.php b/public/include/pages/admin/transactions.inc.php index 2c1126ba..6089858a 100644 --- a/public/include/pages/admin/transactions.inc.php +++ b/public/include/pages/admin/transactions.inc.php @@ -10,18 +10,20 @@ if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { } if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { + $iLimit = 30; $debug->append('No cached version available, fetching from backend', 3); - $aTransactions = $transaction->getAllTransactions(@$_REQUEST['start'], @$_REQUEST['filter'], 5); + $aTransactions = $transaction->getAllTransactions(@$_REQUEST['start'], @$_REQUEST['filter'], $iLimit); $iCountTransactions = $transaction->getCountAllTransactions(); $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('TRANSACTIONTYPES', $aTransactionTypes); + $smarty->assign('TXSTATUS', array('' => '', 'Confirmed' => 'Confirmed', 'Unconfirmed' => 'Unconfirmed', 'Orphan' => 'Orphan')); + $smarty->assign('COUNTTRANSACTIONS', $iCountTransactions); } else { $debug->append('Using cached page', 3); } -$smarty->assign('TRANSACTIONS', $aTransactions); -$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/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl index b9833bad..fff98814 100644 --- a/public/templates/mmcFE/admin/transactions/default.tpl +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -3,7 +3,7 @@ -{if $COUNTTRANSACTIONS / 5 > 1} +{if $COUNTTRANSACTIONS / $LIMIT > 1} {if $smarty.request.start > 0} {else} @@ -12,8 +12,8 @@ {/if} -{if $COUNTTRANSACTIONS / 5 > 1} - {if $COUNTTRANSACTIONS - $smarty.request.start - 5 > 0} +{if $COUNTTRANSACTIONS / $LIMIT > 1} + {if $COUNTTRANSACTIONS - $smarty.request.start - $LIMIT > 0} {else} From 728bfe8c9dd380c8d2030c335fcd7e478cd255a3 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 23 Jul 2013 09:18:04 +0200 Subject: [PATCH 269/650] properly filter Credit_PPS as confirmed --- public/include/classes/transaction.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 0d7bb511..987a8970 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -111,7 +111,7 @@ class Transaction { case 'status': switch ($value) { case 'Confirmed': - $aFilter[] = "b.confirmations >= " . $this->config['confirmations']; + if ($filter['type'] != 'Credit_PPS') $aFilter[] = "b.confirmations >= " . $this->config['confirmations']; break; case 'Unconfirmed': $aFilter[] = "b.confirmations < " . $this->config['confirmations'] . " AND b.confirmations >= 0"; @@ -137,7 +137,7 @@ class Transaction { $sql .= " ORDER BY id DESC LIMIT ?,? - "; + "; $stmt = $this->mysqli->prepare($sql); if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $start, $limit) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); From 61cea524d139f6db9d643db401c657090c6209d2 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 23 Jul 2013 09:18:50 +0200 Subject: [PATCH 270/650] properly use limit for pagination --- public/templates/mmcFE/admin/transactions/default.tpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl index fff98814..ee0b2aac 100644 --- a/public/templates/mmcFE/admin/transactions/default.tpl +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -5,7 +5,7 @@ {if $COUNTTRANSACTIONS / $LIMIT > 1} {if $smarty.request.start > 0} - + {else} {/if} @@ -14,7 +14,7 @@ {if $COUNTTRANSACTIONS / $LIMIT > 1} {if $COUNTTRANSACTIONS - $smarty.request.start - $LIMIT > 0} - + {else} {/if} From ed259a5b4498ba1d5c0739f152d22c4fc80e0d73 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 23 Jul 2013 09:21:11 +0200 Subject: [PATCH 271/650] properly deal with Fee_PPS and Donation_PPS --- public/include/classes/transaction.class.php | 2 +- public/templates/mmcFE/admin/transactions/default.tpl | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 987a8970..2d4431a9 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -111,7 +111,7 @@ class Transaction { case 'status': switch ($value) { case 'Confirmed': - if ($filter['type'] != 'Credit_PPS') $aFilter[] = "b.confirmations >= " . $this->config['confirmations']; + if ($filter['type'] != 'Credit_PPS' && $filter['type'] != 'Fee_PPS' && $filter['type'] != $filter['Donation_PPS']) $aFilter[] = "b.confirmations >= " . $this->config['confirmations']; break; case 'Unconfirmed': $aFilter[] = "b.confirmations < " . $this->config['confirmations'] . " AND b.confirmations >= 0"; diff --git a/public/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl index ee0b2aac..7141b5b1 100644 --- a/public/templates/mmcFE/admin/transactions/default.tpl +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -73,6 +73,8 @@ {$TRANSACTIONS[transaction].type} {if $TRANSACTIONS[transaction].type == 'Credit_PPS' OR + $TRANSACTIONS[transaction].type == 'Fee_PPS' OR + $TRANSACTIONS[transaction].type == 'Donation_PPS' OR $TRANSACTIONS[transaction].type == 'Debit_MP' OR $TRANSACTIONS[transaction].type == 'Debit_AP' OR $TRANSACTIONS[transaction].confirmations > $GLOBAL.confirmations From 330169ae582a6bfe8ba011f9c637153e047fce3b Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Tue, 23 Jul 2013 10:53:47 +0200 Subject: [PATCH 272/650] more fixes and log cleanup --- public/include/classes/transaction.class.php | 4 +++- .../include/pages/admin/transactions.inc.php | 9 ++++++++ .../mmcFE/admin/transactions/default.tpl | 22 +++++++++---------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 2d4431a9..7758f4cf 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -111,7 +111,9 @@ class Transaction { case 'status': switch ($value) { case 'Confirmed': - if ($filter['type'] != 'Credit_PPS' && $filter['type'] != 'Fee_PPS' && $filter['type'] != $filter['Donation_PPS']) $aFilter[] = "b.confirmations >= " . $this->config['confirmations']; + if (empty($filter['type']) || ($filter['type'] != 'Credit_PPS' && $filter['type'] != 'Fee_PPS' && $filter['type'] != 'Donation_PPS')) { + $aFilter[] = "b.confirmations >= " . $this->config['confirmations']; + } break; case 'Unconfirmed': $aFilter[] = "b.confirmations < " . $this->config['confirmations'] . " AND b.confirmations >= 0"; diff --git a/public/include/pages/admin/transactions.inc.php b/public/include/pages/admin/transactions.inc.php index 6089858a..f106de26 100644 --- a/public/include/pages/admin/transactions.inc.php +++ b/public/include/pages/admin/transactions.inc.php @@ -25,5 +25,14 @@ if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { $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/templates/mmcFE/admin/transactions/default.tpl b/public/templates/mmcFE/admin/transactions/default.tpl index 7141b5b1..8014cf09 100644 --- a/public/templates/mmcFE/admin/transactions/default.tpl +++ b/public/templates/mmcFE/admin/transactions/default.tpl @@ -4,8 +4,8 @@ {if $COUNTTRANSACTIONS / $LIMIT > 1} - {if $smarty.request.start > 0} - + {if $smarty.request.start|default:"0" > 0} + {else} {/if} @@ -13,8 +13,8 @@ {if $COUNTTRANSACTIONS / $LIMIT > 1} - {if $COUNTTRANSACTIONS - $smarty.request.start - $LIMIT > 0} - + {if $COUNTTRANSACTIONS - $smarty.request.start|default:"0" - $LIMIT > 0} + {else} {/if} @@ -25,20 +25,20 @@ - Type - {html_options name="filter[type]" options=$TRANSACTIONTYPES selected=$smarty.request.filter.type} + TX Type + {html_options name="filter[type]" options=$TRANSACTIONTYPES selected=$smarty.request.filter.type|default:""} - Status - {html_options name="filter[status]" options=$TXSTATUS selected=$smarty.request.filter.status} + TX Status + {html_options name="filter[status]" options=$TXSTATUS selected=$smarty.request.filter.status|default:""} Account - + Address - + @@ -77,7 +77,7 @@ $TRANSACTIONS[transaction].type == 'Donation_PPS' OR $TRANSACTIONS[transaction].type == 'Debit_MP' OR $TRANSACTIONS[transaction].type == 'Debit_AP' OR - $TRANSACTIONS[transaction].confirmations > $GLOBAL.confirmations + $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations }Confirmed {else if $TRANSACTIONS[transaction].confirmations == -1}Orphaned {else}Unconfirmed{/if} From 3cfef9358013cd7a048634ec3922c4c1ea33b264 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Wed, 17 Jul 2013 11:21:20 +0200 Subject: [PATCH 273/650] WiP to disable fees for specific accounts * added new account table column: `no_fee` * honor `no_fee` flag during payout process * added upgrade SQL file for this feature Address #260 --- cronjobs/pplns_payout.php | 2 +- cronjobs/pps_payout.php | 2 +- cronjobs/proportional_payout.php | 2 +- public/include/classes/share.class.php | 2 ++ sql/005_accounts_nofees.sql | 1 + 5 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 sql/005_accounts_nofees.sql diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php index f7cdb6b2..66d714d3 100755 --- a/cronjobs/pplns_payout.php +++ b/cronjobs/pplns_payout.php @@ -130,7 +130,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $aData['fee' ] = 0; $aData['donation'] = 0; - if ($config['fees'] > 0) + if ($config['fees'] > 0 && $aData['no_fees'] == 0) $aData['fee'] = number_format(round($config['fees'] / 100 * $aData['payout'], 8), 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); diff --git a/cronjobs/pps_payout.php b/cronjobs/pps_payout.php index 5e6f673a..fc66ecff 100755 --- a/cronjobs/pps_payout.php +++ b/cronjobs/pps_payout.php @@ -72,7 +72,7 @@ foreach ($aAccountShares as $aData) { $aData['donation'] = 0; // Calculate block fees - if ($config['fees'] > 0) + if ($config['fees'] > 0 && $aData['no_fees'] == 0) $aData['fee'] = number_format(round($config['fees'] / 100 * $aData['payout'], 8), 8); // Calculate donation amount $aData['donation'] = number_format(round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8), 8); diff --git a/cronjobs/proportional_payout.php b/cronjobs/proportional_payout.php index 36a9a1fe..47e55dc3 100755 --- a/cronjobs/proportional_payout.php +++ b/cronjobs/proportional_payout.php @@ -66,7 +66,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $aData['fee' ] = 0; $aData['donation'] = 0; - if ($config['fees'] > 0) + if ($config['fees'] > 0 && $aData['no_fees'] == 0) $aData['fee'] = number_format(round($config['fees'] / 100 * $aData['payout'], 8), 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); diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index 7bcc6d14..9fe1ff16 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -97,6 +97,7 @@ class Share { SELECT a.id, SUBSTRING_INDEX( s.username , '.', 1 ) as username, + a.no_fees AS no_fees, IFNULL(SUM(IF(our_result='Y', 1, 0)), 0) AS valid, IFNULL(SUM(IF(our_result='N', 1, 0)), 0) AS invalid FROM $this->table AS s @@ -135,6 +136,7 @@ class Share { SELECT a.id, SUBSTRING_INDEX( s.username , '.', 1 ) as account, + a.no_fees AS no_fees, IFNULL(SUM(IF(our_result='Y', 1, 0)), 0) AS valid, IFNULL(SUM(IF(our_result='N', 1, 0)), 0) AS invalid FROM $this->tableArchive AS s diff --git a/sql/005_accounts_nofees.sql b/sql/005_accounts_nofees.sql new file mode 100644 index 00000000..19d52e7e --- /dev/null +++ b/sql/005_accounts_nofees.sql @@ -0,0 +1 @@ +ALTER TABLE `accounts` ADD `no_fees` BOOLEAN NOT NULL DEFAULT FALSE AFTER `is_anonymous` ; From 63b942a7e1576068594935746b0da7422483ab85 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 18 Jul 2013 09:21:33 +0200 Subject: [PATCH 274/650] Adding No Fee option to admin panel Admins can disable a users fee via admin panel now. Fixes #260 --- public/include/classes/statistics.class.php | 1 + public/include/classes/user.class.php | 10 ++++++++++ public/include/pages/admin/user.inc.php | 15 +++++++++------ public/templates/mmcFE/admin/user/default.tpl | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/public/include/classes/statistics.class.php b/public/include/classes/statistics.class.php index e2d1848a..1452db89 100644 --- a/public/include/classes/statistics.class.php +++ b/public/include/classes/statistics.class.php @@ -224,6 +224,7 @@ 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, diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 4f9f5cd4..855bd7e1 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -43,6 +43,9 @@ class User { 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'); } @@ -58,12 +61,19 @@ class User { public function getUserFailed($id) { return $this->getSingle($id, 'failed_logins', 'id'); } + public function isNoFee($id) { + return $this->getUserNoFee($id); + } public function isLocked($id) { return $this->getUserLocked($id); } 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); diff --git a/public/include/pages/admin/user.inc.php b/public/include/pages/admin/user.inc.php index ecb447b9..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']) { diff --git a/public/templates/mmcFE/admin/user/default.tpl b/public/templates/mmcFE/admin/user/default.tpl index fef10bfa..83dbf92d 100644 --- a/public/templates/mmcFE/admin/user/default.tpl +++ b/public/templates/mmcFE/admin/user/default.tpl @@ -1,4 +1,11 @@ '),g.close(),b.location.hash=c)}}(),c}()}(a,this),function(a,d){b.matchMedia=b.matchMedia||function(a,b){var c,d=a.documentElement,e=d.firstElementChild||d.firstChild,f=a.createElement("body"),g=a.createElement("div");return g.id="mq-test-1",g.style.cssText="position:absolute;top:-100em",f.style.background="none",f.appendChild(g),function(a){return g.innerHTML='­',d.insertBefore(f,e),c=g.offsetWidth===42,d.removeChild(f),{matches:c,media:a}}}(c),a.mobile.media=function(a){return b.matchMedia(a).matches}}(a),function(a,b){var d={touch:"ontouchend"in c};a.mobile.support=a.mobile.support||{},a.extend(a.support,d),a.extend(a.mobile.support,d)}(a),function(a,c){a.extend(a.support,{orientation:"orientation"in b&&"onorientationchange"in b})}(a),function(a,d){function e(a){var b=a.charAt(0).toUpperCase()+a.substr(1),c=(a+" "+h.join(b+" ")+b).split(" ");for(var e in c)if(g[c[e]]!==d)return!0}function m(a,b,d){var e=c.createElement("div"),f=function(a){return a.charAt(0).toUpperCase()+a.substr(1)},g=function(a){return a===""?"":"-"+a.charAt(0).toLowerCase()+a.substr(1)+"-"},i=function(c){var d=g(c)+a+": "+b+";",h=f(c),i=h+(h===""?a:f(a));e.setAttribute("style",d),!e.style[i]||(k=!0)},j=d?d:h,k;for(var l=0;l",{href:b}).appendTo("head"),g=a("").prependTo(f),h=g[0].href,c[0].href=e||location.pathname,d&&d.remove(),h.indexOf(b)===0}function p(){var a=c.createElement("x"),d=c.documentElement,e=b.getComputedStyle,f;return"pointerEvents"in a.style?(a.style.pointerEvents="auto",a.style.pointerEvents="x",d.appendChild(a),f=e&&e(a,"").pointerEvents==="auto",d.removeChild(a),!!f):!1}function q(){var a=c.createElement("div");return typeof a.getBoundingClientRect!="undefined"}function r(){var a=b,c=navigator.userAgent,d=navigator.platform,e=c.match(/AppleWebKit\/([0-9]+)/),f=!!e&&e[1],g=c.match(/Fennec\/([0-9]+)/),h=!!g&&g[1],i=c.match(/Opera Mobi\/([0-9]+)/),j=!!i&&i[1];return(d.indexOf("iPhone")>-1||d.indexOf("iPad")>-1||d.indexOf("iPod")>-1)&&f&&f<534||a.operamini&&{}.toString.call(a.operamini)==="[object OperaMini]"||i&&j<7458||c.indexOf("Android")>-1&&f&&f<533||h&&h<6||"palmGetResource"in b&&f&&f<534||c.indexOf("MeeGo")>-1&&c.indexOf("NokiaBrowser/8.5.0")>-1?!1:!0}var f=a("").prependTo("html"),g=f[0].style,h=["Webkit","Moz","O"],i="palmGetResource"in b,j=b.opera,k=b.operamini&&{}.toString.call(b.operamini)==="[object OperaMini]",l=b.blackberry&&!e("-webkit-transform");a.extend(a.mobile,{browser:{}}),a.mobile.browser.oldIE=function(){var a=3,b=c.createElement("div"),d=b.all||[];do b.innerHTML="";while(d[0]);return a>4?a:!a}(),a.extend(a.support,{cssTransitions:"WebKitTransitionEvent"in b||m("transition","height 100ms linear",["Webkit","Moz",""])&&!a.mobile.browser.oldIE&&!j,pushState:"pushState"in history&&"replaceState"in history&&b.navigator.userAgent.search(/CriOS/)===-1,mediaquery:a.mobile.media("only all"),cssPseudoElement:!!e("content"),touchOverflow:!!e("overflowScrolling"),cssTransform3d:n(),boxShadow:!!e("boxShadow")&&!l,fixedPosition:r(),scrollTop:("pageXOffset"in b||"scrollTop"in c.documentElement||"scrollTop"in f[0])&&!i&&!k,dynamicBaseTag:o(),cssPointerEvents:p(),boundingRect:q()}),f.remove();var s=function(){var a=b.navigator.userAgent;return a.indexOf("Nokia")>-1&&(a.indexOf("Symbian/3")>-1||a.indexOf("Series60/5")>-1)&&a.indexOf("AppleWebKit")>-1&&a.match(/(BrowserNG|NokiaBrowser)\/7\.[0-3]/)}();a.mobile.gradeA=function(){return(a.support.mediaquery||a.mobile.browser.oldIE&&a.mobile.browser.oldIE>=7)&&(a.support.boundingRect||a.fn.jquery.match(/1\.[0-7+]\.[0-9+]?/)!==null)},a.mobile.ajaxBlacklist=b.blackberry&&!b.WebKitPoint||k||s,s&&a(function(){a("head link[rel='stylesheet']").attr("rel","alternate stylesheet").attr("rel","stylesheet")}),a.support.boxShadow||a("html").addClass("ui-mobile-nosupport-boxshadow")}(a),function(a,b){var c=a.mobile.window,d,e;a.event.special.navigate=d={bound:!1,pushStateEnabled:!0,originalEventName:b,isPushStateEnabled:function(){return a.support.pushState&&a.mobile.pushStateEnabled===!0&&this.isHashChangeEnabled()},isHashChangeEnabled:function(){return a.mobile.hashListeningEnabled===!0},popstate:function(b){var d=new a.Event("navigate"),e=new a.Event("beforenavigate"),f=b.originalEvent.state||{},g=location.href;c.trigger(e);if(e.isDefaultPrevented())return;b.historyState&&a.extend(f,b.historyState),d.originalEvent=b,setTimeout(function(){c.trigger(d,{state:f})},0)},hashchange:function(b,d){var e=new a.Event("navigate"),f=new a.Event("beforenavigate");c.trigger(f);if(f.isDefaultPrevented())return;e.originalEvent=b,c.trigger(e,{state:b.hashchangeState||{}})},setup:function(a,b){if(d.bound)return;d.bound=!0,d.isPushStateEnabled()?(d.originalEventName="popstate",c.bind("popstate.navigate",d.popstate)):d.isHashChangeEnabled()&&(d.originalEventName="hashchange",c.bind("hashchange.navigate",d.hashchange))}}}(a),function(a,c){var d,e,f,g="&ui-state=dialog";a.mobile.path=d={uiStateKey:"&ui-state",urlParseRE:/^\s*(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,getLocation:function(a){var b=a?this.parseUrl(a):location,c=this.parseUrl(a||location.href).hash;return c=c==="#"?"":c,b.protocol+"//"+b.host+b.pathname+b.search+c},parseLocation:function(){return this.parseUrl(this.getLocation())},parseUrl:function(b){if(a.type(b)==="object")return b;var c=d.urlParseRE.exec(b||"")||[];return{href:c[0]||"",hrefNoHash:c[1]||"",hrefNoSearch:c[2]||"",domain:c[3]||"",protocol:c[4]||"",doubleSlash:c[5]||"",authority:c[6]||"",username:c[8]||"",password:c[9]||"",host:c[10]||"",hostname:c[11]||"",port:c[12]||"",pathname:c[13]||"",directory:c[14]||"",filename:c[15]||"",search:c[16]||"",hash:c[17]||""}},makePathAbsolute:function(a,b){if(a&&a.charAt(0)==="/")return a;a=a||"",b=b?b.replace(/^\/|(\/[^\/]*|[^\/]+)$/g,""):"";var c=b?b.split("/"):[],d=a.split("/");for(var e=0;e-1&&(l=f.slice(h),f=f.slice(0,h)),e=d.makeUrlAbsolute(f,b),g=this.parseUrl(e).search;if(i){if(d.isPath(k)||k.replace("#","").indexOf(this.uiStateKey)===0)k="";l&&k.indexOf(this.uiStateKey)===-1&&(k+=l),k.indexOf("#")===-1&&k!==""&&(k="#"+k),e=d.parseUrl(e),e=e.protocol+"//"+e.host+e.pathname+g+k}else e+=e.indexOf("#")>-1?l:"#"+l;return e},isPreservableHash:function(a){return a.replace("#","").indexOf(this.uiStateKey)===0}},d.documentUrl=d.parseLocation(),f=a("head").find("base"),d.documentBase=f.length?d.parseUrl(d.makeUrlAbsolute(f.attr("href"),d.documentUrl.href)):d.documentUrl,d.documentBaseDiffers=d.documentUrl.hrefNoHash!==d.documentBase.hrefNoHash,d.getDocumentUrl=function(b){return b?a.extend({},d.documentUrl):d.documentUrl.href},d.getDocumentBase=function(b){return b?a.extend({},d.documentBase):d.documentBase.href}}(a),function(a,b){var c=a.mobile.path;a.mobile.History=function(a,b){this.stack=a||[],this.activeIndex=b||0},a.extend(a.mobile.History.prototype,{getActive:function(){return this.stack[this.activeIndex]},getLast:function(){return this.stack[this.previousIndex]},getNext:function(){return this.stack[this.activeIndex+1]},getPrev:function(){return this.stack[this.activeIndex-1]},add:function(a,b){b=b||{},this.getNext()&&this.clearForward(),b.hash&&b.hash.indexOf("#")===-1&&(b.hash="#"+b.hash),b.url=a,this.stack.push(b),this.activeIndex=this.stack.length-1},clearForward:function(){this.stack=this.stack.slice(0,this.activeIndex+1)},find:function(a,b,c){b=b||this.stack;var d,e,f=b.length,g;for(e=0;ee?(c.present||c.forward||a.noop)(this.getActive(),"forward"):d===b&&c.missing&&c.missing(this.getActive())}})}(a),function(a,d){var e=a.mobile.path;a.mobile.Navigator=function(b){this.history=b,this.ignoreInitialHashChange=!0,setTimeout(a.proxy(function(){this.ignoreInitialHashChange=!1},this),200),a.mobile.window.bind({"popstate.history":a.proxy(this.popstate,this),"hashchange.history":a.proxy(this.hashchange,this)})},a.extend(a.mobile.Navigator.prototype,{squash:function(d,f){var g,h,i=e.isPath(d)?e.stripHash(d):d;return h=e.squash(d),g=a.extend({hash:i,url:h},f),b.history.replaceState(g,g.title||c.title,h),g},hash:function(a,b){var c,d,f;c=e.parseUrl(a),d=e.parseLocation();if(d.pathname+d.search===c.pathname+c.search)f=c.hash?c.hash:c.pathname+c.search;else if(e.isPath(a)){var g=e.parseUrl(b);f=g.pathname+g.search+(e.isPreservableHash(g.hash)?g.hash.replace("#",""):"")}else f=a;return f},go:function(d,f,g){var h,i,j,k,l=a.event.special.navigate.isPushStateEnabled();i=e.squash(d),j=this.hash(d,i),g&&j!==e.stripHash(e.parseLocation().hash)&&(this.preventNextHashChange=g),this.preventHashAssignPopState=!0,b.location.hash=j,this.preventHashAssignPopState=!1,h=a.extend({url:i,hash:j,title:c.title},f),l&&(k=new a.Event("popstate"),k.originalEvent={type:"popstate",state:null},this.squash(d,h),g||(this.ignorePopState=!0,a.mobile.window.trigger(k))),this.history.add(h.url,h)},popstate:function(b){var c,d,f,g;if(!a.event.special.navigate.isPushStateEnabled())return;if(this.preventHashAssignPopState){this.preventHashAssignPopState=!1,b.stopImmediatePropagation();return}if(this.ignorePopState){this.ignorePopState=!1;return}if(!b.originalEvent.state&&this.history.stack.length===1&&this.ignoreInitialHashChange){this.ignoreInitialHashChange=!1;return}d=e.parseLocation().hash;if(!b.originalEvent.state&&d){f=this.squash(d),this.history.add(f.url,f),b.historyState=f;return}this.history.direct({url:(b.originalEvent.state||{}).url||d,present:function(c,d){b.historyState=a.extend({},c),b.historyState.direction=d}})},hashchange:function(b){var d,f;if(!a.event.special.navigate.isHashChangeEnabled()||a.event.special.navigate.isPushStateEnabled())return;if(this.preventNextHashChange){this.preventNextHashChange=!1,b.stopImmediatePropagation();return}d=this.history,f=e.parseLocation().hash,this.history.direct({url:f,present:function(c,d){b.hashchangeState=a.extend({},c),b.hashchangeState.direction=d},missing:function(){d.add(f,{hash:f,title:c.title})}})}})}(a),function(a,b){a.mobile.navigate=function(b,c,d){a.mobile.navigate.navigator.go(b,c,d)},a.mobile.navigate.history=new a.mobile.History,a.mobile.navigate.navigator=new a.mobile.Navigator(a.mobile.navigate.history);var c=a.mobile.path.parseLocation();a.mobile.navigate.history.add(c.href,{hash:c.hash})}(a),function(a,b,c,d){function x(a){while(a&&typeof a.originalEvent!="undefined")a=a.originalEvent;return a}function y(b,c){var e=b.type,f,g,i,k,l,m,n,o,p;b=a.Event(b),b.type=c,f=b.originalEvent,g=a.event.props,e.search(/^(mouse|click)/)>-1&&(g=j);if(f)for(n=g.length,k;n;)k=g[--n],b[k]=f[k];e.search(/mouse(down|up)|click/)>-1&&!b.which&&(b.which=1);if(e.search(/^touch/)!==-1){i=x(f),e=i.touches,l=i.changedTouches,m=e&&e.length?e[0]:l&&l.length?l[0]:d;if(m)for(o=0,p=h.length;oe||Math.abs(c.pageY-n)>e,o&&!d&&H("vmousecancel",b,f),H("vmousemove",b,f),F()}function M(a){if(r)return;C();var b=z(a.target),c;H("vmouseup",a,b);if(!o){var d=H("vclick",a,b);d&&d.isDefaultPrevented()&&(c=x(a).changedTouches[0],p.push({touchID:v,x:c.clientX,y:c.clientY}),q=!0)}H("vmouseout",a,b),o=!1,F()}function N(b){var c=a.data(b,e),d;if(c)for(d in c)if(c[d])return!0;return!1}function O(){}function P(b){var c=b.substr(1);return{setup:function(d,f){N(this)||a.data(this,e,{});var g=a.data(this,e);g[b]=!0,k[b]=(k[b]||0)+1,k[b]===1&&t.bind(c,I),a(this).bind(c,O),s&&(k.touchstart=(k.touchstart||0)+1,k.touchstart===1&&t.bind("touchstart",J).bind("touchend",M).bind("touchmove",L).bind("scroll",K))},teardown:function(d,f){--k[b],k[b]||t.unbind(c,I),s&&(--k.touchstart,k.touchstart||t.unbind("touchstart",J).unbind("touchmove",L).unbind("touchend",M).unbind("scroll",K));var g=a(this),h=a.data(this,e);h&&(h[b]=!1),g.unbind(c,O),N(this)||g.removeData(e)}}}var e="virtualMouseBindings",f="virtualTouchID",g="vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split(" "),h="clientX clientY pageX pageY screenX screenY".split(" "),i=a.event.mouseHooks?a.event.mouseHooks.props:[],j=a.event.props.concat(i),k={},l=0,m=0,n=0,o=!1,p=[],q=!1,r=!1,s="addEventListener"in c,t=a(c),u=1,v=0,w;a.vmouse={moveDistanceThreshold:10,clickDistanceThreshold:10,resetTimerDuration:1500};for(var Q=0;Qa.event.special.swipe.horizontalDistanceThreshold&&Math.abs(b.coords[1]-c.coords[1])c.coords[0]?"swipeleft":"swiperight")},setup:function(){var b=this,c=a(b);c.bind(h,function(b){function g(b){if(!e)return;f=a.event.special.swipe.stop(b),Math.abs(e.coords[0]-f.coords[0])>a.event.special.swipe.scrollSupressionThreshold&&b.preventDefault()}var e=a.event.special.swipe.start(b),f;c.bind(j,g).one(i,function(){c.unbind(j,g),e&&f&&a.event.special.swipe.handleSwipe(e,f),e=f=d})})}},a.each({scrollstop:"scrollstart",taphold:"tap",swipeleft:"swipe",swiperight:"swipe"},function(b,c){a.event.special[b]={setup:function(){a(this).bind(c,a.noop)}}})}(a,this),function(a){a.event.special.throttledresize={setup:function(){a(this).bind("resize",c)},teardown:function(){a(this).unbind("resize",c)}};var b=250,c=function(){f=(new Date).getTime(),g=f-d,g>=b?(d=f,a(this).trigger("throttledresize")):(e&&clearTimeout(e),e=setTimeout(c,b-g))},d=0,e,f,g}(a),function(a,b){function o(){var a=g();a!==h&&(h=a,d.trigger(e))}var d=a(b),e="orientationchange",f,g,h,i,j,k={0:!0,180:!0};if(a.support.orientation){var l=b.innerWidth||d.width(),m=b.innerHeight||d.height(),n=50;i=l>m&&l-m>n,j=k[b.orientation];if(i&&j||!i&&!j)k={"-90":!0,90:!0}}a.event.special.orientationchange=a.extend({},a.event.special.orientationchange,{setup:function(){if(a.support.orientation&&!a.event.special.orientationchange.disabled)return!1;h=g(),d.bind("throttledresize",o)},teardown:function(){if(a.support.orientation&&!a.event.special.orientationchange.disabled)return!1;d.unbind("throttledresize",o)},add:function(a){var b=a.handler;a.handler=function(a){return a.orientation=g(),b.apply(this,arguments)}}}),a.event.special.orientationchange.orientation=g=function(){var d=!0,e=c.documentElement;return a.support.orientation?d=k[b.orientation]:d=e&&e.clientWidth/e.clientHeight<1.1,d?"portrait":"landscape"},a.fn[e]=function(a){return a?this.bind(e,a):this.trigger(e)},a.attrFn&&(a.attrFn[e]=!0)}(a,this),function(a,b){a.widget("mobile.page",a.mobile.widget,{options:{theme:"c",domCache:!1,keepNativeDefault:":jqmData(role='none'), :jqmData(role='nojs')"},_create:function(){if(this._trigger("beforecreate")===!1)return!1;this.element.attr("tabindex","0").addClass("ui-page ui-body-"+this.options.theme),this._on(this.element,{pagebeforehide:"removeContainerBackground",pagebeforeshow:"_handlePageBeforeShow"})},_handlePageBeforeShow:function(a){this.setContainerBackground()},removeContainerBackground:function(){a.mobile.pageContainer.removeClass("ui-overlay-"+a.mobile.getInheritedTheme(this.element.parent()))},setContainerBackground:function(b){this.options.theme&&a.mobile.pageContainer.addClass("ui-overlay-"+(b||this.options.theme))},keepNativeSelector:function(){var b=this.options,c=b.keepNative&&a.trim(b.keepNative);return c&&b.keepNative!==b.keepNativeDefault?[b.keepNative,b.keepNativeDefault].join(", "):b.keepNativeDefault}})}(a),function(a,b,c){var d=function(d){return d===c&&(d=!0),function(c,e,f,g){var h=new a.Deferred,i=e?" reverse":"",j=a.mobile.urlHistory.getActive(),k=j.lastScroll||a.mobile.defaultHomeScroll,l=a.mobile.getScreenHeight(),m=a.mobile.maxTransitionWidth!==!1&&a.mobile.window.width()>a.mobile.maxTransitionWidth,n=!a.support.cssTransitions||m||!c||c==="none"||Math.max(a.mobile.window.scrollTop(),k)>a.mobile.getMaxScrollForTransition(),o=" ui-page-pre-in",p=function(){a.mobile.pageContainer.toggleClass("ui-mobile-viewport-transitioning viewport-"+c)},q=function(){a.event.special.scrollstart.enabled=!1,b.scrollTo(0,k),setTimeout(function(){a.event.special.scrollstart.enabled=!0},150)},r=function(){g.removeClass(a.mobile.activePageClass+" out in reverse "+c).height("")},s=function(){d?g.animationComplete(t):t(),g.height(l+a.mobile.window.scrollTop()).addClass(c+" out"+i)},t=function(){g&&d&&r(),u()},u=function(){f.css("z-index",-10),f.addClass(a.mobile.activePageClass+o),a.mobile.focusPage(f),f.height(l+k),q(),f.css("z-index",""),n||f.animationComplete(v),f.removeClass(o).addClass(c+" in"+i),n&&v()},v=function(){d||g&&r(),f.removeClass("out in reverse "+c).height(""),p(),a.mobile.window.scrollTop()!==k&&q(),h.resolve(c,e,f,g,!0)};return p(),g&&!n?s():t(),h.promise()}},e=d(),f=d(!1),g=function(){return a.mobile.getScreenHeight()*3};a.mobile.defaultTransitionHandler=e,a.mobile.transitionHandlers={"default":a.mobile.defaultTransitionHandler,sequential:e,simultaneous:f},a.mobile.transitionFallbacks={},a.mobile._maybeDegradeTransition=function(b){return b&&!a.support.cssTransform3d&&a.mobile.transitionFallbacks[b]&&(b=a.mobile.transitionFallbacks[b]),b},a.mobile.getMaxScrollForTransition=a.mobile.getMaxScrollForTransition||g}(a,this),function(a,d){function w(b){!!j&&(!j.closest("."+a.mobile.activePageClass).length||b)&&j.removeClass(a.mobile.activeBtnClass),j=null}function x(){o=!1,n.length>0&&a.mobile.changePage.apply(null,n.pop())}function B(b,c,d,e){c&&c.data("mobile-page")._trigger("beforehide",null,{nextPage:b}),b.data("mobile-page")._trigger("beforeshow",null,{prevPage:c||a("")}),a.mobile.hidePageLoadingMsg(),d=a.mobile._maybeDegradeTransition(d);var f=a.mobile.transitionHandlers[d||"default"]||a.mobile.defaultTransitionHandler,g=f(d,e,b,c);return g.done(function(){c&&c.data("mobile-page")._trigger("hide",null,{nextPage:b}),b.data("mobile-page")._trigger("show",null,{prevPage:c||a("")})}),g}function C(b,c){c&&b.attr("data-"+a.mobile.ns+"role",c),b.page()}function D(){var b=a.mobile.activePage&&F(a.mobile.activePage);return b||s.hrefNoHash}function E(a){while(a){if(typeof a.nodeName=="string"&&a.nodeName.toLowerCase()==="a")break;a=a.parentNode}return a}function F(b){var c=a(b).closest(".ui-page").jqmData("url"),d=s.hrefNoHash;if(!c||!h.isPath(c))c=d;return h.makeUrlAbsolute(c,d)}var e=a.mobile.window,f=a("html"),g=a("head"),h=a.extend(a.mobile.path,{getFilePath:function(b){var c="&"+a.mobile.subPageUrlKey;return b&&b.split(c)[0].split(p)[0]},isFirstPageUrl:function(b){var c=h.parseUrl(h.makeUrlAbsolute(b,this.documentBase)),e=c.hrefNoHash===this.documentUrl.hrefNoHash||this.documentBaseDiffers&&c.hrefNoHash===this.documentBase.hrefNoHash,f=a.mobile.firstPage,g=f&&f[0]?f[0].id:d;return e&&(!c.hash||c.hash==="#"||g&&c.hash.replace(/^#/,"")===g)},isPermittedCrossDomainRequest:function(b,c){return a.mobile.allowCrossDomainPages&&b.protocol==="file:"&&c.search(/^https?:/)!==-1}}),i=null,j=null,k=a.Deferred(),l=a.mobile.navigate.history,m="[tabindex],a,button:visible,select:visible,input",n=[],o=!1,p="&ui-state=dialog",q=g.children("base"),r=h.documentUrl,s=h.documentBase,t=h.documentBaseDiffers,u=a.mobile.getScreenHeight,v=a.support.dynamicBaseTag?{element:q.length?q:a("",{href:s.hrefNoHash}).prependTo(g),set:function(a){a=h.parseUrl(a).hrefNoHash,v.element.attr("href",h.makeUrlAbsolute(a,s))},reset:function(){v.element.attr("href",s.hrefNoSearch)}}:d;a.mobile.getDocumentUrl=h.getDocumentUrl,a.mobile.getDocumentBase=h.getDocumentBase,a.mobile.back=function(){var a=b.navigator;this.phonegapNavigationEnabled&&a&&a.app&&a.app.backHistory?a.app.backHistory():b.history.back()},a.mobile.focusPage=function(a){var b=a.find("[autofocus]"),c=a.find(".ui-title:eq(0)");if(b.length){b.focus();return}c.length?c.focus():a.focus()};var y=!0,z,A;z=function(){if(!y)return;var b=a.mobile.urlHistory.getActive();if(b){var c=e.scrollTop();b.lastScroll=c
    "),r=d.match(/]*>([^<]*)/)&&RegExp.$1,s=new RegExp("(<[^>]+\\bdata-"+a.mobile.ns+"role=[\"']?page[\"']?[^>]*>)"),t=new RegExp("\\bdata-"+a.mobile.ns+"url=[\"']?([^\"'>]*)[\"']?");s.test(d)&&RegExp.$1&&t.test(RegExp.$1)&&RegExp.$1&&(b=k=h.getFilePath(a("
    "+RegExp.$1+"
    ").text())),v&&v.set(k),p.get(0).innerHTML=d,g=p.find(":jqmData(role='page'), :jqmData(role='dialog')").first(),g.length||(g=a("
    "+(d.split(/<\/?body[^>]*>/gmi)[1]||"")+"
    ")),r&&!g.jqmData("title")&&(~r.indexOf("&")&&(r=a("
    "+r+"
    ").text()),g.jqmData("title",r));if(!a.support.dynamicBaseTag){var u=h.get(k);g.find("[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]").each(function(){var b=a(this).is("[href]")?"href":a(this).is("[src]")?"src":"action",c=a(this).attr(b);c=c.replace(location.protocol+"//"+location.host+location.pathname,""),/^(\w+:|#|\/)/.test(c)||a(this).attr(b,u+c)})}g.attr("data-"+a.mobile.ns+"url",h.convertUrlToDataUrl(k)).attr("data-"+a.mobile.ns+"external-page",!0).appendTo(f.pageContainer),g.one("pagecreate",a.mobile._bindPageRemove),C(g,f.role),j.indexOf("&"+a.mobile.subPageUrlKey)>-1&&(g=f.pageContainer.children("[data-"+a.mobile.ns+"url='"+l+"']")),f.showLoadMsg&&q(),o.xhr=n,o.textStatus=m,o.page=g,f.pageContainer.trigger("pageload",o),e.resolve(j,c,g,i)},error:function(b,d,g){v&&v.set(h.get()),o.xhr=b,o.textStatus=d,o.errorThrown=g;var i=new a.Event("pageloadfailed");f.pageContainer.trigger(i,o);if(i.isDefaultPrevented())return;f.showLoadMsg&&(q(),a.mobile.showPageLoadingMsg(a.mobile.pageLoadErrorMessageTheme,a.mobile.pageLoadErrorMessage,!0),setTimeout(a.mobile.hidePageLoadingMsg,1500)),e.reject(j,c)}}),e.promise()},a.mobile.loadPage.defaults={type:"get",data:d,reloadPage:!1,role:d,showLoadMsg:!1,pageContainer:d,loadMsgDelay:50},a.mobile.changePage=function(b,e){if(o){n.unshift(arguments);return}var f=a.extend({},a.mobile.changePage.defaults,e),g;f.pageContainer=f.pageContainer||a.mobile.pageContainer,f.fromPage=f.fromPage||a.mobile.activePage,g=typeof b=="string";var i=f.pageContainer,j=new a.Event("pagebeforechange"),k={toPage:b,options:f};g?k.absUrl=h.makeUrlAbsolute(b,D()):k.absUrl=b.data("absUrl"),i.trigger(j,k);if(j.isDefaultPrevented())return;b=k.toPage,g=typeof b=="string",o=!0;if(g){f.target=b,a.mobile.loadPage(b,f).done(function(b,c,d,e){o=!1,c.duplicateCachedPage=e,d.data("absUrl",k.absUrl),a.mobile.changePage(d,c)}).fail(function(a,b){o=!1,w(!0),x(),f.pageContainer.trigger("pagechangefailed",k)});return}b[0]===a.mobile.firstPage[0]&&!f.dataUrl&&(f.dataUrl=r.hrefNoHash);var m=f.fromPage,q=f.dataUrl&&h.convertUrlToDataUrl(f.dataUrl)||b.jqmData("url"),s=q,t=h.getFilePath(q),u=l.getActive(),v=l.activeIndex===0,y=0,z=c.title,A=f.role==="dialog"||b.jqmData("role")==="dialog";if(m&&m[0]===b[0]&&!f.allowSamePageTransition){o=!1,i.trigger("pagechange",k),f.fromHashChange&&l.direct({url:q});return}C(b,f.role),f.fromHashChange&&(y=e.direction==="back"?-1:1);try{c.activeElement&&c.activeElement.nodeName.toLowerCase()!=="body"?a(c.activeElement).blur():a("input:focus, textarea:focus, select:focus").blur()}catch(E){}var F=!1;A&&u&&(u.url&&u.url.indexOf(p)>-1&&a.mobile.activePage&&!a.mobile.activePage.is(".ui-dialog")&&l.activeIndex>0&&(f.changeHash=!1,F=!0),q=u.url||"",!F&&q.indexOf("#")>-1?q+=p:q+="#"+p,l.activeIndex===0&&q===l.initialDst&&(q+=p));var G=u?b.jqmData("title")||b.children(":jqmData(role='header')").find(".ui-title").getEncodedText():z;!!G&&z===c.title&&(z=G),b.jqmData("title")||b.jqmData("title",z),f.transition=f.transition||(y&&!v?u.transition:d)||(A?a.mobile.defaultDialogTransition:a.mobile.defaultPageTransition),!y&&F&&(l.getActive().pageUrl=s);if(q&&!f.fromHashChange){var H;!h.isPath(q)&&q.indexOf("#")<0&&(q="#"+q),H={transition:f.transition,title:z,pageUrl:s,role:f.role},f.changeHash!==!1&&a.mobile.hashListeningEnabled?a.mobile.navigate(q,H,!0):b[0]!==a.mobile.firstPage[0]&&a.mobile.navigate.history.add(q,H)}c.title=z,a.mobile.activePage=b,f.reverse=f.reverse||y<0,B(b,m,f.transition,f.reverse).done(function(c,d,e,g,h){w(),f.duplicateCachedPage&&f.duplicateCachedPage.remove(),h||a.mobile.focusPage(b),x(),i.trigger("pagechange",k)})},a.mobile.changePage.defaults={transition:d,reverse:!1,changeHash:!0,fromHashChange:!1,role:d,duplicateCachedPage:d,pageContainer:d,showLoadMsg:!0,dataUrl:d,fromPage:d,allowSamePageTransition:!1},a.mobile.navreadyDeferred=a.Deferred(),a.mobile._registerInternalEvents=function(){var c=function(b,c){var d,e,f,g=!0,j,k;return!a.mobile.ajaxEnabled||b.is(":jqmData(ajax='false')")||!b.jqmHijackable().length?!1:(e=b.attr("target"),f=b.attr("action"),f||(f=F(b),f===s.hrefNoHash&&(f=r.hrefNoSearch)),f=h.makeUrlAbsolute(f,F(b)),h.isExternal(f)&&!h.isPermittedCrossDomainRequest(r,f)||e?!1:(c||(d=b.attr("method"),j=b.serializeArray(),i&&i[0].form===b[0]&&(k=i.attr("name"),k&&(a.each(j,function(a,b){if(b.name===k)return k="",!1}),k&&j.push({name:k,value:i.attr("value")}))),g={url:f,options:{type:d&&d.length&&d.toLowerCase()||"get",data:a.param(j),transition:b.jqmData("transition"),reverse:b.jqmData("direction")==="reverse",reloadPage:!0}}),g))};a.mobile.document.delegate("form","submit",function(b){var d=c(a(this));d&&(a.mobile.changePage(d.url,d.options),b.preventDefault())}),a.mobile.document.bind("vclick",function(b){var d,e,f=b.target,g=!1;if(b.which>1||!a.mobile.linkBindingEnabled)return;i=a(f);if(a.data(f,"mobile-button")){if(!c(a(f).closest("form"),!0))return;f.parentNode&&(f=f.parentNode)}else{f=E(f);if(!f||h.parseUrl(f.getAttribute("href")||"#").hash==="#")return;if(!a(f).jqmHijackable().length)return}~f.className.indexOf("ui-link-inherit")?f.parentNode&&(e=a.data(f.parentNode,"buttonElements")):e=a.data(f,"buttonElements"),e?f=e.outer:g=!0,d=a(f),g&&(d=d.closest(".ui-btn")),d.length>0&&!d.hasClass("ui-disabled")&&(w(!0),j=d,j.addClass(a.mobile.activeBtnClass))}),a.mobile.document.bind("click",function(c){if(!a.mobile.linkBindingEnabled||c.isDefaultPrevented())return;var e=E(c.target),f=a(e),g;if(!e||c.which>1||!f.jqmHijackable().length)return;g=function(){b.setTimeout(function(){w(!0)},200)};if(f.is(":jqmData(rel='back')"))return a.mobile.back(),!1;var i=F(f),j=h.makeUrlAbsolute(f.attr("href")||"#",i);if(!a.mobile.ajaxEnabled&&!h.isEmbeddedPage(j)){g();return}if(j.search("#")!==-1){j=j.replace(/[^#]*#/,"");if(!j){c.preventDefault();return}h.isPath(j)?j=h.makeUrlAbsolute(j,i):j=h.makeUrlAbsolute("#"+j,r.hrefNoHash)}var k=f.is("[rel='external']")||f.is(":jqmData(ajax='false')")||f.is("[target]"),l=k||h.isExternal(j)&&!h.isPermittedCrossDomainRequest(r,j);if(l){g();return}var m=f.jqmData("transition"),n=f.jqmData("direction")==="reverse"||f.jqmData("back"),o=f.attr("data-"+a.mobile.ns+"rel")||d;a.mobile.changePage(j,{transition:m,reverse:n,role:o,link:f}),c.preventDefault()}),a.mobile.document.delegate(".ui-page","pageshow.prefetch",function(){var b=[];a(this).find("a:jqmData(prefetch)").each(function(){var c=a(this),d=c.attr("href");d&&a.inArray(d,b)===-1&&(b.push(d),a.mobile.loadPage(d,{role:c.attr("data-"+a.mobile.ns+"rel")}))})}),a.mobile._handleHashChange=function(c,e){var f=h.stripHash(c),g=a.mobile.urlHistory.stack.length===0?"none":d,i={changeHash:!1,fromHashChange:!0,reverse:e.direction==="back"};a.extend(i,e,{transition:(l.getLast()||{}).transition||g});if(l.activeIndex>0&&f.indexOf(p)>-1&&l.initialDst!==f){if(a.mobile.activePage&&!a.mobile.activePage.is(".ui-dialog")){e.direction==="back"?a.mobile.back():b.history.forward();return}f=e.pageUrl;var j=a.mobile.urlHistory.getActive();a.extend(i,{role:j.role,transition:j.transition,reverse:e.direction==="back"})}f?(f=h.isPath(f)?f:h.makeUrlAbsolute("#"+f,s),f===h.makeUrlAbsolute("#"+l.initialDst,s)&&l.stack.length&&l.stack[0].url!==l.initialDst.replace(p,"")&&(f=a.mobile.firstPage),a.mobile.changePage(f,i)):a.mobile.changePage(a.mobile.firstPage,i)},e.bind("navigate",function(b,c){var d=a.event.special.navigate.originalEventName.indexOf("hashchange")>-1?c.state.hash:c.state.url;d||(d=a.mobile.path.parseLocation().hash);if(!d||d==="#"||d.indexOf("#"+a.mobile.path.uiStateKey)===0)d=location.href;a.mobile._handleHashChange(d,c.state)}),a.mobile.document.bind("pageshow",a.mobile.resetActivePageHeight),a.mobile.window.bind("throttledresize",a.mobile.resetActivePageHeight)},a(function(){k.resolve()}),a.when(k,a.mobile.navreadyDeferred).done(function(){a.mobile._registerInternalEvents()})}(a),function(a,b,c){a.mobile.transitionFallbacks.flip="fade"}(a,this),function(a,b,c){a.mobile.transitionFallbacks.flow="fade"}(a,this),function(a,b,c){a.mobile.transitionFallbacks.pop="fade"}(a,this),function(a,b,c){a.mobile.transitionHandlers.slide=a.mobile.transitionHandlers.simultaneous,a.mobile.transitionFallbacks.slide="fade"}(a,this),function(a,b,c){a.mobile.transitionFallbacks.slidedown="fade"}(a,this),function(a,b,c){a.mobile.transitionFallbacks.slidefade="fade"}(a,this),function(a,b,c){a.mobile.transitionFallbacks.slideup="fade"}(a,this),function(a,b,c){a.mobile.transitionFallbacks.turn="fade"}(a,this),function(a,b){a.mobile.page.prototype.options.degradeInputs={color:!1,date:!1,datetime:!1,"datetime-local":!1,email:!1,month:!1,number:!1,range:"number",search:"text",tel:!1,time:!1,url:!1,week:!1},a.mobile.document.bind("pagecreate create",function(b){var c=a.mobile.closestPageData(a(b.target)),d;if(!c)return;d=c.options,a(b.target).find("input").not(c.keepNativeSelector()).each(function(){var b=a(this),c=this.getAttribute("type"),e=d.degradeInputs[c]||"text";if(d.degradeInputs[c]){var f=a("
    ").html(b.clone()).html(),g=f.indexOf(" type=")>-1,h=g?/\s+type=["']?\w+['"]?/:/\/?>/,i=' type="'+e+'" data-'+a.mobile.ns+'type="'+c+'"'+(g?"":">");b.replaceWith(f.replace(h,i))}})})}(a),function(a,b,c){a.widget("mobile.dialog",a.mobile.widget,{options:{closeBtn:"left",closeBtnText:"Close",overlayTheme:"a",corners:!0,initSelector:":jqmData(role='dialog')"},_handlePageBeforeShow:function(){this._isCloseable=!0,this.options.overlayTheme&&this.element.page("removeContainerBackground").page("setContainerBackground",this.options.overlayTheme)},_create:function(){var b=this,c=this.element,d=this.options.corners?" ui-corner-all":"",e=a("
    ",{role:"dialog","class":"ui-dialog-contain ui-overlay-shadow"+d});c.addClass("ui-dialog ui-overlay-"+this.options.overlayTheme),c.wrapInner(e),c.bind("vclick submit",function(b){var c=a(b.target).closest(b.type==="vclick"?"a":"form"),d;c.length&&!c.jqmData("transition")&&(d=a.mobile.urlHistory.getActive()||{},c.attr("data-"+a.mobile.ns+"transition",d.transition||a.mobile.defaultDialogTransition).attr("data-"+a.mobile.ns+"direction","reverse"))}),this._on(c,{pagebeforeshow:"_handlePageBeforeShow"}),a.extend(this,{_createComplete:!1}),this._setCloseBtn(this.options.closeBtn)},_setCloseBtn:function(b){var c=this,d,e;this._headerCloseButton&&(this._headerCloseButton.remove(),this._headerCloseButton=null),b!=="none"&&(e=b==="left"?"left":"right",d=a(""+this.options.closeBtnText+""),this.element.children().find(":jqmData(role='header')").first().prepend(d),this._createComplete&&a.fn.buttonMarkup&&d.buttonMarkup(),this._createComplete=!0,d.bind("click",function(){c.close()}),this._headerCloseButton=d)},_setOption:function(b,c){b==="closeBtn"&&(this._setCloseBtn(c),this._super(b,c),this.element.attr("data-"+(a.mobile.ns||"")+"close-btn",c))},close:function(){var b,c,d=a.mobile.navigate.history;this._isCloseable&&(this._isCloseable=!1,a.mobile.hashListeningEnabled&&d.activeIndex>0?a.mobile.back():(b=Math.max(0,d.activeIndex-1),c=d.stack[b].pageUrl||d.stack[b].url,d.previousIndex=d.activeIndex,d.activeIndex=b,a.mobile.path.isPath(c)||(c=a.mobile.path.makeUrlAbsolute("#"+c)),a.mobile.changePage(c,{direction:"back",changeHash:!1,fromHashChange:!0})))}}),a.mobile.document.delegate(a.mobile.dialog.prototype.options.initSelector,"pagecreate",function(){a.mobile.dialog.prototype.enhance(this)})}(a,this),function(a,b){a.mobile.page.prototype.options.backBtnText="Back",a.mobile.page.prototype.options.addBackBtn=!1,a.mobile.page.prototype.options.backBtnTheme=null,a.mobile.page.prototype.options.headerTheme="a",a.mobile.page.prototype.options.footerTheme="a",a.mobile.page.prototype.options.contentTheme=null,a.mobile.document.bind("pagecreate",function(b){var c=a(b.target),d=c.data("mobile-page").options,e=c.jqmData("role"),f=d.theme;a(":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')",c).jqmEnhanceable().each(function(){var b=a(this),g=b.jqmData("role"),h=b.jqmData("theme"),i=h||d.contentTheme||e==="dialog"&&f,j,k,l,m;b.addClass("ui-"+g);if(g==="header"||g==="footer"){var n=h||(g==="header"?d.headerTheme:d.footerTheme)||f;b.addClass("ui-bar-"+n).attr("role",g==="header"?"banner":"contentinfo"),g==="header"&&(j=b.children("a, button"),k=j.hasClass("ui-btn-left"),l=j.hasClass("ui-btn-right"),k=k||j.eq(0).not(".ui-btn-right").addClass("ui-btn-left").length,l=l||j.eq(1).addClass("ui-btn-right").length),d.addBackBtn&&g==="header"&&a(".ui-page").length>1&&c.jqmData("url")!==a.mobile.path.stripHash(location.hash)&&!k&&(m=a(""+d.backBtnText+"").attr("data-"+a.mobile.ns+"theme",d.backBtnTheme||n).prependTo(b)),b.children("h1, h2, h3, h4, h5, h6").addClass("ui-title").attr({role:"heading","aria-level":"1"})}else g==="content"&&(i&&b.addClass("ui-body-"+i),b.attr("role","main"))})})}(a),function(a,b){a.mobile.behaviors.addFirstLastClasses={_getVisibles:function(a,b){var c;return b?c=a.not(".ui-screen-hidden"):(c=a.filter(":visible"),c.length===0&&(c=a.not(".ui-screen-hidden"))),c},_addFirstLastClasses:function(a,b,c){a.removeClass("ui-first-child ui-last-child"),b.eq(0).addClass("ui-first-child").end().last().addClass("ui-last-child"),c||this.element.trigger("updatelayout")}}}(a),function(a,b){a.fn.fieldcontain=function(a){return this.addClass("ui-field-contain ui-body ui-br").contents().filter(function(){return this.nodeType===3&&!/\S/.test(this.nodeValue)}).remove()},a(c).bind("pagecreate create",function(b){a(":jqmData(role='fieldcontain')",b.target).jqmEnhanceable().fieldcontain()})}(a),function(a,b){a.fn.grid=function(b){return this.each(function(){var c=a(this),d=a.extend({grid:null},b),e=c.children(),f={solo:1,a:2,b:3,c:4,d:5},g=d.grid,h;if(!g)if(e.length<=5)for(var i in f)f[i]===e.length&&(g=i);else g="a",c.addClass("ui-grid-duo");h=f[g],c.addClass("ui-grid-"+g),e.filter(":nth-child("+h+"n+1)").addClass("ui-block-a"),h>1&&e.filter(":nth-child("+h+"n+2)").addClass("ui-block-b"),h>2&&e.filter(":nth-child("+h+"n+3)").addClass("ui-block-c"),h>3&&e.filter(":nth-child("+h+"n+4)").addClass("ui-block-d"),h>4&&e.filter(":nth-child("+h+"n+5)").addClass("ui-block-e")})}}(a),function(a,b){a(c).bind("pagecreate create",function(b){a(":jqmData(role='nojs')",b.target).addClass("ui-nojs")})}(a),function(a,b){a.mobile.behaviors.formReset={_handleFormReset:function(){this._on(this.element.closest("form"),{reset:function(){this._delay("_reset")}})}}}(a),function(a,b){function e(a){var b;while(a){b=typeof a.className=="string"&&a.className+" ";if(b&&b.indexOf("ui-btn ")>-1&&b.indexOf("ui-disabled ")<0)break;a=a.parentNode}return a}function f(d,e,f,g,h){var i=a.data(d[0],"buttonElements");d.removeClass(e).addClass(f),i&&(i.bcls=a(c.createElement("div")).addClass(i.bcls+" "+f).removeClass(e).attr("class"),g!==b&&(i.hover=g),i.state=h)}var d=function(a,c){var d=a.getAttribute(c);return d==="true"?!0:d==="false"?!1:d===null?b:d};a.fn.buttonMarkup=function(e){var f=this,h="data-"+a.mobile.ns,i;e=e&&a.type(e)==="object"?e:{};for(var j=0;j a, .ui-header > a, .ui-footer > a, .ui-bar > :jqmData(role='controlgroup') > a",b.target).jqmEnhanceable().not("button, input, .ui-btn, :jqmData(role='none'), :jqmData(role='nojs')").buttonMarkup()})}(a),function(a,b){a.widget("mobile.collapsible",a.mobile.widget,{options:{expandCueText:" click to expand contents",collapseCueText:" click to collapse contents",collapsed:!0,heading:"h1,h2,h3,h4,h5,h6,legend",collapsedIcon:"plus",expandedIcon:"minus",iconpos:"left",theme:null,contentTheme:null,inset:!0,corners:!0,mini:!1,initSelector:":jqmData(role='collapsible')"},_create:function(){var c=this.element,d=this.options,e=c.addClass("ui-collapsible"),f=c.children(d.heading).first(),g=e.wrapInner("
    ").children(".ui-collapsible-content"),h=c.closest(":jqmData(role='collapsible-set')").addClass("ui-collapsible-set"),i="";f.is("legend")&&(f=a("
    "+f.html()+"
    ").insertBefore(f),f.next().remove()),h.length?(d.theme||(d.theme=h.jqmData("theme")||a.mobile.getInheritedTheme(h,"c")),d.contentTheme||(d.contentTheme=h.jqmData("content-theme")),d.collapsedIcon=c.jqmData("collapsed-icon")||h.jqmData("collapsed-icon")||d.collapsedIcon,d.expandedIcon=c.jqmData("expanded-icon")||h.jqmData("expanded-icon")||d.expandedIcon,d.iconpos=c.jqmData("iconpos")||h.jqmData("iconpos")||d.iconpos,h.jqmData("inset")!==b?d.inset=h.jqmData("inset"):d.inset=!0,d.corners=!1,d.mini||(d.mini=h.jqmData("mini"))):d.theme||(d.theme=a.mobile.getInheritedTheme(c,"c")),!d.inset||(i+=" ui-collapsible-inset",!d.corners||(i+=" ui-corner-all")),d.contentTheme&&(i+=" ui-collapsible-themed-content",g.addClass("ui-body-"+d.contentTheme)),i!==""&&e.addClass(i),f.insertBefore(g).addClass("ui-collapsible-heading").append("").wrapInner("").find("a").first().buttonMarkup({shadow:!1,corners:!1,iconpos:d.iconpos,icon:d.collapsedIcon,mini:d.mini,theme:d.theme}),e.bind("expand collapse",function(b){if(!b.isDefaultPrevented()){var c=a(this),e=b.type==="collapse";b.preventDefault(),f.toggleClass("ui-collapsible-heading-collapsed",e).find(".ui-collapsible-heading-status").text(e?d.expandCueText:d.collapseCueText).end().find(".ui-icon").toggleClass("ui-icon-"+d.expandedIcon,!e).toggleClass("ui-icon-"+d.collapsedIcon,e||d.expandedIcon===d.collapsedIcon).end().find("a").first().removeClass(a.mobile.activeBtnClass),c.toggleClass("ui-collapsible-collapsed",e),g.toggleClass("ui-collapsible-content-collapsed",e).attr("aria-hidden",e),g.trigger("updatelayout")}}).trigger(d.collapsed?"collapse":"expand"),f.bind("tap",function(b){f.find("a").first().addClass(a.mobile.activeBtnClass)}).bind("click",function(a){var b=f.is(".ui-collapsible-heading-collapsed")?"expand":"collapse";e.trigger(b),a.preventDefault(),a.stopPropagation()})}}),a.mobile.document.bind("pagecreate create",function(b){a.mobile.collapsible.prototype.enhanceWithin(b.target)})}(a),function(a,b){a.widget("mobile.collapsibleset",a.mobile.widget,{options:{initSelector:":jqmData(role='collapsible-set')"},_create:function(){var c=this.element.addClass("ui-collapsible-set"),d=this.options;d.theme||(d.theme=a.mobile.getInheritedTheme(c,"c")),d.contentTheme||(d.contentTheme=c.jqmData("content-theme")),d.corners||(d.corners=c.jqmData("corners")),c.jqmData("inset")!==b&&(d.inset=c.jqmData("inset")),d.inset=d.inset!==b?d.inset:!0,d.corners=d.corners!==b?d.corners:!0,!!d.corners&&!!d.inset&&c.addClass("ui-corner-all"),c.jqmData("collapsiblebound")||c.jqmData("collapsiblebound",!0).bind("expand",function(b){var c=a(b.target).closest(".ui-collapsible");c.parent().is(":jqmData(role='collapsible-set')")&&c.siblings(".ui-collapsible").trigger("collapse")})},_init:function(){var a=this.element,b=a.children(":jqmData(role='collapsible')"),c=b.filter(":jqmData(collapsed='false')");this._refresh("true"),c.trigger("expand")},_refresh:function(b){var c=this.element.children(":jqmData(role='collapsible')");a.mobile.collapsible.prototype.enhance(c.not(".ui-collapsible")),this._addFirstLastClasses(c,this._getVisibles(c,b),b)},refresh:function(){this._refresh(!1)}}),a.widget("mobile.collapsibleset",a.mobile.collapsibleset,a.mobile.behaviors.addFirstLastClasses),a.mobile.document.bind("pagecreate create",function(b){a.mobile.collapsibleset.prototype.enhanceWithin(b.target)})}(a),function(a,b){a.widget("mobile.navbar",a.mobile.widget,{options:{iconpos:"top",grid:null,initSelector:":jqmData(role='navbar')"},_create:function(){var d=this.element,e=d.find("a"),f=e.filter(":jqmData(icon)").length?this.options.iconpos:b;d.addClass("ui-navbar ui-mini").attr("role","navigation").find("ul").jqmEnhanceable().grid({grid:this.options.grid}),e.buttonMarkup({corners:!1,shadow:!1,inline:!0,iconpos:f}),d.delegate("a","vclick",function(b){if(!a(b.target).hasClass("ui-disabled")){e.removeClass(a.mobile.activeBtnClass),a(this).addClass(a.mobile.activeBtnClass);var d=a(this);a(c).one("pagechange",function(b){d.removeClass(a.mobile.activeBtnClass)})}}),d.closest(".ui-page").bind("pagebeforeshow",function(){e.filter(".ui-state-persist").addClass(a.mobile.activeBtnClass)})}}),a.mobile.document.bind("pagecreate create",function(b){a.mobile.navbar.prototype.enhanceWithin(b.target)})}(a),function(a,b){var d={};a.widget("mobile.listview",a.mobile.widget,{options:{theme:null,countTheme:"c",headerTheme:"b",dividerTheme:"b",icon:"arrow-r",splitIcon:"arrow-r",splitTheme:"b",corners:!0,shadow:!0,inset:!1,initSelector:":jqmData(role='listview')"},_create:function(){var a=this,b="";b+=a.options.inset?" ui-listview-inset":"",!a.options.inset||(b+=a.options.corners?" ui-corner-all":"",b+=a.options.shadow?" ui-shadow":""),a.element.addClass(function(a,c){return c+" ui-listview"+b}),a.refresh(!0)},_findFirstElementByTagName:function(a,b,c,d){var e={};e[c]=e[d]=!0;while(a){if(e[a.nodeName])return a;a=a[b]}return null},_getChildrenByTagName:function(b,c,d){var e=[],f={};f[c]=f[d]=!0,b=b.firstChild;while(b)f[b.nodeName]&&e.push(b),b=b.nextSibling;return a(e)},_addThumbClasses:function(b){var c,d,e=b.length;for(c=0;c1||z===!1?!1:z||j||d.icon,theme:r}),z!==!1&&s.length===1&&p.addClass("ui-li-has-arrow"),s.first().removeClass("ui-link").addClass("ui-link-inherit"),s.length>1&&(q+=" ui-li-has-alt",t=s.last(),u=h||t.jqmData("theme")||d.splitTheme,C=t.jqmData("icon"),t.appendTo(p).attr("title",a.trim(t.getEncodedText())).addClass("ui-li-link-alt").empty().buttonMarkup({shadow:!1,corners:!1,theme:r,icon:!1,iconpos:"notext"}).find(".ui-btn-inner").append(a(c.createElement("span")).buttonMarkup({shadow:!0,corners:!0,theme:u,iconpos:"notext",icon:C||z||i||d.splitIcon})))):F?(q+=" ui-li-divider ui-bar-"+(p.jqmData("theme")||g),p.attr("role","heading"),l&&(n||n===0?m?v=parseInt(n,10):(x=parseInt(n,10)-1,p.css("counter-reset","listnumbering "+x)):m&&(v=1))):q+=" ui-li-static ui-btn-up-"+r}l&&m&&q.indexOf("ui-li-divider")<0&&(y=q.indexOf("ui-li-static")>0?p:p.find(".ui-link-inherit"),y.addClass("ui-li-jsnumbering").prepend(""+v++ +". ")),o[q]||(o[q]=[]),o[q].push(p[0])}for(q in o)a(o[q]).addClass(q).children(".ui-btn-inner").addClass(q);e.find("h1, h2, h3, h4, h5, h6").addClass("ui-li-heading").end().find("p, dl").addClass("ui-li-desc").end().find(".ui-li-aside").each(function(){var b=a(this);b.prependTo(b.parent())}).end().find(".ui-li-count").each(function(){a(this).closest("li").addClass("ui-li-has-count")}).addClass("ui-btn-up-"+(e.jqmData("counttheme")||this.options.countTheme)+" ui-btn-corner-all"),this._addThumbClasses(k),this._addThumbClasses(e.find(".ui-link-inherit")),this._addFirstLastClasses(k,this._getVisibles(k,b),b),this._trigger("afterrefresh")},_idStringEscape:function(a){return a.replace(/[^a-zA-Z0-9]/g,"-")},_createSubPages:function(){var b=this.element,c=b.closest(".ui-page"),e=c.jqmData("url"),f=e||c[0][a.expando],g=b.attr("id"),h=this.options,i="data-"+a.mobile.ns,j=this,k=c.find(":jqmData(role='footer')").jqmData("id"),l;typeof d[f]=="undefined"&&(d[f]=-1),g=g||++d[f],a(b.find("li>ul, li>ol").toArray().reverse()).each(function(c){var d=this,f=a(this),j=f.attr("id")||g+"-"+c,m=f.parent(),n=a(f.prevAll().toArray().reverse()),p=n.length?n:a(""+a.trim(m.contents()[0].nodeValue)+""),q=p.first().getEncodedText(),r=(e||"")+"&"+a.mobile.subPageUrlKey+"="+j,s=f.jqmData("theme")||h.theme,t=f.jqmData("counttheme")||b.jqmData("counttheme")||h.countTheme,u,v;l=!0,u=f.detach().wrap("
    ").parent().before("
    "+q+"
    ").after(k?a("
    "):"").parent().appendTo(a.mobile.pageContainer),u.page(),v=m.find("a:first"),v.length||(v=a("").html(p||q).prependTo(m.empty())),v.attr("href","#"+r)}).listview();if(l&&c.is(":jqmData(external-page='true')")&&c.data("mobile-page").options.domCache===!1){var m=function(b,d){var f=d.nextPage,g,h=new a.Event("pageremove");d.nextPage&&(g=f.jqmData("url"),g.indexOf(e+"&"+a.mobile.subPageUrlKey)!==0&&(j.childPages().remove(),c.trigger(h),h.isDefaultPrevented()||c.removeWithDependents()))};c.unbind("pagehide.remove").bind("pagehide.remove",m)}},childPages:function(){var b=this.parentPage.jqmData("url");return a(":jqmData(url^='"+b+"&"+a.mobile.subPageUrlKey+"')")}}),a.widget("mobile.listview",a.mobile.listview,a.mobile.behaviors.addFirstLastClasses),a.mobile.document.bind("pagecreate create",function(b){a.mobile.listview.prototype.enhanceWithin(b.target)})}(a),function(a,b){a.mobile.listview.prototype.options.autodividers=!1,a.mobile.listview.prototype.options.autodividersSelector=function(b){var c=a.trim(b.text())||null;return c?(c=c.slice(0,1).toUpperCase(),c):null},a.mobile.document.delegate("ul,ol","listviewcreate",function(){var b=a(this),d=b.data("mobile-listview");if(!d||!d.options.autodividers)return;var e=function(){b.find("li:jqmData(role='list-divider')").remove();var e=b.find("li"),f=null,g,h;for(var i=0;i
    ")[b.html()?"html":"text"](b.html()||b.val()).insertBefore(b).buttonMarkup(d).addClass(e).append(b.addClass("ui-btn-hidden")),c=this.button,b.bind({focus:function(){c.addClass(a.mobile.focusClass)},blur:function(){c.removeClass(a.mobile.focusClass)}}),this.refresh()},_setOption:function(b,c){var d={};d[b]=c,b!=="initSelector"&&(this.button.buttonMarkup(d),this.element.attr("data-"+(a.mobile.ns||"")+b.replace(/([A-Z])/,"-$1").toLowerCase(),c)),this._super("_setOption",b,c)},enable:function(){return this.element.attr("disabled",!1),this.button.removeClass("ui-disabled").attr("aria-disabled",!1),this._setOption("disabled",!1)},disable:function(){return this.element.attr("disabled",!0),this.button.addClass("ui-disabled").attr("aria-disabled",!0),this._setOption("disabled",!0)},refresh:function(){var b=this.element;b.prop("disabled")?this.disable():this.enable(),a(this.button.data("buttonElements").text)[b.html()?"html":"text"](b.html()||b.val())}}),a.mobile.document.bind("pagecreate create",function(b){a.mobile.button.prototype.enhanceWithin(b.target,!0)})}(a),function(a,b){a.widget("mobile.controlgroup",a.mobile.widget,{options:{shadow:!1,corners:!0,excludeInvisible:!0,type:"vertical",mini:!1,initSelector:":jqmData(role='controlgroup')"},_create:function(){var c=this.element,d={inner:a("
    "),legend:a("
    ")},e=c.children("legend"),f=this;c.wrapInner(d.inner),e.length&&d.legend.append(e).insertBefore(c.children(0)),c.addClass("ui-corner-all ui-controlgroup"),a.extend(this,{_initialRefresh:!0}),a.each(this.options,function(a,c){f.options[a]=b,f._setOption(a,c,!0)})},_init:function(){this.refresh()},_setOption:function(c,d){var e="_set"+c.charAt(0).toUpperCase()+c.slice(1);this[e]!==b&&this[e](d),this._super(c,d),this.element.attr("data-"+(a.mobile.ns||"")+c.replace(/([A-Z])/,"-$1").toLowerCase(),d)},_setType:function(a){this.element.removeClass("ui-controlgroup-horizontal ui-controlgroup-vertical").addClass("ui-controlgroup-"+a),this.refresh()},_setCorners:function(a){this.element.toggleClass("ui-corner-all",a)},_setShadow:function(a){this.element.toggleClass("ui-shadow",a)},_setMini:function(a){this.element.toggleClass("ui-mini",a)},container:function(){return this.element.children(".ui-controlgroup-controls")},refresh:function(){var b=this.element.find(".ui-btn").not(".ui-slider-handle"),c=this._initialRefresh;a.mobile.checkboxradio&&this.element.find(":mobile-checkboxradio").checkboxradio("refresh"),this._addFirstLastClasses(b,this.options.excludeInvisible?this._getVisibles(b,c):b,c),this._initialRefresh=!1}}),a.widget("mobile.controlgroup",a.mobile.controlgroup,a.mobile.behaviors.addFirstLastClasses),a(function(){a.mobile.document.bind("pagecreate create",function(b){a.mobile.controlgroup.prototype.enhanceWithin(b.target,!0)})})}(a),function(a,b){a(c).bind("pagecreate create",function(b){a(b.target).find("a").jqmEnhanceable().not(".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')").addClass("ui-link")})}(a),function(a,d){function e(a,b,c,d){var e=d;return athis._ui.screen.height()&&this._ui.screen.height(a)},_handleWindowKeyUp:function(b){if(this._isOpen&&b.keyCode===a.mobile.keyCode.ESCAPE)return this._eatEventAndClose(b)},_expectResizeEvent:function(){var b=f();if(this._resizeData){if(b.x===this._resizeData.winCoords.x&&b.y===this._resizeData.winCoords.y&&b.cx===this._resizeData.winCoords.cx&&b.cy===this._resizeData.winCoords.cy)return!1;clearTimeout(this._resizeData.timeoutId)}return this._resizeData={timeoutId:setTimeout(a.proxy(this,"_resizeTimeout"),200),winCoords:b},!0},_resizeTimeout:function(){this._isOpen?this._expectResizeEvent()||(this._ui.container.hasClass("ui-popup-hidden")&&(this._ui.container.removeClass("ui-popup-hidden"),this.reposition({positionTo:"window"}),this._ignoreResizeEvents()),this._resizeScreen(),this._resizeData=null,this._orientationchangeInProgress=!1):(this._resizeData=null,this._orientationchangeInProgress=!1)},_ignoreResizeEvents:function(){var a=this;this._ignoreResizeTo&&clearTimeout(this._ignoreResizeTo),this._ignoreResizeTo=setTimeout(function(){a._ignoreResizeTo=0},1e3)},_handleWindowResize:function(a){this._isOpen&&this._ignoreResizeTo===0&&(this._expectResizeEvent()||this._orientationchangeInProgress)&&!this._ui.container.hasClass("ui-popup-hidden")&&this._ui.container.addClass("ui-popup-hidden").removeAttr("style")},_handleWindowOrientationchange:function(a){!this._orientationchangeInProgress&&this._isOpen&&this._ignoreResizeTo===0&&(this._expectResizeEvent(),this._orientationchangeInProgress=!0)},_handleDocumentFocusIn:function(b){var d=b.target,e,f=this._ui;if(!this._isOpen)return;if(d!==f.container[0]){e=a(b.target);if(0===e.parents().filter(f.container[0]).length)return a(c.activeElement).one("focus",function(a){e.blur()}),f.focusElement.focus(),b.preventDefault(),b.stopImmediatePropagation(),!1;f.focusElement[0]===f.container[0]&&(f.focusElement=e)}else f.focusElement&&f.focusElement[0]!==f.container[0]&&(f.container.blur(),f.focusElement.focus());this._ignoreResizeEvents()},_create:function(){var b={screen:a("
    "),placeholder:a("
    "),container:a("
    ")},c=this.element.closest(".ui-page"),e=this.element.attr("id"),f=this;this.options.history=this.options.history&&a.mobile.ajaxEnabled&&a.mobile.hashListeningEnabled,c.length===0&&(c=a("body")),this.options.container=this.options.container||a.mobile.pageContainer,c.append(b.screen),b.container.insertAfter(b.screen),b.placeholder.insertAfter(this.element),e&&(b.screen.attr("id",e+"-screen"),b.container.attr("id",e+"-popup"),b.placeholder.html("")),b.container.append(this.element),b.focusElement=b.container,this.element.addClass("ui-popup"),a.extend(this,{_scrollTop:0,_page:c,_ui:b,_fallbackTransition:"",_currentTransition:!1,_prereqs:null,_isOpen:!1,_tolerance:null,_resizeData:null,_ignoreResizeTo:0,_orientationchangeInProgress:!1}),a.each(this.options,function(a,b){f.options[a]=d,f._setOption(a,b,!0)}),b.screen.bind("vclick",a.proxy(this,"_eatEventAndClose")),this._on(a.mobile.window,{orientationchange:a.proxy(this,"_handleWindowOrientationchange"),resize:a.proxy(this,"_handleWindowResize"),keyup:a.proxy(this,"_handleWindowKeyUp")}),this._on(a.mobile.document,{focusin:a.proxy(this,"_handleDocumentFocusIn")})},_applyTheme:function(a,b,c){var d=(a.attr("class")||"").split(" "),e=!0,f=null,g,h=String(b);while(d.length>0){f=d.pop(),g=(new RegExp("^ui-"+c+"-([a-z])$")).exec(f);if(g&&g.length>1){f=g[1];break}f=null}b!==f&&(a.removeClass("ui-"+c+"-"+f),b!==null&&b!=="none"&&a.addClass("ui-"+c+"-"+h))},_setTheme:function(a){this._applyTheme(this.element,a,"body")},_setOverlayTheme:function(a){this._applyTheme(this._ui.screen,a,"overlay"),this._isOpen&&this._ui.screen.addClass("in")},_setShadow:function(a){this.element.toggleClass("ui-overlay-shadow",a)},_setCorners:function(a){this.element.toggleClass("ui-corner-all",a)},_applyTransition:function(b){this._ui.container.removeClass(this._fallbackTransition),b&&b!=="none"&&(this._fallbackTransition=a.mobile._maybeDegradeTransition(b),this._fallbackTransition==="none"&&(this._fallbackTransition=""),this._ui.container.addClass(this._fallbackTransition))},_setTransition:function(a){this._currentTransition||this._applyTransition(a)},_setTolerance:function(b){var c={t:30,r:15,b:30,l:15};if(b!==d){var e=String(b).split(",");a.each(e,function(a,b){e[a]=parseInt(b,10)});switch(e.length){case 1:isNaN(e[0])||(c.t=c.r=c.b=c.l=e[0]);break;case 2:isNaN(e[0])||(c.t=c.b=e[0]),isNaN(e[1])||(c.l=c.r=e[1]);break;case 4:isNaN(e[0])||(c.t=e[0]),isNaN(e[1])||(c.r=e[1]),isNaN(e[2])||(c.b=e[2]),isNaN(e[3])||(c.l=e[3]);break;default:}}this._tolerance=c},_setOption:function(b,c){var e,f="_set"+b.charAt(0).toUpperCase()+b.slice(1);this[f]!==d&&this[f](c),e=["initSelector","closeLinkSelector","closeLinkEvents","navigateEvents","closeEvents","history","container"],a.mobile.widget.prototype._setOption.apply(this,arguments),a.inArray(b,e)===-1&&this.element.attr("data-"+(a.mobile.ns||"")+b.replace(/([A-Z])/,"-$1").toLowerCase(),c)},_placementCoords:function(a){var b=f(),d={x:this._tolerance.l,y:b.y+this._tolerance.t,cx:b.cx-this._tolerance.l-this._tolerance.r,cy:b.cy-this._tolerance.t-this._tolerance.b},g,h;this._ui.container.css("max-width",d.cx),g={cx:this._ui.container.outerWidth(!0),cy:this._ui.container.outerHeight(!0)},h={x:e(d.cx,g.cx,d.x,a.x),y:e(d.cy,g.cy,d.y,a.y)},h.y=Math.max(0,h.y);var i=c.documentElement,j=c.body,k=Math.max(i.clientHeight,j.scrollHeight,j.offsetHeight,i.scrollHeight,i.offsetHeight);return h.y-=Math.min(h.y,Math.max(0,h.y+g.cy-k)),{left:h.x,top:h.y}},_createPrereqs:function(b,c,d){var e=this,f;f={screen:a.Deferred(),container:a.Deferred()},f.screen.then(function(){f===e._prereqs&&b()}),f.container.then(function(){f===e._prereqs&&c()}),a.when(f.screen,f.container).done(function(){f===e._prereqs&&(e._prereqs=null,d())}),e._prereqs=f},_animate:function(b){this._ui.screen.removeClass(b.classToRemove).addClass(b.screenClassToAdd),b.prereqs.screen.resolve();if(b.transition&&b.transition!=="none"){b.applyTransition&&this._applyTransition(b.transition);if(this._fallbackTransition){this._ui.container.animationComplete(a.proxy(b.prereqs.container,"resolve")).addClass(b.containerClassToAdd).removeClass(b.classToRemove);return}}this._ui.container.removeClass(b.classToRemove),b.prereqs.container.resolve()},_desiredCoords:function(b){var c=null,d,e=f(),g=b.x,h=b.y,i=b.positionTo;if(i&&i!=="origin")if(i==="window")g=e.cx/2+e.x,h=e.cy/2+e.y;else{try{c=a(i)}catch(j){c=null}c&&(c.filter(":visible"),c.length===0&&(c=null))}c&&(d=c.offset(),g=d.left+c.outerWidth()/2,h=d.top+c.outerHeight()/2);if(a.type(g)!=="number"||isNaN(g))g=e.cx/2+e.x;if(a.type(h)!=="number"||isNaN(h))h=e.cy/2+e.y;return{x:g,y:h}},_reposition:function(a){a={x:a.x,y:a.y,positionTo:a.positionTo},this._trigger("beforeposition",a),this._ui.container.offset(this._placementCoords(this._desiredCoords(a)))},reposition:function(a){this._isOpen&&this._reposition(a)},_openPrereqsComplete:function(){this._ui.container.addClass("ui-popup-active"),this._isOpen=!0,this._resizeScreen(),this._ui.container.attr("tabindex","0").focus(),this._ignoreResizeEvents(),this._trigger("afteropen")},_open:function(c){var d=a.extend({},this.options,c),e=function(){var a=b,c=navigator.userAgent,d=c.match(/AppleWebKit\/([0-9\.]+)/),e=!!d&&d[1],f=c.match(/Android (\d+(?:\.\d+))/),g=!!f&&f[1],h=c.indexOf("Chrome")>-1;return f!==null&&g==="4.0"&&e&&e>534.13&&!h?!0:!1}();this._createPrereqs(a.noop,a.noop,a.proxy(this,"_openPrereqsComplete")),this._currentTransition=d.transition,this._applyTransition(d.transition),this.options.theme||this._setTheme(this._page.jqmData("theme")||a.mobile.getInheritedTheme(this._page,"c")),this._ui.screen.removeClass("ui-screen-hidden"),this._ui.container.removeClass("ui-popup-hidden"),this._reposition(d),this.options.overlayTheme&&e&&this.element.closest(".ui-page").addClass("ui-popup-open"),this._animate({additionalCondition:!0,transition:d.transition,classToRemove:"",screenClassToAdd:"in",containerClassToAdd:"in",applyTransition:!1,prereqs:this._prereqs})},_closePrereqScreen:function(){this._ui.screen.removeClass("out").addClass("ui-screen-hidden")},_closePrereqContainer:function(){this._ui.container.removeClass("reverse out").addClass("ui-popup-hidden").removeAttr("style")},_closePrereqsDone:function(){var b=this.options;this._ui.container.removeAttr("tabindex"),a.mobile.popup.active=d,this._trigger("afterclose")},_close:function(b){this._ui.container.removeClass("ui-popup-active"),this._page.removeClass("ui-popup-open"),this._isOpen=!1,this._createPrereqs(a.proxy(this,"_closePrereqScreen"),a.proxy(this,"_closePrereqContainer"),a.proxy(this,"_closePrereqsDone")),this._animate({additionalCondition:this._ui.screen.hasClass("in"),transition:b?"none":this._currentTransition,classToRemove:"in",screenClassToAdd:"out",containerClassToAdd:"reverse out",applyTransition:!0,prereqs:this._prereqs})},_unenhance:function(){this._setTheme("none"),this.element.detach().insertAfter(this._ui.placeholder).removeClass("ui-popup ui-overlay-shadow ui-corner-all"),this._ui.screen.remove(),this._ui.container.remove(),this._ui.placeholder.remove()},_destroy:function(){a.mobile.popup.active===this?(this.element.one("popupafterclose",a.proxy(this,"_unenhance")),this.close()):this._unenhance()},_closePopup:function(c,d){var e,f,g=this.options,h=!1;b.scrollTo(0,this._scrollTop),c&&c.type==="pagebeforechange"&&d&&(typeof d.toPage=="string"?e=d.toPage:e=d.toPage.jqmData("url"),e=a.mobile.path.parseUrl(e),f=e.pathname+e.search+e.hash,this._myUrl!==a.mobile.path.makeUrlAbsolute(f)?h=!0:c.preventDefault()),g.container.unbind(g.closeEvents),this.element.undelegate(g.closeLinkSelector,g.closeLinkEvents),this._close(h)},_bindContainerClose:function(){this.options.container.one(this.options.closeEvents,a.proxy(this,"_closePopup"))},open:function(c){var d=this,e=this.options,f,g,h,i,j,k;if(a.mobile.popup.active)return;a.mobile.popup.active=this,this._scrollTop=a.mobile.window.scrollTop();if(!e.history){d._open(c),d._bindContainerClose(),d.element.delegate(e.closeLinkSelector,e.closeLinkEvents,function(a){d.close(),a.preventDefault()});return}k=a.mobile.urlHistory,g=a.mobile.dialogHashKey,h=a.mobile.activePage,i=h.is(".ui-dialog"),this._myUrl=f=k.getActive().url,j=f.indexOf(g)>-1&&!i&&k.activeIndex>0;if(j){d._open(c),d._bindContainerClose();return}f.indexOf(g)===-1&&!i?f=f+(f.indexOf("#")>-1?g:"#"+g):f=a.mobile.path.parseLocation().hash+g,k.activeIndex===0&&f===k.initialDst&&(f+=g),a(b).one("beforenavigate",function(a){a.preventDefault(),d._open(c),d._bindContainerClose()}),this.urlAltered=!0,a.mobile.navigate(f,{role:"dialog"})},close:function(){if(a.mobile.popup.active!==this)return;this._scrollTop=a.mobile.window.scrollTop(),this.options.history&&this.urlAltered?(a.mobile.back(),this.urlAltered=!1):this._closePopup()}}),a.mobile.popup.handleLink=function(b){var c=b.closest(":jqmData(role='page')"),d=c.length===0?a("body"):c,e=a(a.mobile.path.parseUrl(b.attr("href")).hash,d[0]),f;e.data("mobile-popup")&&(f=b.offset(),e.popup("open",{x:f.left+b.outerWidth()/2,y:f.top+b.outerHeight()/2,transition:b.jqmData("transition"),positionTo:b.jqmData("position-to")})),setTimeout(function(){var c=b.parent().parent();c.hasClass("ui-li")&&(b=c.parent()),b.removeClass(a.mobile.activeBtnClass)},300)},a.mobile.document.bind("pagebeforechange",function(b,c){c.options.role==="popup"&&(a.mobile.popup.handleLink(c.options.link),b.preventDefault())}),a.mobile.document.bind("pagecreate create",function(b){a.mobile.popup.prototype.enhanceWithin(b.target,!0)})}(a),function(a,d){a.widget("mobile.panel",a.mobile.widget,{options:{classes:{panel:"ui-panel",panelOpen:"ui-panel-open",panelClosed:"ui-panel-closed",panelFixed:"ui-panel-fixed",panelInner:"ui-panel-inner",modal:"ui-panel-dismiss",modalOpen:"ui-panel-dismiss-open",pagePanel:"ui-page-panel",pagePanelOpen:"ui-page-panel-open",contentWrap:"ui-panel-content-wrap",contentWrapOpen:"ui-panel-content-wrap-open",contentWrapClosed:"ui-panel-content-wrap-closed",contentFixedToolbar:"ui-panel-content-fixed-toolbar",contentFixedToolbarOpen:"ui-panel-content-fixed-toolbar-open",contentFixedToolbarClosed:"ui-panel-content-fixed-toolbar-closed",animate:"ui-panel-animate"},animate:!0,theme:"c",position:"left",dismissible:!0,display:"reveal",initSelector:":jqmData(role='panel')",swipeClose:!0,positionFixed:!1},_panelID:null,_closeLink:null,_page:null,_modal:null,_pannelInner:null,_wrapper:null,_fixedToolbar:null,_create:function(){var b=this,c=b.element,d=c.closest(":jqmData(role='page')"),e=function(){var b=a.data(d[0],"mobilePage").options.theme,c="ui-body-"+b;return c},f=function(){var a=c.find("."+b.options.classes.panelInner);return a.length===0&&(a=c.children().wrapAll('
    ').parent()),a},g=function(){var c=d.find("."+b.options.classes.contentWrap);return c.length===0&&(c=d.children(".ui-header:not(:jqmData(position='fixed')), .ui-content:not(:jqmData(role='popup')), .ui-footer:not(:jqmData(position='fixed'))").wrapAll('
    ').parent(),a.support.cssTransform3d&&!!b.options.animate&&c.addClass(b.options.classes.animate)),c},h=function(){var c=d.find("."+b.options.classes.contentFixedToolbar);return c.length===0&&(c=d.find(".ui-header:jqmData(position='fixed'), .ui-footer:jqmData(position='fixed')").addClass(b.options.classes.contentFixedToolbar),a.support.cssTransform3d&&!!b.options.animate&&c.addClass(b.options.classes.animate)),c};a.extend(this,{_panelID:c.attr("id"),_closeLink:c.find(":jqmData(rel='close')"),_page:c.closest(":jqmData(role='page')"),_pageTheme:e(),_pannelInner:f(),_wrapper:g(),_fixedToolbar:h()}),b._addPanelClasses(),b._wrapper.addClass(this.options.classes.contentWrapClosed),b._fixedToolbar.addClass(this.options.classes.contentFixedToolbarClosed),b._page.addClass(b.options.classes.pagePanel),a.support.cssTransform3d&&!!b.options.animate&&this.element.addClass(b.options.classes.animate),b._bindUpdateLayout(),b._bindCloseEvents(),b._bindLinkListeners(),b._bindPageEvents(),!b.options.dismissible||b._createModal(),b._bindSwipeEvents()},_createModal:function(b){var c=this;c._modal=a("
    ").on("mousedown",function(){c.close()}).appendTo(this._page)},_getPosDisplayClasses:function(a){return a+"-position-"+this.options.position+" "+a+"-display-"+this.options.display},_getPanelClasses:function(){var a=this.options.classes.panel+" "+this._getPosDisplayClasses(this.options.classes.panel)+" "+this.options.classes.panelClosed;return this.options.theme&&(a+=" ui-body-"+this.options.theme),!this.options.positionFixed||(a+=" "+this.options.classes.panelFixed),a},_addPanelClasses:function(){this.element.addClass(this._getPanelClasses())},_bindCloseEvents:function(){var a=this;a._closeLink.on("click.panel",function(b){return b.preventDefault(),a.close(),!1}),a.element.on("click.panel","a:jqmData(ajax='false')",function(b){a.close()})},_positionPanel:function(){var b=this,c=b._pannelInner.outerHeight(),d=c>a.mobile.getScreenHeight();d||!b.options.positionFixed?(d&&(b._unfixPanel(),a.mobile.resetActivePageHeight(c)),b._scrollIntoView(c)):b._fixPanel()},_scrollIntoView:function(c){c"+d.columnBtnText+"
    "),h=a("
    "),i=a("
    ");c.headers.not("td").each(function(){var b=a(this).jqmData("priority"),c=a(this).add(a(this).jqmData("cells"));b&&(c.addClass(d.classes.priorityPrefix+b),a("").appendTo(i).children(0).jqmData("cells",c).checkboxradio({theme:d.columnPopupTheme}))}),i.appendTo(h),i.on("change","input",function(b){this.checked?a(this).jqmData("cells").removeClass("ui-table-cell-hidden").addClass("ui-table-cell-visible"):a(this).jqmData("cells").removeClass("ui-table-cell-visible").addClass("ui-table-cell-hidden")}),g.insertBefore(b).buttonMarkup({theme:d.columnBtnTheme}),h.insertBefore(b).popup(),c.refresh=function(){i.find("input").each(function(){this.checked=a(this).jqmData("cells").eq(0).css("display")==="table-cell",a(this).checkboxradio("refresh")})},a.mobile.window.on("throttledresize",c.refresh),c.refresh()})}(a),function(a,b){a.mobile.table.prototype.options.mode="reflow",a.mobile.table.prototype.options.classes=a.extend(a.mobile.table.prototype.options.classes,{reflowTable:"ui-table-reflow",cellLabels:"ui-table-cell-label"}),a.mobile.document.delegate(":jqmData(role='table')","tablecreate",function(){var b=a(this),c=b.data("mobile-table"),d=c.options;if(d.mode!=="reflow")return;c.element.addClass(d.classes.reflowTable);var e=a(c.allHeaders.get().reverse());e.each(function(b){var c=a(this).jqmData("cells"),e=a(this).jqmData("colstart"),f=c.not(this).filter("thead th").length&&" ui-table-cell-label-top",g=a(this).text();if(g!=="")if(f){var h=parseInt(a(this).attr("colspan"),10),i="";h&&(i="td:nth-child("+h+"n + "+e+")"),c.filter(i).prepend(""+g+"")}else c.prepend(""+g+"")})})}(a),function(a){var b=a("meta[name=viewport]"),c=b.attr("content"),d=c+",maximum-scale=1, user-scalable=no",e=c+",maximum-scale=10, user-scalable=yes",f=/(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test(c);a.mobile.zoom=a.extend({},{enabled:!f,locked:!1,disable:function(c){!f&&!a.mobile.zoom.locked&&(b.attr("content",d),a.mobile.zoom.enabled=!1,a.mobile.zoom.locked=c||!1)},enable:function(c){!f&&(!a.mobile.zoom.locked||c===!0)&&(b.attr("content",e),a.mobile.zoom.enabled=!0,a.mobile.zoom.locked=!1)},restore:function(){f||(b.attr("content",c),a.mobile.zoom.enabled=!0)}})}(a),function(a,b){a.widget("mobile.textinput",a.mobile.widget,{options:{theme:null,mini:!1,preventFocusZoom:/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>-1,initSelector:"input[type='text'], input[type='search'], :jqmData(type='search'), input[type='number'], :jqmData(type='number'), input[type='password'], input[type='email'], input[type='url'], input[type='tel'], textarea, input[type='time'], input[type='date'], input[type='month'], input[type='week'], input[type='datetime'], input[type='datetime-local'], input[type='color'], input:not([type]), input[type='file']",clearBtn:!1,clearSearchButtonText:null,clearBtnText:"clear text",disabled:!1},_create:function(){function o(){setTimeout(function(){j.toggleClass("ui-input-clear-hidden",!c.val())},0)}var b=this,c=this.element,d=this.options,e=d.theme||a.mobile.getInheritedTheme(this.element,"c"),f=" ui-body-"+e,g=d.mini?" ui-mini":"",h=c.is("[type='search'], :jqmData(type='search')"),i,j,k=d.clearSearchButtonText||d.clearBtnText,l=c.is("textarea, :jqmData(type='range')"),m=!!d.clearBtn&&!l,n=c.is("input")&&!c.is(":jqmData(type='range')");a("label[for='"+c.attr("id")+"']").addClass("ui-input-text"),i=c.addClass("ui-input-text ui-body-"+e),typeof c[0].autocorrect!="undefined"&&!a.support.touchOverflow&&(c[0].setAttribute("autocorrect","off"),c[0].setAttribute("autocomplete","off")),h?i=c.wrap("").parent():n&&(i=c.wrap("
    ").parent()),m||h?(j=a(""+k+"").bind("click",function(a){c.val("").focus().trigger("change"),j.addClass("ui-input-clear-hidden"),a.preventDefault()}).appendTo(i).buttonMarkup({icon:"delete",iconpos:"notext",corners:!0,shadow:!0,mini:d.mini}),h||i.addClass("ui-input-has-clear"),o(),c.bind("paste cut keyup input focus change blur",o)):!n&&!h&&c.addClass("ui-corner-all ui-shadow-inset"+f+g),c.focus(function(){d.preventFocusZoom&&a.mobile.zoom.disable(!0),i.addClass(a.mobile.focusClass)}).blur(function(){i.removeClass(a.mobile.focusClass),d.preventFocusZoom&&a.mobile.zoom.enable(!0)});if(c.is("textarea")){var p=15,q=100,r;this._keyup=function(){var a=c[0].scrollHeight,b=c[0].clientHeight;b",{"class":"ui-listview-filter ui-bar-"+d.options.filterTheme,role:"search"}).submit(function(a){a.preventDefault(),g.blur()}),f=function(e){var f=a(this),g=this.value.toLowerCase(),h=null,i=b.children(),j=f.jqmData("lastval")+"",k=!1,l="",m,n=d.options.filterCallback!==c;if(j&&j===g)return;d._trigger("beforefilter","beforefilter",{input:this}),f.jqmData("lastval",g),n||g.length=0;o--)m=a(h[o]),l=m.jqmData("filtertext")||m.text(),m.is("li:jqmData(role=list-divider)")?(m.toggleClass("ui-filter-hidequeue",!k),k=!1):d.options.filterCallback(l,g,m)?m.toggleClass("ui-filter-hidequeue",!0):k=!0;h.filter(":not(.ui-filter-hidequeue)").toggleClass("ui-screen-hidden",!1),h.filter(".ui-filter-hidequeue").toggleClass("ui-screen-hidden",!0).toggleClass("ui-filter-hidequeue",!1)}else h.toggleClass("ui-screen-hidden",!!d.options.filterReveal);d._addFirstLastClasses(i,d._getVisibles(i,!1),!1)},g=a("",{placeholder:d.options.filterPlaceholder}).attr("data-"+a.mobile.ns+"type","search").jqmData("lastval","").bind("keyup change input",f).appendTo(e).textinput();d.options.inset&&e.addClass("ui-listview-filter-inset"),e.bind("submit",function(){return!1}).insertBefore(b)})}(a),function(a,d){a.widget("mobile.slider",a.mobile.widget,{widgetEventPrefix:"slide",options:{theme:null,trackTheme:null,disabled:!1,initSelector:"input[type='range'], :jqmData(type='range'), :jqmData(role='slider')",mini:!1,highlight:!1},_create:function(){var e=this,f=this.element,g=a.mobile.getInheritedTheme(f,"c"),h=this.options.theme||g,i=this.options.trackTheme||g,j=f[0].nodeName.toLowerCase(),k=this.isToggleSwitch=j==="select",l=f.parent().is(":jqmData(role='rangeslider')"),m=this.isToggleSwitch?"ui-slider-switch":"",n=f.attr("id"),o=a("[for='"+n+"']"),p=o.attr("id")||n+"-label",q=o.attr("id",p),r=this.isToggleSwitch?0:parseFloat(f.attr("min")),s=this.isToggleSwitch?f.find("option").length-1:parseFloat(f.attr("max")),t=b.parseFloat(f.attr("step")||1),u=this.options.mini||f.jqmData("mini")?" ui-mini":"",v=c.createElement("a"),w=a(v),x=c.createElement("div"),y=a(x),z=this.options.highlight&&!this.isToggleSwitch?function(){var b=c.createElement("div");return b.className="ui-slider-bg "+a.mobile.activeBtnClass+" ui-btn-corner-all",a(b).prependTo(y)}():!1,A;v.setAttribute("href","#"),x.setAttribute("role","application"),x.className=[this.isToggleSwitch?"ui-slider ":"ui-slider-track ",m," ui-btn-down-",i," ui-btn-corner-all",u].join(""),v.className="ui-slider-handle",x.appendChild(v),w.buttonMarkup({corners:!0,theme:h,shadow:!0}).attr({role:"slider","aria-valuemin":r,"aria-valuemax":s,"aria-valuenow":this._value(),"aria-valuetext":this._value(),title:this._value(),"aria-labelledby":p}),a.extend(this,{slider:y,handle:w,type:j,step:t,max:s,min:r,valuebg:z,isRangeslider:l,dragging:!1,beforeStart:null,userModified:!1,mouseMoved:!1});if(this.isToggleSwitch){var B=c.createElement("div");B.className="ui-slider-inneroffset";for(var C=0,D=x.childNodes.length;C":"
    ";f.add(y).wrapAll(B)}this.isToggleSwitch&&this.handle.bind({focus:function(){y.addClass(a.mobile.focusClass)},blur:function(){y.removeClass(a.mobile.focusClass)}}),this._on(this.handle,{vmousedown:"_handleVMouseDown",keydown:"_handleKeydown",keyup:"_handleKeyup"}),this.handle.bind("vclick",!1),this._handleFormReset&&this._handleFormReset(),this.refresh(d,d,!0)},_controlChange:function(a){if(this._trigger("controlchange",a)===!1)return!1;this.mouseMoved||this.refresh(this._value(),!0)},_controlKeyup:function(a){this.refresh(this._value(),!0,!0)},_controlBlur:function(a){this.refresh(this._value(),!0)},_controlVMouseUp:function(a){this._checkedRefresh()},_handleVMouseDown:function(a){this.handle.focus()},_handleKeydown:function(b){var c=this._value();if(this.options.disabled)return;switch(b.keyCode){case a.mobile.keyCode.HOME:case a.mobile.keyCode.END:case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:b.preventDefault(),this._keySliding||(this._keySliding=!0,this.handle.addClass("ui-state-active"))}switch(b.keyCode){case a.mobile.keyCode.HOME:this.refresh(this.min);break;case a.mobile.keyCode.END:this.refresh(this.max);break;case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:this.refresh(c+this.step);break;case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:this.refresh(c-this.step)}},_handleKeyup:function(a){this._keySliding&&(this._keySliding=!1,this.handle.removeClass("ui-state-active"))},_sliderVMouseDown:function(a){return this.options.disabled?!1:this._trigger("beforestart",a)===!1?!1:(this.dragging=!0,this.userModified=!1,this.mouseMoved=!1,this.isToggleSwitch&&(this.beforeStart=this.element[0].selectedIndex),this.refresh(a),this._trigger("start"),!1)},_sliderVMouseUp:function(){if(this.dragging)return this.dragging=!1,this.isToggleSwitch&&(this.handle.addClass("ui-slider-handle-snapping"),this.mouseMoved?this.userModified?this.refresh(this.beforeStart===0?1:0):this.refresh(this.beforeStart):this.refresh(this.beforeStart===0?1:0)),this.mouseMoved=!1,this._trigger("stop"),!1},_preventDocumentDrag:function(a){if(this._trigger("drag",a)===!1)return!1;if(this.dragging&&!this.options.disabled)return this.mouseMoved=!0,this.isToggleSwitch&&this.handle.removeClass("ui-slider-handle-snapping"),this.refresh(a),this.userModified=this.beforeStart!==this.element[0].selectedIndex,!1},_checkedRefresh:function(){this.value!=this._value()&&this.refresh(this._value())},_value:function(){return this.isToggleSwitch?this.element[0].selectedIndex:parseFloat(this.element.val())},_reset:function(){this.refresh(d,!1,!0)},refresh:function(b,d,e){var f=this,g=a.mobile.getInheritedTheme(this.element,"c"),h=this.options.theme||g,i=this.options.trackTheme||g;f.slider[0].className=[this.isToggleSwitch?"ui-slider ui-slider-switch":"ui-slider-track"," ui-btn-down-"+i," ui-btn-corner-all",this.options.mini?" ui-mini":""].join(""),(this.options.disabled||this.element.attr("disabled"))&&this.disable(),this.value=this._value(),this.options.highlight&&!this.isToggleSwitch&&this.slider.find(".ui-slider-bg").length===0&&(this.valuebg=function(){var b=c.createElement("div");return b.className="ui-slider-bg "+a.mobile.activeBtnClass+" ui-btn-corner-all",a(b).prependTo(f.slider)}()),this.handle.buttonMarkup({corners:!0,theme:h,shadow:!0});var j,k,l=this.element,m=!this.isToggleSwitch,n=m?[]:l.find("option"),o=m?parseFloat(l.attr("min")):0,p=m?parseFloat(l.attr("max")):n.length-1,q=m&&parseFloat(l.attr("step"))>0?parseFloat(l.attr("step")):1;if(typeof b=="object"){var r,s,t=b,u=8;r=this.slider.offset().left,s=this.slider.width(),j=s/((p-o)/q);if(!this.dragging||t.pageXr+s+u)return;j>1?k=(t.pageX-r)/s*100:k=Math.round((t.pageX-r)/s*100)}else b==null&&(b=m?parseFloat(l.val()||0):l[0].selectedIndex),k=(parseFloat(b)-o)/(p-o)*100;if(isNaN(k))return;var v=k/100*(p-o)+o,w=(v-o)%q,x=v-w;Math.abs(w)*2>=q&&(x+=w>0?q:-q);var y=100/((p-o)/q);v=parseFloat(x.toFixed(5)),typeof j=="undefined"&&(j=s/((p-o)/q)),j>1&&m&&(k=(v-o)*y*(1/q)),k<0&&(k=0),k>100&&(k=100),vp&&(v=p),this.handle.css("left",k+"%"),this.handle[0].setAttribute("aria-valuenow",m?v:n.eq(v).attr("value")),this.handle[0].setAttribute("aria-valuetext",m?v:n.eq(v).getEncodedText()),this.handle[0].setAttribute("title",m?v:n.eq(v).getEncodedText()),this.valuebg&&this.valuebg.css("width",k+"%");if(this._labels){var z=this.handle.width()/this.slider.width()*100,A=k&&z+(100-z)*k/100,B=k===100?0:Math.min(z+100-A,100);this._labels.each(function(){var b=a(this).is(".ui-slider-label-a");a(this).width((b?A:B)+"%")})}if(!e){var C=!1;m?(C=l.val()!==v,l.val(v)):(C=l[0].selectedIndex!==v,l[0].selectedIndex=v);if(this._trigger("beforechange",b)===!1)return!1;!d&&C&&l.trigger("change")}},enable:function(){return this.element.attr("disabled",!1),this.slider.removeClass("ui-disabled").attr("aria-disabled",!1),this._setOption("disabled",!1)},disable:function(){return this.element.attr("disabled",!0),this.slider.addClass("ui-disabled").attr("aria-disabled",!0),this._setOption("disabled",!0)}}),a.widget("mobile.slider",a.mobile.slider,a.mobile.behaviors.formReset),a.mobile.document.bind("pagecreate create",function(b){a.mobile.slider.prototype.enhanceWithin(b.target,!0)})}(a),function(a,b){a.widget("mobile.rangeslider",a.mobile.widget,{options:{theme:null,trackTheme:null,disabled:!1,initSelector:":jqmData(role='rangeslider')",mini:!1,highlight:!0},_create:function(){var b,c=this.element,d=this.options.mini?"ui-rangeslider ui-mini":"ui-rangeslider",e=c.find("input").first(),f=c.find("input").last(),g=c.find("label").first(),h=a.data(e.get(0),"mobileSlider").slider,i=a.data(f.get(0),"mobileSlider").slider,j=a.data(e.get(0),"mobileSlider").handle,k=a('
    ').appendTo(c);c.find("label").length>1&&(b=c.find("label").last().hide()),e.addClass("ui-rangeslider-first"),f.addClass("ui-rangeslider-last"),c.addClass(d),h.appendTo(k),i.appendTo(k),g.prependTo(c),j.prependTo(i),a.extend(this,{_inputFirst:e,_inputLast:f,_sliderFirst:h,_sliderLast:i,_targetVal:null,_sliderTarget:!1,_sliders:k,_proxy:!1}),this.refresh(),this._on(this.element.find("input.ui-slider-input"),{slidebeforestart:"_slidebeforestart",slidestop:"_slidestop",slidedrag:"_slidedrag",slidebeforechange:"_change",blur:"_change",keyup:"_change"}),this._on(j,{vmousedown:"_dragFirstHandle"})},_dragFirstHandle:function(b){return a.data(this._inputFirst.get(0),"mobileSlider").dragging=!0,a.data(this._inputFirst.get(0),"mobileSlider").refresh(b),!1},_slidedrag:function(b){var c=a(b.target).is(this._inputFirst),d=c?this._inputLast:this._inputFirst;this._sliderTarget=!1;if(this._proxy==="first"&&c||this._proxy==="last"&&!c)return a.data(d.get(0),"mobileSlider").dragging=!0,a.data(d.get(0),"mobileSlider").refresh(b),!1},_slidestop:function(b){var c=a(b.target).is(this._inputFirst);this._proxy=!1,this.element.find("input").trigger("vmouseup"),this._sliderFirst.css("z-index",c?1:"")},_slidebeforestart:function(b){this._sliderTarget=!1,a(b.originalEvent.target).hasClass("ui-slider-track")&&(this._sliderTarget=!0,this._targetVal=a(b.target).val())},_setOption:function(a){this._superApply(a),this.refresh()},refresh:function(){var a=this.element,b=this.options;a.find("input").slider({theme:b.theme,trackTheme:b.trackTheme,disabled:b.disabled,mini:b.mini,highlight:b.highlight}).slider("refresh"),this._updateHighlight()},_change:function(b){if(b.type=="keyup")return this._updateHighlight(),!1;var c=parseFloat(this._inputFirst.val(),10),d=parseFloat(this._inputLast.val(),10),e=a(b.target).hasClass("ui-rangeslider-first"),f=e?this._inputFirst:this._inputLast,g=e?this._inputLast:this._inputFirst;if(c>d&&!this._sliderTarget)f.val(e?d:c).slider("refresh"),this._trigger("normalize");else if(c>d){f.val(this._targetVal).slider("refresh");var h=this;setTimeout(function(){g.val(e?c:d).slider("refresh"),a.data(g.get(0),"mobileSlider").handle.focus(),h._sliderFirst.css("z-index",e?"":1),h._trigger("normalize")},0),this._proxy=e?"first":"last"}c===d?(a.data(f.get(0),"mobileSlider").handle.css("z-index",1),a.data(g.get(0),"mobileSlider").handle.css("z-index",0)):(a.data(g.get(0),"mobileSlider").handle.css("z-index",""),a.data(f.get(0),"mobileSlider").handle.css("z-index","")),this._updateHighlight();if(c>=d)return!1},_updateHighlight:function(){var b=parseInt(a.data(this._inputFirst.get(0),"mobileSlider").handle.get(0).style.left,10),c=parseInt(a.data(this._inputLast.get(0),"mobileSlider").handle.get(0).style.left,10),d=c-b;this.element.find(".ui-slider-bg").css({"margin-left":b+"%",width:d+"%"})},_destroy:function(){this.element.removeClass("ui-rangeslider ui-mini").find("label").show(),this._inputFirst.after(this._sliderFirst),this._inputLast.after(this._sliderLast),this._sliders.remove(),this.element.find("input").removeClass("ui-rangeslider-first ui-rangeslider-last").slider("destroy")}}),a.widget("mobile.rangeslider",a.mobile.rangeslider,a.mobile.behaviors.formReset),a(c).bind("pagecreate create",function(b){a.mobile.rangeslider.prototype.enhanceWithin(b.target,!0)})}(a),function(a,d){a.widget("mobile.selectmenu",a.mobile.widget,{options:{theme:null,disabled:!1,icon:"arrow-d",iconpos:"right",inline:!1,corners:!0,shadow:!0,iconshadow:!0,overlayTheme:"a",dividerTheme:"b",hidePlaceholderMenuItems:!0,closeText:"Close",nativeMenu:!0,preventFocusZoom:/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>-1,initSelector:"select:not( :jqmData(role='slider') )",mini:!1},_button:function(){return a("
    ")},_setDisabled:function(a){return this.element.attr("disabled",a),this.button.attr("aria-disabled",a),this._setOption("disabled",a)},_focusButton:function(){var a=this;setTimeout(function(){a.button.focus()},40)},_selectOptions:function(){return this.select.find("option")},_preExtension:function(){var b="";!~this.element[0].className.indexOf("ui-btn-left")||(b=" ui-btn-left"),!~this.element[0].className.indexOf("ui-btn-right")||(b=" ui-btn-right"),this.select=this.element.removeClass("ui-btn-left ui-btn-right").wrap("
    "),this.selectID=this.select.attr("id"),this.label=a("label[for='"+this.selectID+"']").addClass("ui-select"),this.isMultiple=this.select[0].multiple,this.options.theme||(this.options.theme=a.mobile.getInheritedTheme(this.select,"c"))},_destroy:function(){var a=this.element.parents(".ui-select");a.length>0&&(a.is(".ui-btn-left, .ui-btn-right")&&this.element.addClass(a.is(".ui-btn-left")?"ui-btn-left":"ui-btn-right"),this.element.insertAfter(a),a.remove())},_create:function(){this._preExtension(),this._trigger("beforeCreate"),this.button=this._button();var c=this,d=this.options,e=d.inline||this.select.jqmData("inline"),f=d.mini||this.select.jqmData("mini"),g=d.icon?d.iconpos||this.select.jqmData("iconpos"):!1,h=this.select[0].selectedIndex===-1?0:this.select[0].selectedIndex,i=this.button.insertBefore(this.select).buttonMarkup({theme:d.theme,icon:d.icon,iconpos:g,inline:e,corners:d.corners,shadow:d.shadow,iconshadow:d.iconshadow,mini:f});this.setButtonText(),d.nativeMenu&&b.opera&&b.opera.version&&i.addClass("ui-select-nativeonly"),this.isMultiple&&(this.buttonCount=a("").addClass("ui-li-count ui-btn-up-c ui-btn-corner-all").hide().appendTo(i.addClass("ui-li-has-count"))),(d.disabled||this.element.attr("disabled"))&&this.disable(),this.select.change(function(){c.refresh()}),this._handleFormReset&&this._handleFormReset(),this.build()},build:function(){var b=this;this.select.appendTo(b.button).bind("vmousedown",function(){b.button.addClass(a.mobile.activeBtnClass)}).bind("focus",function(){b.button.addClass(a.mobile.focusClass)}).bind("blur",function(){b.button.removeClass(a.mobile.focusClass)}).bind("focus vmouseover",function(){b.button.trigger("vmouseover")}).bind("vmousemove",function(){b.button.removeClass(a.mobile.activeBtnClass)}).bind("change blur vmouseout",function(){b.button.trigger("vmouseout").removeClass(a.mobile.activeBtnClass)}).bind("change blur",function(){b.button.removeClass("ui-btn-down-"+b.options.theme)}),b.button.bind("vmousedown",function(){b.options.preventFocusZoom&&a.mobile.zoom.disable(!0)}),b.label.bind("click focus",function(){b.options.preventFocusZoom&&a.mobile.zoom.disable(!0)}),b.select.bind("focus",function(){b.options.preventFocusZoom&&a.mobile.zoom.disable(!0)}),b.button.bind("mouseup",function(){b.options.preventFocusZoom&&setTimeout(function(){a.mobile.zoom.enable(!0)},0)}),b.select.bind("blur",function(){b.options.preventFocusZoom&&a.mobile.zoom.enable(!0)})},selected:function(){return this._selectOptions().filter(":selected")},selectedIndices:function(){var a=this;return this.selected().map(function(){return a._selectOptions().index(this)}).get()},setButtonText:function(){var b=this,d=this.selected(),e=this.placeholder,f=a(c.createElement("span"));this.button.find(".ui-btn-text").html(function(){return d.length?e=d.map(function(){return a(this).text()}).get().join(", "):e=b.placeholder,f.text(e).addClass(b.select.attr("class")).addClass(d.attr("class"))})},setButtonCount:function(){var a=this.selected();this.isMultiple&&this.buttonCount[a.length>1?"show":"hide"]().text(a.length)},_reset:function(){this.refresh()},refresh:function(){this.setButtonText(),this.setButtonCount()},open:a.noop,close:a.noop,disable:function(){this._setDisabled(!0),this.button.addClass("ui-disabled")},enable:function(){this._setDisabled(!1),this.button.removeClass("ui-disabled")}}),a.widget("mobile.selectmenu",a.mobile.selectmenu,a.mobile.behaviors.formReset),a.mobile.document.bind("pagecreate create",function(b){a.mobile.selectmenu.prototype.enhanceWithin(b.target,!0)})}(a),function(a,b){var d=function(d){var e=d.select,f=d._destroy,g=d.selectID,h=g?g:(a.mobile.ns||"")+"uuid-"+d.uuid,i=h+"-listbox",j=h+"-dialog",k=d.label,l=d.select.closest(".ui-page"),m=d._selectOptions(),n=d.isMultiple=d.select[0].multiple,o=g+"-button",p=g+"-menu",q=a("
    "+"
    "+"
    "+k.getEncodedText()+"
    "+"
    "+"
    "+"
    "),r=a("
    ").insertAfter(d.select).popup({theme:d.options.overlayTheme}),s=a("
    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}), +d={width:c.width(),height:c.height()},e=document.activeElement;c.wrap(b);if(c[0]===e||f.contains(c[0],e))f(e).focus();b=c.parent();if(c.css("position")==="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(g,h){a[h]=c.css(h);if(isNaN(parseInt(a[h],10)))a[h]="auto"});c.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})}c.css(d);return b.css(a).show()}, +removeWrapper:function(c){var a=document.activeElement;if(c.parent().is(".ui-effects-wrapper")){c.parent().replaceWith(c);if(c[0]===a||f.contains(c[0],a))f(a).focus()}return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){var h=c.cssUnit(g);if(h[0]>0)d[g]=h[0]*b+h[1]});return d}});f.fn.extend({effect:function(){function c(h){function n(){f.isFunction(k)&&k.call(j[0]);f.isFunction(h)&&h()}var j=f(this),k=a.complete,i=a.mode;(j.is(":hidden")?i==="hide":i==="show")?n():e.call(j[0], +a,n)}var a=o.apply(this,arguments),b=a.mode,d=a.queue,e=f.effects.effect[a.effect],g=!e&&u&&f.effects[a.effect];if(f.fx.off||!(e||g))return b?this[b](a.duration,a.complete):this.each(function(){a.complete&&a.complete.call(this)});return e?d===false?this.each(c):this.queue(d||"fx",c):g.call(this,{options:a,duration:a.duration,callback:a.complete,mode:a.mode})},_show:f.fn.show,show:function(c){if(q(c))return this._show.apply(this,arguments);else{var a=o.apply(this,arguments);a.mode="show";return this.effect.call(this, +a)}},_hide:f.fn.hide,hide:function(c){if(q(c))return this._hide.apply(this,arguments);else{var a=o.apply(this,arguments);a.mode="hide";return this.effect.call(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(q(c)||typeof c==="boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=o.apply(this,arguments);a.mode="toggle";return this.effect.call(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a), +e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a, +b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2* +((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+ +b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=e*0.3,h=d;if(a==0)return b;if((a/=e)==1)return b+d;if(h + + +// Create a jquery plugin that prints the given element. +jQuery.fn.print = function(){ + // NOTE: We are trimming the jQuery collection down to the + // first element in the collection. + if (this.size() > 1){ + this.eq( 0 ).print(); + return; + } else if (!this.size()){ + return; + } + + var chart = $(this).closest('div.quintile-outer-container').find('div.jqplot-target'); + // var imgelem = chart.jqplotToImageElem(); + var imageElemStr = chart.jqplotToImageElemStr(); + // var statsrows = $(this).closest('div.quintile-outer-container').find('table.stats-table tr'); + var statsTable = $('
    ').append($(this).closest('div.quintile-outer-container').find('table.stats-table').clone()); + // var rowstyles = window.getComputedStyle(statsrows.get(0), ''); + + // ASSERT: At this point, we know that the current jQuery + // collection (as defined by THIS), contains only one + // printable element. + + // Create a random name for the print frame. + var strFrameName = ("printer-" + (new Date()).getTime()); + + // Create an iFrame with the new name. + var jFrame = $( "Close
    - - - \ No newline at end of file diff --git a/public/site_assets/test/js/dist/docs/files/changes-txt.html b/public/site_assets/test/js/dist/docs/files/changes-txt.html deleted file mode 100644 index 93e1154c..00000000 --- a/public/site_assets/test/js/dist/docs/files/changes-txt.html +++ /dev/null @@ -1,39 +0,0 @@ - - -Change Log - - - - - - - - - -

    1.0.8

    • Issue #375: sortMergedLabels does not sort string labels
    • Issue #279: Groups > 3 Causes Alignment Issues
    • Issue #439: IE can’t display a customized legend in Quirks mode
    • Issue #482: “Undefined” error message when plotting a chart with no data
    • Issue #116: Don’t mix spaces and tabs for indentation
    • Issue #564: Metergauge renderer not resizable when replotting
    • Issue #409: MeterGaugeRenderer replot/redraw offsets center
    • Issue #523: Adding rectangles to Canvas Overlay plugin
    • Issue #756: jqplot.min files contain non-UTF-8 characters
    • Issue #223: fillToZero does not color negative values when crossover point is 0
    • Pull Request #23: Adding rectangles to Canvas Overlay plugin
    • Pull Request #28: Cross-over points of 0 will actually change colors
    • Pull Request #35: Don’t highlight hidden bars or show tooltips for them
    • Pull Request #41: Add dutch(nl) and svenska(sv) translations for dates
    • Add tooltip support for Pie Charts
    • Update to latest YUI compressor

    1.0.7

    • Issue #726: Bug in sprintf %p, sometimes it outputs exponential form rather than decimal
    • Issue #717: Plot’s preDrawHooks not called
    • Issue #707: Browser hangs with LogAxisRenderer when value is 0
    • Issue #695: Horizontal Bar Chart Negative Series Colors Not Working
    • Issue #670: Examples IE7, IE8 and IE9 multipleBarColors.html failure and fix
    • Issue #636: X Axis Date Renderer Single Day Not plotting
    • Issue #607: Integration issue
    • Issue #571: Decimal numbers not properly formatted
    • Issue #552: jqPlot crashes when interval too small
    • Issue #536: DateAxisRenderer invalid scaling
    • Issue #534: “decimalMark” in the “jqplot.sprintf.js”
    • Issue #529: Scientific notation on label values ending in 0
    • Issue #521: invalid JS in meterGaugeRenderer.js
    • Issue #516: Including BezierCurveRenderer plugin and initializing jqplot with no options give error
    • Issue #500: DateAxisRenderer has timezone related issues
    • Issue #452: Including ALL jqPlot plugins causes an Error
    • Issue #494: No point when use LogAxisRenderer and a point has a zero value
    • Issue #430: getIsoWeek: invalid method call
    • Issue #280: jqplot Options
    • Issue #179: Spelling/grammar
    • Pull Request #18: Implement getTop in CanvasAxisTickRenderer
    • Pull Request #21: Performance issue when drawing pointlabels with zeros/null values
    • Pull Request #24: Added suggested fix in comment #8 for issue #536
    • Pull Request #29: Removed unbalanced addition of UTC offset
    • Pull Request #33: Documentation fixes (issue #179, other changes)
    • Pull Request #34: Start of updating jqPlotOptions.txt
    • Pull Request #37: Example and suggested fix for issues #552 and issue #536
    • Pull Request #39: Fixed trailing comma which caused issues with IE7

    1.0.6

    • Add left sidebar navigation to examples
    • Update examples for jquery 1.9.1 and jquery ui 1.10.0
    • Add colorpicker.js to distribution
    • Fix some problems with examples when viewing with local file system
    • Add “minified” copyright notice for minified files, similar to jquery’s notice.
    • Pull Request #25: jqplot.sprintf.js is no longer the last file in the concatenated jquery.jqplot.js
    • Pull Request #17: Fixed bug causing custom pointLabels passed with plot data to be ignored for horizontal bar graphs.
    • Pull Request #10: Build error by invalid encoding.
    • Issue #714: handle tickColor in meterGaugeRenderer
    • Issue #519: jsDate Polish Localization

    1.0.5

    • Updated to jQuery 1.9

    1.0.0b2

    • Major improvements in memory usage: ** Merged in changes from Timo Besenruether to reuse canvas elements and improve memory performance.  ** Fixed all identifiable DOM leaks.  ** Mergged in changes from cguillot for memory improvements in IE < 9.
    • Added vertical and dashed vertical line support for canvas overlay.
    • Fixed bug where initially hidden plots would not display.
    • Fixed bug with point labels and null data points.
    • Updated to jQuery 1.6.1.
    • Improved pie slice margin calculation and fixed slice margin and pie positioning with small slices.
    • Improved bar renderer so bars always start at 0 if: ** The axis is a linear axis (not log/date).  ** There are no other line types besides bars attached to the axis.  ** The data on the axis is all >= 0.  ** The user has not specified a pad, padMin or forceTickAt0 = true option.
    • Modified tick prefix behavious so prefix no added to all ticks, even if format string is specified.
    • Fix to ensure original tick formats are applied when zooming and resetting zoom.
    • Updated auto tick format string so format adjusted when zooming.
    • Modified auto tick computation to put less ticks on small plots and more ticks on large plots.
    • Update bubble render to support gradients in IE 9.

    1.0.0b1

    • Much improved tick generation algorithm to get precise rounded tick values (Thanks Scott Prahl!).
    • Auto compute tick format string if none is provided.
    • Much better “slicing” of pie charts when using “sliceMargin” option to set a gap between the slices.
    • Expanded canvasOverlay plugin to create arbitrary dashed and solid horizontal and vertical lines on top of plot.
    • Added defaultColors and defaultNegativeColors options to $.jqplot.config.
    • Fixed issue #318, highlighter & bar renderer incompatability.
    • Improve highlighter tooltip positioning with negative bars.
    • Fixed #305, mispelling of jqlotDragStart and jqlotDragStop.  MUST NOW BIND TO jqplotDragStart and jqplotDragStop.
    • Fixed #290, some variables left in global scope.
    • Fixed #289, OHLC line widths hard coded at 1.5.  Now set by lineWidth option.
    • Fixed #296 for determining databounds on log axes.
    • Updated to jQuery 1.5.1
    • Fixed waterfall plot to ensure first and last bars always fill to zero.
    • Added lineJoin and lineCap option to series lines.
    • Bar widths now based on width of grid, not plot target for better scaling.
    • Added looseZoom option to cursor so zooming can produce well rounded ticks.
    • Added forceTickAt0 and forceTickAt100 options to ensure there will always be a tick at 0 or 100 in the plot.
    • Fixed bug where cursor legend didn’t honor series showLabel option.

    1.0.0a

    • Series can now be moved forward or backward in stack to e.g. bring a line forward when mousing over a point.
    • Can now move outside of grid area while zooming.  Can have zoom constrained to grid area or allow zooming outside.
    • Fixed issue #142 with tooltip drawn on top of event canvas, hiding mouse events.
    • Fixed #147 where pie slices with 0 value not rendering properly in IE.
    • Fixed #130 where stack data not sorted properly.
    • Fixed bug with null values not handled properly in category axes.
    • Fixed #156 where pie charts not rendering on QTWebKit.
    • Now using feature detection for canvas and canvas text capability rather than browser version.
    • Added enahncedLegendRenderer plugin to allow multi row/column legends and clickable labels to show/hide series.
    • Added fillToValue option to allow filled line plot to fill to an arbitrary value.
    • Added block plot plugin.
    • Added funnel type charts.
    • Added meter gauge type charts.
    • Added plot theming support.
    • $.jqplot.config.enablePlugins now false by default.
    • Implemented highlighting on bar, pie, donut, funnel, etc. charts.
    • Fix to pointlabels plugin to align labels properly on multi series plots.
    • Added custom error handling to display error message in plot area.
    • Fixed issue where would call to draw grid border of 0 width would result in a default border being drawn.
    • Added options to place legend outside of grid and shrink grid so everything stays within plot div.
    • Fixed bug in color generator so now calls to get() continually cycle through colors just like next().
    • Added defaultAxisStart option.
    • Added gradient fills to bubbles.
    • Added bubble charts.
    • Added showLabels option to bubble charts.
    • Pass bubble radius to event callback in bubble charts.
    • Fixed #207, typo in docs.
    • Fixed #206 where “value” pie slice data labels were displaying wrong value.
    • Fixed #147 with 0 value slices in IE6.
    • Fixed issue #241, disabled varyBarColor option in stacked charts.
    • Added dataRenderer option to allow custom processors for JSON, AJAX and anywhere else you might want to get data.
    • Fixed null value handling so plot now properly skip or join over nulls.
    • Fixed showTicks and showTickMarks option conflicts.
    • Fixed issue #185 where pointLabels plugin incompatibility could crash pie, donut and other plots.
    • Fixed #23 and #143 to obey gridPadding option.
    • Fixed #233 with highlighter tooltip separator.
    • Fixed #224 where type checking failing on GWT.
    • Fixed #272 with pie highlighting not working on replot.
    • Memory performance improvements.
    • Changes to build script so everything should build when pulled from repo.
    • Fixed issue #275, IE 6/7 don’t support array indexing of strings.
    • Added event listener hooks for mouseUp, mouseDown, etc. to all line plots.
    • Fixed bug with highlighter not working when null in data.
    • Updated to jQuery 1.4.4
    • Fixed bug where donut plots showed value of radians of slice instead of actual data.
    • Reverted to excanvas r3 so IE8 no longer has to emulate IE7.
    • Added tooltipContentEditor option to highlighter, allowing callback to manipulate tooltip content at run time (thanks Tim Bunce!).
    • Fixed bug where axes scale not resetting.
    • Fixed bug with date axes where data bounds not properly set.
    • Fixed issue where tick marks disappear if grid lines turned off.
    • Updated replot method to allow passing in axes options for more control.
    • Added experimental support for “broken” axes.
    • Fixed bug with pies where pies with 0 valued slices did not draw correctly.
    • Added canvasOverlay plugin to allow drawing of arbitrary shapes on a canvas over the plot.
    • Added option to display arbitrary text/html (message, animated gif, etc.) if plot is constructed without data.  Allow a “data loading” indicator to be shown.
    • Added resetAxisValues method to manually update axis ticks without redrawing the plot.
    • Fix to labels on negative bars so label postiion of ‘n’ will be below a negative bar, just as it is above a positive bar (thanks guigod!).
    • Added thousands separator character (‘) to sprintf formatting (thanks yuichi1004!).
    • Re-factored date parsing/formatting to use new jsDate module which does not extend the Date prototype.

    0.9.7

    • Added Mekko chart plot type with enhanced legend and axes support.
    • Implemented vertical waterfall charts.  Can create waterfall plot as option to bar chart.  See examples folder of distribution.
    • Enhanced plot labels for waterfall style.
    • Enhanced bar plots so you can now color each bar of a series independently with the “varyBarColor” option.
    • Re-factored series drawing so that each series and series shadow drawn on its own canvas.  Allows series to be redrawn independently of each other.
    • Added additional default series colors.
    • Added useNegativeColors option to turn off negative color array and use only seriesColors array to define all bar/filled line colors.
    • Fix css for cursor legend.
    • Modified shape renderer so rectangles can be stroked and filled.
    • Re-factored date methods out of dateAxisRenderer so that date formatter and methods can be accesses outside of dateAxisRenderer plugin.
    • Fixed #132, now trigger series change event on plot target instead of drag canvas.
    • Fixes issue #116 where some source files had mix of tabs and spaces for indentation.  Should have been all spaces.
    • Fixed issue #126, some links broken in docs section of web site.
    • Fixed issue #90, trendline plugin incompatibility with pie renderer.
    • Updated samples in examples folder of distribution to include navigation links if web server is set up to process .html files with php.

    0.9.6

    • New, easier to use, replot() method for placing plots in tabs, accordions, resizable containers or for changing plot parameters programmatically.
    • Updated legend renderer for pie charts to draw swatches which will print correctly.
    • Fixed issue #118 with patch from taum so autoscale option will honor tickInterval and numberTicks options
    • Fix to plot diameter calculation for initially hidden plots.
    • Added examples for making plots in jQuery UI tabs and accordions.
    • Fixed issue #120 where pie chart with single slice not displaying correctly in IE and Chrome

    0.9.5.2

    • Fixed #102 where double clicking on plot that has zoom enabled, but has not been zoomed resulted in error.
    • Fixed bug where candlestick coloring options not working.
    • Added option to turn individual series labels off in the legend.

    0.9.5.1

    • Fixed bug where tooltip not working with OHLC and candlestick charts.
    • Added additional marker styles: plus, X and dash.

    0.9.5

    • Implemented “zoomProxy”.  zoomProxy allows zooming one plot from another such as an overview plot.
    • Zooming can now be constrained to just x or y axis.
    • Enhanced cursor plugin with vertical “dataTracking” line.  This is a line at the cursor location with a readout of data points at the line location which are displayed in the chart legend.
    • Changed cursor tooltip format string.  Now one format string is used for entire tooltip.
    • Added mechanisms to specify plot size when plot target is hidden or plot height/width otherwise cannot be determined from markup.
    • Added $.jqplot.config object to specify jqplot wide configuration options.  These include enablePlugins to globally set the default plugin state on/off and defaultHeight/defaultWidth to specify default plot height/width.
    • Added fillToZero option which forces filled charts to fill to zero as opposed to axis minimum.  Thus negative filled bar/line values will fill upwards to zero axis value.
    • Added option to disable stacking on individual lines.
    • Changed targetId property of the plot object so it now includes a “#” before the id string.
    • Improved tick and body sizing of Open Hi Low Close and candlestick charts.
    • Removed lots of web site related files from the repository.  This means that, if working from the sources, user’s won’t be able to build the jqplot web site and the docs/tests that are hosted on that site.  The minified and compressed distribution packages will build fine.
    • Lots of examples were added to a separate examples directory to better show functionality of jqPlot for local testing with the distribution.
    • Many various bug fixes and other minor enhancements.

    0.9.4

    • Implemented axis labels.  Labels can be rendered in div tags or as canvas elements supporting rotated text.
    • Improved rotated axis label positioning so labels will start or end at a tick position.
    • Fixed bug where an empty data series would hang plot rendering.
    • completed issue #66 for misc. improvements to documentation.
    • Fixed issue #64 where the same ID’s were assigned to cursor and highlighter elements.
    • Added option to legend to encode special HTML characters.
    • Fixed undesirable behavior where point labels for points off the plot were being rendered.
    • Added edgeTolerance option to point label renderer to control rendering of labels near plot edges.

    0.9.3

    • Preliminary support for axis labels.  Currently rendered into DIV tags, so no rotated label support.  This feature is currently experimental.
    • Fixed bug #52, needed space in tick div tag between style and class declarations or plot failed in certain application doctypes.
    • Fixed issue #54, miter style line join for chart lines causing spikes at steep changes in slope.  Changed miter style to round.
    • Added examples for new autoscaling algorithm.
    • Fixed bug #57, category axis labels disappear on redraw()
    • Improved algorithm which controlled maximum number of labels that would display on a category axis.
    • Fixed bug #45 where null values causing errors in plotData and gridData.
    • Fixed issue #60 where seriesColors option was not working.

    0.9.2

    • Fixed bug #45 where a plot could crash if series had different numbers of points.
    • Fixed issue #50, added option to turn off sorting of series data.
    • Fixed issue #31, implemented a better axis autoscaling algorithm and added an autoscale option.

    0.9.1

    • Fixed bug #40, when axis pad, padMax, padMin set to 0, graph would fail to render.
    • Fixed bug #41 where pie and bar charts not rendered correctly on redraw().
    • Fixed bug #11, filled stacked line plots not rendering correctly in IE.
    • Fixed bug #42 where stacked charts not rendering with string date axis ticks.
    • Fixed bug in redraw() method where axes ticks were not reset.
    • Fixed “jqplotPreRedrawEvent” that should have been named “jqplotPostRedraw” event.

    0.9.0

    • Added Open Hi Low Close charts, Candlestick charts and Hi Low Close charts.
    • Added support for arbitrary labels on the data points.
    • Enhanced highlighter plugin to allow custom formatting control of entire tooltip.
    • Enhanced highlighter to support multiple y values in a data point.
    • Fixed bug #38 where series with a single point with a negative value would fail.
    • Improvements to examples to show what plugins to include.
    • Expanded documentation for some of the plugins.

    0.8.5

    • Added zooming ability with double click or single click options to reset zoom.
    • Modified default tick spacing algorithm for date axes to give more space to ticks.
    • Fixed bug #2 where tickInterval wasn’t working properly.
    • Added neighborThreshold option to control how close mouse must be to point to trigger neighbor detection.
    • Added double click event handler on plot.

    0.8.0

    • Support for up to 9 y axes.
    • Added option to control padding at max/min bounds of axes separately.
    • Closed issue #21, added options to control grid line color and width.
    • Closed issue #20, added options to filled line charts to stoke above fill and customize fill color and transparency.
    • Improved structure of on line documentation to make usage and options docs default.
    • Added much documentation on options and css styling.

    0.7.1

    • Bug fix release
    • Fixed bug #6, missing semi-colons messing up some javascript compressors.
    • Fixed bug #13 where 2D ticks array of [values, labels] would fail to renderer with DateAxisRenderer.
    • Fixes bug #16 where pie renderer overwriting options for all plot types and crashing non pie plots.
    • Fixes bug #17 constrainTo dragable option mispelled as “contstrainTo”.  Fixed dragable color issue when used with trend lines.

    0.7.0

    • Pie chart support
    • Enabled tooltipLocation option in highlighter.
    • Highlighter Tooltip will account for mark size and highlight size when positioning itself.
    • Added ability to show just x, y or both axes in highlighter tooltip.
    • Added customization of separator between axes values in highlighter tooltip.
    • Modified how shadows are drawn for lines, bars and markers.  Now drawn first, so they are always behind the object.
    • Adjustments to shadow parameters on lines to account for new shadow positioning.
    • Added a ColorGenerator class to robustly return next available color for a plot with wrap around to first color at end.
    • Udates to docs about css file.
    • Fixed bug with String x values in series and IE error on sorting (Category Axis).
    • Added cursor changes in dragable plugin when cursor near dragable point.

    0.6.6b

    • Added excanvas.js and excanvas.min.js to compressed distributions.
    • Added example/test html pages I had locally into repository and to compressed distributions.

    0.6.6a

    • Removed absolute positioning from dom element and put back into css file.
    • Duplicate of 0.6.6 with a suffix to unambiguously differentiate between previously posted 0.6.6 release.

    0.6.6

    • Fixed bug #5, trend line plugin failing when no trend line options specified.
    • Added absolute position css spec to axis tick dom element.
    • Enhancement to category axes, more intuitive handling of series with missing data values.

    0.6.5

    • Fixed bug #4, series of unequal data length not rendering correctly.  This is a bugfix release only.

    0.6.4

    • Fixed bug (issue #1 in tracker) where flat line data series (all x and/or y values are euqal) or single value data series would crash.

    0.6.3

    • Support for stacked line (a.k.a. area) and stacked bar (horizontal and vertical) charts.
    • Refactored barRenderer to use default shape and shadow renderers.
    • Added info (contacts & support information) page to web site.

    0.6.2

    • This is a minor upgrade to docs and build only.  No functionality has changed.
    • Ant build script generates entire site, examples, tests and distribution.
    • Improvements to documentation.

    0.6.1

    • New sprintf implementation from Ash Searle that implements %g.
    • Fix to sprintf e/f formats.
    • Created new format specifier, %p and %P to preserve significance.
    • Modified p/P format to better display larger numbers.
    • Fixed and simplified significant digits calculation for sprintf.
    • Added option to have cursor tooltip follow the mouse or not.
    • Added options to change size of highlight.
    • Updates to handle dates like ‘6-May-09’.
    • Mods to improve look of web site.
    • Updates to documentation.
    • Added license and copyright statement to source files.

    0.6.0

    • Added rotated text support.  Uses native canvas text functionality in browsers that support it or draws text on canvas with Hershey font
    • metrics for non-supporting browsers.
    • Removed lots of lint in js code.
    • Moved tick css from js code into css file.
    • Fix to tick positioning css.  y axis ticks were positioned to wrong side of axis div.
    • Re-factored axis tick renderer instantiation into the axes renderers themselves.

    For changes prior to 0.6.0 release, please see change log at http://bitbucket.org/cleonello/jqplot/changesets/

    - -
    - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/site_assets/test/js/dist/docs/files/gpl-2-0-txt.html b/public/site_assets/test/js/dist/docs/files/gpl-2-0-txt.html deleted file mode 100644 index c71baf32..00000000 --- a/public/site_assets/test/js/dist/docs/files/gpl-2-0-txt.html +++ /dev/null @@ -1,39 +0,0 @@ - - -GPL Version 2 - - - - - - - - - -

    GNU GENERAL PUBLIC LICENSE Version 2, June 1991

    Copyright © 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.

    Preamble

    The licenses for most software are designed to take away your freedom to share and change it.  By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.  This General Public License applies to most of the Free Software Foundation’s software and to any other program whose authors commit to using it.  (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.)  You can apply it to your programs, too.

    When we speak of free software, we are referring to freedom, not price.  Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.

    To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights.  These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.

    For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have.  You must make sure that they, too, receive or can get the source code.  And you must show them these terms so they know their rights.

    We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.

    Also, for each author’s protection and ours, we want to make certain that everyone understands that there is no warranty for this free software.  If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors’ reputations.

    Finally, any free program is threatened constantly by software patents.  We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary.  To prevent this, we have made it clear that any patent must be licensed for everyone’s free use or not licensed at all.

    The precise terms and conditions for copying, distribution and modification follow.

    GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

    0.  This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License.  The “Program”, below, refers to any such program or work, and a “work based on the Program” means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language.  (Hereinafter, translation is included without limitation in the term “modification”.)  Each licensee is addressed as “you”.

    Activities other than copying, distribution and modification are not covered by this License; they are outside its scope.  The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program).  Whether that is true depends on what the Program does.

    1.  You may copy and distribute verbatim copies of the Program’s source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.

    You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.

    2.  You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.

    c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License.  (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)

    These requirements apply to the modified work as a whole.  If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works.  But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.

    Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.

    In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.

    3.  You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer to distribute corresponding source code.  (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)

    The source code for a work means the preferred form of the work for making modifications to it.  For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable.  However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.

    If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.

    4.  You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License.  Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License.  However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.

    5.  You are not required to accept this License, since you have not signed it.  However, nothing else grants you permission to modify or distribute the Program or its derivative works.  These actions are prohibited by law if you do not accept this License.  Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.

    6.  Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions.  You may not impose any further restrictions on the recipients’ exercise of the rights granted herein.  You are not responsible for enforcing compliance by third parties to this License.

    7.  If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License.  If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all.  For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.

    If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.

    It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices.  Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.

    This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.

    8.  If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded.  In such case, this License incorporates the limitation as if written in the body of this License.

    9.  The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time.  Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.

    Each version is given a distinguishing version number.  If the Program specifies a version number of this License which applies to it and “any later version”, you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation.  If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.

    10.  If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission.  For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this.  Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.

    NO WARRANTY

    11.  BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

    12.  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

    - -
    - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/site_assets/test/js/dist/docs/files/images/background.jpg b/public/site_assets/test/js/dist/docs/files/images/background.jpg deleted file mode 100644 index c15505294720a0e1d83a5c039e05298d080e8a54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1101 zcmex=%kxU@K!VDr=Sy-4^*jU-vz?hwr zgN>b&osEryn}d@J1b{3aZY~}W2V@9DD-$a-Gbv!^|O!0je; zaq`9Wk6&J#G%H|hY_`JIz233ax_R@YC$CNjvr;*EeCylenMUVtu&zB*xN?$W<>6c0 zHNRNbUU$0lC%nhvtw2hjW9H$x-7!wzzP?GFFUq?1r|zdMB?WipMl^yL?H^S>H>IW~3b+w*PF{l*G= z_nYV(d9`=H{9~gd6|8TMl)XD$_%ZITrXkN(>EGA5Rm_W696sw=Oh3C diff --git a/public/site_assets/test/js/dist/docs/files/images/basicline.png b/public/site_assets/test/js/dist/docs/files/images/basicline.png deleted file mode 100644 index 1cc6bc69136dcce4bea1206afac445ae996c0e8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17024 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 Ghx{*hLh2m= diff --git a/public/site_assets/test/js/dist/docs/files/images/basiclogaxis.png b/public/site_assets/test/js/dist/docs/files/images/basiclogaxis.png deleted file mode 100644 index 7c169633ac6e387f51c1f88400439b490b45cb23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19902 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%0Dc;I#BfBZmY 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 diff --git a/public/site_assets/test/js/dist/docs/files/images/basicoptions.png b/public/site_assets/test/js/dist/docs/files/images/basicoptions.png deleted file mode 100644 index 4ea441c89adac7b20bc38d2a16c7d042791b722c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19864 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 xg7W$JUlD1FscxxC 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
    ').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(), -top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle= -this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=a.handles||(!e(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne", -nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all")this.handles="n,e,s,w,se,sw,ne,nw";var c=this.handles.split(",");this.handles={};for(var d=0;d
    ');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor== -String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),l=0;l=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,l);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection(); -this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){if(!a.disabled){e(this).removeClass("ui-resizable-autohide");b._handles.show()}},function(){if(!a.disabled)if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy(); -var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a= -false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(),d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"}); -this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset=this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff= -{width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio:this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis]; -if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);this._updateVirtualBoundaries(b.shiftKey);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize",b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false}, -_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height;f=f?0:c.sizeDiff.width;f={width:c.helper.width()-f,height:c.helper.height()-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f, -{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop",b);this._helper&&this.helper.remove();return false},_updateVirtualBoundaries:function(b){var a=this.options,c,d,f;a={minWidth:k(a.minWidth)?a.minWidth:0,maxWidth:k(a.maxWidth)?a.maxWidth:Infinity,minHeight:k(a.minHeight)?a.minHeight:0,maxHeight:k(a.maxHeight)?a.maxHeight: -Infinity};if(this._aspectRatio||b){b=a.minHeight*this.aspectRatio;d=a.minWidth/this.aspectRatio;c=a.maxHeight*this.aspectRatio;f=a.maxWidth/this.aspectRatio;if(b>a.minWidth)a.minWidth=b;if(d>a.minHeight)a.minHeight=d;if(cb.width,h=k(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height,l=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&l)b.left=i-a.minWidth;if(d&&l)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left= -null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a
    ');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+ -a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+ -c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]); -b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable,{version:"1.8.16"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(), -10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize,function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top- -f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var l=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:l.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n=(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(l.css("position"))){c._revertToRelativePosition=true;l.css({position:"absolute",top:"auto",left:"auto"})}l.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType? -e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition=false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a= -e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left-a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing, -step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize",b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement= -e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top","Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset; -var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset,f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left: -a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left=a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top- -d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition, -f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25, -display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative",height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b= -e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height= -d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},k=function(b){return!isNaN(parseInt(b,10))}})(jQuery); -;/* - * jQuery UI Selectable 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Selectables - * - * Depends: - * jquery.ui.core.js - * jquery.ui.mouse.js - * jquery.ui.widget.js - */ -(function(e){e.widget("ui.selectable",e.ui.mouse,{options:{appendTo:"body",autoRefresh:true,distance:0,filter:"*",tolerance:"touch"},_create:function(){var c=this;this.element.addClass("ui-selectable");this.dragged=false;var f;this.refresh=function(){f=e(c.options.filter,c.element[0]);f.each(function(){var d=e(this),b=d.offset();e.data(this,"selectable-item",{element:this,$element:d,left:b.left,top:b.top,right:b.left+d.outerWidth(),bottom:b.top+d.outerHeight(),startselected:false,selected:d.hasClass("ui-selected"), -selecting:d.hasClass("ui-selecting"),unselecting:d.hasClass("ui-unselecting")})})};this.refresh();this.selectees=f.addClass("ui-selectee");this._mouseInit();this.helper=e("
    ")},destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item");this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable");this._mouseDestroy();return this},_mouseStart:function(c){var f=this;this.opos=[c.pageX, -c.pageY];if(!this.options.disabled){var d=this.options;this.selectees=e(d.filter,this.element[0]);this._trigger("start",c);e(d.appendTo).append(this.helper);this.helper.css({left:c.clientX,top:c.clientY,width:0,height:0});d.autoRefresh&&this.refresh();this.selectees.filter(".ui-selected").each(function(){var b=e.data(this,"selectable-item");b.startselected=true;if(!c.metaKey){b.$element.removeClass("ui-selected");b.selected=false;b.$element.addClass("ui-unselecting");b.unselecting=true;f._trigger("unselecting", -c,{unselecting:b.element})}});e(c.target).parents().andSelf().each(function(){var b=e.data(this,"selectable-item");if(b){var g=!c.metaKey||!b.$element.hasClass("ui-selected");b.$element.removeClass(g?"ui-unselecting":"ui-selected").addClass(g?"ui-selecting":"ui-unselecting");b.unselecting=!g;b.selecting=g;(b.selected=g)?f._trigger("selecting",c,{selecting:b.element}):f._trigger("unselecting",c,{unselecting:b.element});return false}})}},_mouseDrag:function(c){var f=this;this.dragged=true;if(!this.options.disabled){var d= -this.options,b=this.opos[0],g=this.opos[1],h=c.pageX,i=c.pageY;if(b>h){var j=h;h=b;b=j}if(g>i){j=i;i=g;g=j}this.helper.css({left:b,top:g,width:h-b,height:i-g});this.selectees.each(function(){var a=e.data(this,"selectable-item");if(!(!a||a.element==f.element[0])){var k=false;if(d.tolerance=="touch")k=!(a.left>h||a.righti||a.bottomb&&a.rightg&&a.bottom *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){var a=this.options;this.containerCache={};this.element.addClass("ui-sortable"); -this.refresh();this.floating=this.items.length?a.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a=== -"disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&& -!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem=c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top, -left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]}; -this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment();if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!= -document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start",a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a); -return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0], -e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a,c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset(); -c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp({target:null});this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"): -this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate",null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}if(this.placeholder){this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null, -dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem):d(this.domPosition.parent).prepend(this.currentItem)}return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")}, -toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute||"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+jg&&b+la[this.floating?"width":"height"]?j:g0?"down":"up")},_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith(); -if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!=this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), -this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a=this.currentItem.find(":data(sortable-item)"),b=0;b=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable");if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h=0;b--){var c=this.items[b];if(!(c.instance!=this.currentContainer&&this.currentContainer&&c.item[0]!=this.currentItem[0])){var e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b= -this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width=this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f= -d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f},update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")|| -0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b=null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out", -a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this));this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h- -f)this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g- -this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])?g:!(g-this.offset.click.topthis.containment[2])?f:!(f-this.offset.click.left=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive",g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this, -this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over=0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop", -a,this._uiHash());for(e=0;e li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:false,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var a=this,b=a.options;a.running=0;a.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"); -a.headers=a.element.find(b.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){b.disabled||c(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){b.disabled||c(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){b.disabled||c(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){b.disabled||c(this).removeClass("ui-state-focus")});a.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom"); -if(b.navigation){var d=a.element.find("a").filter(b.navigationFilter).eq(0);if(d.length){var h=d.closest(".ui-accordion-header");a.active=h.length?h:d.closest(".ui-accordion-content").prev()}}a.active=a._findActive(a.active||b.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top");a.active.next().addClass("ui-accordion-content-active");a._createIcons();a.resize();a.element.attr("role","tablist");a.headers.attr("role","tab").bind("keydown.accordion", -function(f){return a._keydown(f)}).next().attr("role","tabpanel");a.headers.not(a.active||"").attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).next().hide();a.active.length?a.active.attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}):a.headers.eq(0).attr("tabIndex",0);c.browser.safari||a.headers.find("a").attr("tabIndex",-1);b.event&&a.headers.bind(b.event.split(" ").join(".accordion ")+".accordion",function(f){a._clickHandler.call(a,f,this);f.preventDefault()})},_createIcons:function(){var a= -this.options;if(a.icons){c("").addClass("ui-icon "+a.icons.header).prependTo(this.headers);this.active.children(".ui-icon").toggleClass(a.icons.header).toggleClass(a.icons.headerSelected);this.element.addClass("ui-accordion-icons")}},_destroyIcons:function(){this.headers.children(".ui-icon").remove();this.element.removeClass("ui-accordion-icons")},destroy:function(){var a=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role");this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("tabIndex"); -this.headers.find("a").removeAttr("tabIndex");this._destroyIcons();var b=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");if(a.autoHeight||a.fillHeight)b.css("height","");return c.Widget.prototype.destroy.call(this)},_setOption:function(a,b){c.Widget.prototype._setOption.apply(this,arguments);a=="active"&&this.activate(b);if(a=="icons"){this._destroyIcons(); -b&&this._createIcons()}if(a=="disabled")this.headers.add(this.headers.next())[b?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(a){if(!(this.options.disabled||a.altKey||a.ctrlKey)){var b=c.ui.keyCode,d=this.headers.length,h=this.headers.index(a.target),f=false;switch(a.keyCode){case b.RIGHT:case b.DOWN:f=this.headers[(h+1)%d];break;case b.LEFT:case b.UP:f=this.headers[(h-1+d)%d];break;case b.SPACE:case b.ENTER:this._clickHandler({target:a.target},a.target); -a.preventDefault()}if(f){c(a.target).attr("tabIndex",-1);c(f).attr("tabIndex",0);f.focus();return false}return true}},resize:function(){var a=this.options,b;if(a.fillSpace){if(c.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}b=this.element.parent().height();c.browser.msie&&this.element.parent().css("overflow",d);this.headers.each(function(){b-=c(this).outerHeight(true)});this.headers.next().each(function(){c(this).height(Math.max(0,b-c(this).innerHeight()+ -c(this).height()))}).css("overflow","auto")}else if(a.autoHeight){b=0;this.headers.next().each(function(){b=Math.max(b,c(this).height("").height())}).height(b)}return this},activate:function(a){this.options.active=a;a=this._findActive(a)[0];this._clickHandler({target:a},a);return this},_findActive:function(a){return a?typeof a==="number"?this.headers.filter(":eq("+a+")"):this.headers.not(this.headers.not(a)):a===false?c([]):this.headers.filter(":eq(0)")},_clickHandler:function(a,b){var d=this.options; -if(!d.disabled)if(a.target){a=c(a.currentTarget||b);b=a[0]===this.active[0];d.active=d.collapsible&&b?false:this.headers.index(a);if(!(this.running||!d.collapsible&&b)){var h=this.active;j=a.next();g=this.active.next();e={options:d,newHeader:b&&d.collapsible?c([]):a,oldHeader:this.active,newContent:b&&d.collapsible?c([]):j,oldContent:g};var f=this.headers.index(this.active[0])>this.headers.index(a[0]);this.active=b?c([]):a;this._toggle(j,g,e,b,f);h.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header); -if(!b){a.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected);a.next().addClass("ui-accordion-content-active")}}}else if(d.collapsible){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);this.active.next().addClass("ui-accordion-content-active");var g=this.active.next(), -e={options:d,newHeader:c([]),oldHeader:d.active,newContent:c([]),oldContent:g},j=this.active=c([]);this._toggle(j,g,e)}},_toggle:function(a,b,d,h,f){var g=this,e=g.options;g.toShow=a;g.toHide=b;g.data=d;var j=function(){if(g)return g._completed.apply(g,arguments)};g._trigger("changestart",null,g.data);g.running=b.size()===0?a.size():b.size();if(e.animated){d={};d=e.collapsible&&h?{toShow:c([]),toHide:b,complete:j,down:f,autoHeight:e.autoHeight||e.fillSpace}:{toShow:a,toHide:b,complete:j,down:f,autoHeight:e.autoHeight|| -e.fillSpace};if(!e.proxied)e.proxied=e.animated;if(!e.proxiedDuration)e.proxiedDuration=e.duration;e.animated=c.isFunction(e.proxied)?e.proxied(d):e.proxied;e.duration=c.isFunction(e.proxiedDuration)?e.proxiedDuration(d):e.proxiedDuration;h=c.ui.accordion.animations;var i=e.duration,k=e.animated;if(k&&!h[k]&&!c.easing[k])k="slide";h[k]||(h[k]=function(l){this.slide(l,{easing:k,duration:i||700})});h[k](d)}else{if(e.collapsible&&h)a.toggle();else{b.hide();a.show()}j(true)}b.prev().attr({"aria-expanded":"false", -"aria-selected":"false",tabIndex:-1}).blur();a.prev().attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(!this.running){this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""});this.toHide.removeClass("ui-accordion-content-active");if(this.toHide.length)this.toHide.parent()[0].className=this.toHide.parent()[0].className;this._trigger("change",null,this.data)}}});c.extend(c.ui.accordion,{version:"1.8.16", -animations:{slide:function(a,b){a=c.extend({easing:"swing",duration:300},a,b);if(a.toHide.size())if(a.toShow.size()){var d=a.toShow.css("overflow"),h=0,f={},g={},e;b=a.toShow;e=b[0].style.width;b.width(parseInt(b.parent().width(),10)-parseInt(b.css("paddingLeft"),10)-parseInt(b.css("paddingRight"),10)-(parseInt(b.css("borderLeftWidth"),10)||0)-(parseInt(b.css("borderRightWidth"),10)||0));c.each(["height","paddingTop","paddingBottom"],function(j,i){g[i]="hide";j=(""+c.css(a.toShow[0],i)).match(/^([\d+-.]+)(.*)$/); -f[i]={value:j[1],unit:j[2]||"px"}});a.toShow.css({height:0,overflow:"hidden"}).show();a.toHide.filter(":hidden").each(a.complete).end().filter(":visible").animate(g,{step:function(j,i){if(i.prop=="height")h=i.end-i.start===0?0:(i.now-i.start)/(i.end-i.start);a.toShow[0].style[i.prop]=h*f[i.prop].value+f[i.prop].unit},duration:a.duration,easing:a.easing,complete:function(){a.autoHeight||a.toShow.css("height","");a.toShow.css({width:e,overflow:d});a.complete()}})}else a.toHide.animate({height:"hide", -paddingTop:"hide",paddingBottom:"hide"},a);else a.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},a)},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1E3:200})}}})})(jQuery); -;/* - * jQuery UI Autocomplete 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Autocomplete - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - * jquery.ui.position.js - */ -(function(d){var e=0;d.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:false,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var a=this,b=this.element[0].ownerDocument,g;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!(a.options.disabled||a.element.propAttr("readOnly"))){g= -false;var f=d.ui.keyCode;switch(c.keyCode){case f.PAGE_UP:a._move("previousPage",c);break;case f.PAGE_DOWN:a._move("nextPage",c);break;case f.UP:a._move("previous",c);c.preventDefault();break;case f.DOWN:a._move("next",c);c.preventDefault();break;case f.ENTER:case f.NUMPAD_ENTER:if(a.menu.active){g=true;c.preventDefault()}case f.TAB:if(!a.menu.active)return;a.menu.select(c);break;case f.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!= -a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay);break}}}).bind("keypress.autocomplete",function(c){if(g){g=false;c.preventDefault()}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)}; -this.menu=d("
      ").addClass("ui-autocomplete").appendTo(d(this.options.appendTo||"body",b)[0]).mousedown(function(c){var f=a.menu.element[0];d(c.target).closest(".ui-menu-item").length||setTimeout(function(){d(document).one("mousedown",function(h){h.target!==a.element[0]&&h.target!==f&&!d.ui.contains(f,h.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,f){f=f.item.data("item.autocomplete");false!==a._trigger("focus",c,{item:f})&&/^key/.test(c.originalEvent.type)&& -a.element.val(f.value)},selected:function(c,f){var h=f.item.data("item.autocomplete"),i=a.previous;if(a.element[0]!==b.activeElement){a.element.focus();a.previous=i;setTimeout(function(){a.previous=i;a.selectedItem=h},1)}false!==a._trigger("select",c,{item:h})&&a.element.val(h.value);a.term=a.element.val();a.close(c);a.selectedItem=h},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu"); -d.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup");this.menu.element.remove();d.Widget.prototype.destroy.call(this)},_setOption:function(a,b){d.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(d(b||"body",this.element[0].ownerDocument)[0]);a==="disabled"&& -b&&this.xhr&&this.xhr.abort()},_initSource:function(){var a=this,b,g;if(d.isArray(this.options.source)){b=this.options.source;this.source=function(c,f){f(d.ui.autocomplete.filter(b,c.term))}}else if(typeof this.options.source==="string"){g=this.options.source;this.source=function(c,f){a.xhr&&a.xhr.abort();a.xhr=d.ajax({url:g,data:c,dataType:"json",autocompleteRequest:++e,success:function(h){this.autocompleteRequest===e&&f(h)},error:function(){this.autocompleteRequest===e&&f([])}})}}else this.source= -this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length").data("item.autocomplete",b).append(d("
      ").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});d.extend(d.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, -"\\$&")},filter:function(a,b){var g=new RegExp(d.ui.autocomplete.escapeRegex(b),"i");return d.grep(a,function(c){return g.test(c.label||c.value||c)})}})})(jQuery); -(function(d){d.widget("ui.menu",{_create:function(){var e=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(a){if(d(a.target).closest(".ui-menu-item a").length){a.preventDefault();e.select(a)}});this.refresh()},refresh:function(){var e=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex", --1).mouseenter(function(a){e.activate(a,d(this).parent())}).mouseleave(function(){e.deactivate()})},activate:function(e,a){this.deactivate();if(this.hasScroll()){var b=a.offset().top-this.element.offset().top,g=this.element.scrollTop(),c=this.element.height();if(b<0)this.element.scrollTop(g+b);else b>=c&&this.element.scrollTop(g+b-c+a.height())}this.active=a.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",e,{item:a})},deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id"); -this._trigger("blur");this.active=null}},next:function(e){this.move("next",".ui-menu-item:first",e)},previous:function(e){this.move("prev",".ui-menu-item:last",e)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(e,a,b){if(this.active){e=this.active[e+"All"](".ui-menu-item").eq(0);e.length?this.activate(b,e):this.activate(b,this.element.children(a))}else this.activate(b, -this.element.children(a))},nextPage:function(e){if(this.hasScroll())if(!this.active||this.last())this.activate(e,this.element.children(".ui-menu-item:first"));else{var a=this.active.offset().top,b=this.element.height(),g=this.element.children(".ui-menu-item").filter(function(){var c=d(this).offset().top-a-b+d(this).height();return c<10&&c>-10});g.length||(g=this.element.children(".ui-menu-item:last"));this.activate(e,g)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active|| -this.last()?":first":":last"))},previousPage:function(e){if(this.hasScroll())if(!this.active||this.first())this.activate(e,this.element.children(".ui-menu-item:last"));else{var a=this.active.offset().top,b=this.element.height();result=this.element.children(".ui-menu-item").filter(function(){var g=d(this).offset().top-a+b-d(this).height();return g<10&&g>-10});result.length||(result=this.element.children(".ui-menu-item:first"));this.activate(e,result)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active|| -this.first()?":last":":first"))},hasScroll:function(){return this.element.height()").addClass("ui-button-text").html(this.options.label).appendTo(a.empty()).text(),e=this.options.icons,f=e.primary&&e.secondary,d=[];if(e.primary||e.secondary){if(this.options.text)d.push("ui-button-text-icon"+(f?"s":e.primary?"-primary":"-secondary"));e.primary&&a.prepend("");e.secondary&&a.append("");if(!this.options.text){d.push(f?"ui-button-icons-only": -"ui-button-icon-only");this.hasTitle||a.attr("title",c)}}else d.push("ui-button-text-only");a.addClass(d.join(" "))}}});b.widget("ui.buttonset",{options:{items:":button, :submit, :reset, :checkbox, :radio, a, :data(button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(a,c){a==="disabled"&&this.buttons.button("option",a,c);b.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){var a=this.element.css("direction")=== -"ltr";this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return b(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(a?"ui-corner-left":"ui-corner-right").end().filter(":last").addClass(a?"ui-corner-right":"ui-corner-left").end().end()},destroy:function(){this.element.removeClass("ui-buttonset");this.buttons.map(function(){return b(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy"); -b.Widget.prototype.destroy.call(this)}})})(jQuery); -;/* - * jQuery UI Dialog 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Dialog - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - * jquery.ui.button.js - * jquery.ui.draggable.js - * jquery.ui.mouse.js - * jquery.ui.position.js - * jquery.ui.resizable.js - */ -(function(c,l){var m={buttons:true,height:true,maxHeight:true,maxWidth:true,minHeight:true,minWidth:true,width:true},n={maxHeight:true,maxWidth:true,minHeight:true,minWidth:true},o=c.attrFn||{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true,click:true};c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false, -position:{my:"center",at:"center",collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title");if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||" ",e=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("
      ")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+ -b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&!i.isDefaultPrevented()&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog","aria-labelledby":e}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var f=(a.uiDialogTitlebar=c("
      ")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g), -h=c('').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i);return false}).appendTo(f);(a.uiDialogTitlebarCloseText=c("")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("").addClass("ui-dialog-title").attr("id", -e).html(d).prependTo(f);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;f.find("*").add(f).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&&g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"); -a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d,e;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog");b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!== -b.uiDialog[0]){e=c(this).css("z-index");isNaN(e)||(d=Math.max(d,e))}});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,e=d.options;if(e.modal&&!a||!e.stack&&!e.modal)return d._trigger("focus",b);if(e.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ=e.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.scrollTop(),scrollLeft:d.element.scrollLeft()};c.ui.dialog.maxZ+=1; -d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;a._size();a._position(b.position);d.show(b.show);a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(e){if(e.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),f=g.filter(":first");g=g.filter(":last");if(e.target===g[0]&&!e.shiftKey){f.focus(1);return false}else if(e.target=== -f[0]&&e.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false,e=c("
      ").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("
      ").addClass("ui-dialog-buttonset").appendTo(e);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a, -function(){return!(d=true)});if(d){c.each(a,function(f,h){h=c.isFunction(h)?{click:h,text:f}:h;var i=c('').click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.each(h,function(j,k){if(j!=="click")j in o?i[j](k):i.attr(j,k)});c.fn.button&&i.button()});e.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(f){return{position:f.position,offset:f.offset}}var b=this,d=b.options,e=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close", -handle:".ui-dialog-titlebar",containment:"document",start:function(f,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",f,a(h))},drag:function(f,h){b._trigger("drag",f,a(h))},stop:function(f,h){d.position=[h.position.left-e.scrollLeft(),h.position.top-e.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g);b._trigger("dragStop",f,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(f){return{originalPosition:f.originalPosition, -originalSize:f.originalSize,position:f.position,size:f.size}}a=a===l?this.options.resizable:a;var d=this,e=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:e.maxWidth,maxHeight:e.maxHeight,minWidth:e.minWidth,minHeight:d._minHeight(),handles:a,start:function(f,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",f,b(h))},resize:function(f,h){d._trigger("resize", -f,b(h))},stop:function(f,h){c(this).removeClass("ui-dialog-resizing");e.height=c(this).height();e.width=c(this).width();d._trigger("resizeStop",f,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,a.height)},_position:function(a){var b=[],d=[0,0],e;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "): -[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,f){if(+b[g]===b[g]){d[g]=b[g];b[g]=f}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(e=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(c.extend({of:window},a));e||this.uiDialog.hide()},_setOptions:function(a){var b=this,d={},e=false;c.each(a,function(g,f){b._setOption(g,f); -if(g in m)e=true;if(g in n)d[g]=f});e&&this._size();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",d)},_setOption:function(a,b){var d=this,e=d.uiDialog;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":e.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?e.addClass("ui-dialog-disabled"): -e.removeClass("ui-dialog-disabled");break;case "draggable":var g=e.is(":data(draggable)");g&&!b&&e.draggable("destroy");!g&&b&&d._makeDraggable();break;case "position":d._position(b);break;case "resizable":(g=e.is(":data(resizable)"))&&!b&&e.resizable("destroy");g&&typeof b==="string"&&e.resizable("option","handles",b);!g&&b!==false&&d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||" "));break}c.Widget.prototype._setOption.apply(d,arguments)},_size:function(){var a= -this.options,b,d,e=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();d=Math.max(0,a.minHeight-b);if(a.height==="auto")if(c.support.minHeight)this.element.css({minHeight:d,height:"auto"});else{this.uiDialog.show();a=this.element.css("height","auto").height();e||this.uiDialog.hide();this.element.height(Math.max(a,d))}else this.element.height(Math.max(a.height- -b,0));this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.16",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(a){return a+".dialog-overlay"}).join(" "), -create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()
      ").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){var b=c.inArray(a,this.instances);b!=-1&&this.oldInstances.push(this.instances.splice(b,1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var d=0;c.each(this.instances,function(){d=Math.max(d,this.css("z-index"))});this.maxZ=d},height:function(){var a,b;if(c.browser.msie&& -c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a
      ").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(b.range==="min"||b.range==="max"?" ui-slider-range-"+b.range:""))}for(var j=c.length;j"); -this.handles=c.add(d(e.join("")).appendTo(a.element));this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(g){g.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur();else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(g){d(this).data("index.ui-slider-handle", -g)});this.handles.keydown(function(g){var k=true,l=d(this).data("index.ui-slider-handle"),i,h,m;if(!a.options.disabled){switch(g.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:k=false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");i=a._start(g,l);if(i===false)return}break}m=a.options.step;i=a.options.values&&a.options.values.length? -(h=a.values(l)):(h=a.value());switch(g.keyCode){case d.ui.keyCode.HOME:h=a._valueMin();break;case d.ui.keyCode.END:h=a._valueMax();break;case d.ui.keyCode.PAGE_UP:h=a._trimAlignValue(i+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:h=a._trimAlignValue(i-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(i===a._valueMax())return;h=a._trimAlignValue(i+m);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(i===a._valueMin())return;h=a._trimAlignValue(i- -m);break}a._slide(g,l,h);return k}}).keyup(function(g){var k=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(g,k);a._change(g,k);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider");this._mouseDestroy(); -return this},_mouseCapture:function(a){var b=this.options,c,f,e,j,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});f=this._valueMax()-this._valueMin()+1;j=this;this.handles.each(function(k){var l=Math.abs(c-j.values(k));if(f>l){f=l;e=d(this);g=k}});if(b.range===true&&this.values(1)===b.min){g+=1;e=d(this.handles[g])}if(this._start(a,g)===false)return false; -this._mouseSliding=true;j._handleIndex=g;e.addClass("ui-state-active").focus();b=e.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-e.width()/2,top:a.pageY-b.top-e.height()/2-(parseInt(e.css("borderTopWidth"),10)||0)-(parseInt(e.css("borderBottomWidth"),10)||0)+(parseInt(e.css("marginTop"),10)||0)};this.handles.hasClass("ui-state-hover")||this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b= -this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b= -this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b); -c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var f;if(this.options.values&&this.options.values.length){f=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>f||b===1&&c1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}else if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;f=arguments[0];for(e=0;e=this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=(a-this._valueMin())%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a= -this.options.range,b=this.options,c=this,f=!this._animateOff?b.animate:false,e,j={},g,k,l,i;if(this.options.values&&this.options.values.length)this.handles.each(function(h){e=(c.values(h)-c._valueMin())/(c._valueMax()-c._valueMin())*100;j[c.orientation==="horizontal"?"left":"bottom"]=e+"%";d(this).stop(1,1)[f?"animate":"css"](j,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(h===0)c.range.stop(1,1)[f?"animate":"css"]({left:e+"%"},b.animate);if(h===1)c.range[f?"animate":"css"]({width:e- -g+"%"},{queue:false,duration:b.animate})}else{if(h===0)c.range.stop(1,1)[f?"animate":"css"]({bottom:e+"%"},b.animate);if(h===1)c.range[f?"animate":"css"]({height:e-g+"%"},{queue:false,duration:b.animate})}g=e});else{k=this.value();l=this._valueMin();i=this._valueMax();e=i!==l?(k-l)/(i-l)*100:0;j[c.orientation==="horizontal"?"left":"bottom"]=e+"%";this.handle.stop(1,1)[f?"animate":"css"](j,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[f?"animate":"css"]({width:e+"%"}, -b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[f?"animate":"css"]({width:100-e+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[f?"animate":"css"]({height:e+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[f?"animate":"css"]({height:100-e+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.16"})})(jQuery); -;/* - * jQuery UI Tabs 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Tabs - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - */ -(function(d,p){function u(){return++v}function w(){return++x}var v=0,x=0;d.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:false,cookie:null,collapsible:false,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"
      ",remove:null,select:null,show:null,spinner:"Loading…",tabTemplate:"
    • #{label}
    • "},_create:function(){this._tabify(true)},_setOption:function(b,e){if(b=="selected")this.options.collapsible&& -e==this.options.selected||this.select(e);else{this.options[b]=e;this._tabify()}},_tabId:function(b){return b.title&&b.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+u()},_sanitizeSelector:function(b){return b.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+w());return d.cookie.apply(null,[b].concat(d.makeArray(arguments)))},_ui:function(b,e){return{tab:b,panel:e,index:this.anchors.index(b)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b= -d(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(b){function e(g,f){g.css("display","");!d.support.opacity&&f.opacity&&g[0].style.removeAttribute("filter")}var a=this,c=this.options,h=/^#.+/;this.list=this.element.find("ol,ul").eq(0);this.lis=d(" > li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return d("a",this)[0]});this.panels=d([]);this.anchors.each(function(g,f){var i=d(f).attr("href"),l=i.split("#")[0],q;if(l&&(l===location.toString().split("#")[0]|| -(q=d("base")[0])&&l===q.href)){i=f.hash;f.href=i}if(h.test(i))a.panels=a.panels.add(a.element.find(a._sanitizeSelector(i)));else if(i&&i!=="#"){d.data(f,"href.tabs",i);d.data(f,"load.tabs",i.replace(/#.*$/,""));i=a._tabId(f);f.href="#"+i;f=a.element.find("#"+i);if(!f.length){f=d(c.panelTemplate).attr("id",i).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(a.panels[g-1]||a.list);f.data("destroy.tabs",true)}a.panels=a.panels.add(f)}else c.disabled.push(g)});if(b){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"); -this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(c.selected===p){location.hash&&this.anchors.each(function(g,f){if(f.hash==location.hash){c.selected=g;return false}});if(typeof c.selected!=="number"&&c.cookie)c.selected=parseInt(a._cookie(),10);if(typeof c.selected!=="number"&&this.lis.filter(".ui-tabs-selected").length)c.selected= -this.lis.index(this.lis.filter(".ui-tabs-selected"));c.selected=c.selected||(this.lis.length?0:-1)}else if(c.selected===null)c.selected=-1;c.selected=c.selected>=0&&this.anchors[c.selected]||c.selected<0?c.selected:0;c.disabled=d.unique(c.disabled.concat(d.map(this.lis.filter(".ui-state-disabled"),function(g){return a.lis.index(g)}))).sort();d.inArray(c.selected,c.disabled)!=-1&&c.disabled.splice(d.inArray(c.selected,c.disabled),1);this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active"); -if(c.selected>=0&&this.anchors.length){a.element.find(a._sanitizeSelector(a.anchors[c.selected].hash)).removeClass("ui-tabs-hide");this.lis.eq(c.selected).addClass("ui-tabs-selected ui-state-active");a.element.queue("tabs",function(){a._trigger("show",null,a._ui(a.anchors[c.selected],a.element.find(a._sanitizeSelector(a.anchors[c.selected].hash))[0]))});this.load(c.selected)}d(window).bind("unload",function(){a.lis.add(a.anchors).unbind(".tabs");a.lis=a.anchors=a.panels=null})}else c.selected=this.lis.index(this.lis.filter(".ui-tabs-selected")); -this.element[c.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible");c.cookie&&this._cookie(c.selected,c.cookie);b=0;for(var j;j=this.lis[b];b++)d(j)[d.inArray(b,c.disabled)!=-1&&!d(j).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");c.cache===false&&this.anchors.removeData("cache.tabs");this.lis.add(this.anchors).unbind(".tabs");if(c.event!=="mouseover"){var k=function(g,f){f.is(":not(.ui-state-disabled)")&&f.addClass("ui-state-"+g)},n=function(g,f){f.removeClass("ui-state-"+ -g)};this.lis.bind("mouseover.tabs",function(){k("hover",d(this))});this.lis.bind("mouseout.tabs",function(){n("hover",d(this))});this.anchors.bind("focus.tabs",function(){k("focus",d(this).closest("li"))});this.anchors.bind("blur.tabs",function(){n("focus",d(this).closest("li"))})}var m,o;if(c.fx)if(d.isArray(c.fx)){m=c.fx[0];o=c.fx[1]}else m=o=c.fx;var r=o?function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.hide().removeClass("ui-tabs-hide").animate(o,o.duration||"normal", -function(){e(f,o);a._trigger("show",null,a._ui(g,f[0]))})}:function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");a._trigger("show",null,a._ui(g,f[0]))},s=m?function(g,f){f.animate(m,m.duration||"normal",function(){a.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");e(f,m);a.element.dequeue("tabs")})}:function(g,f){a.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");a.element.dequeue("tabs")}; -this.anchors.bind(c.event+".tabs",function(){var g=this,f=d(g).closest("li"),i=a.panels.filter(":not(.ui-tabs-hide)"),l=a.element.find(a._sanitizeSelector(g.hash));if(f.hasClass("ui-tabs-selected")&&!c.collapsible||f.hasClass("ui-state-disabled")||f.hasClass("ui-state-processing")||a.panels.filter(":animated").length||a._trigger("select",null,a._ui(this,l[0]))===false){this.blur();return false}c.selected=a.anchors.index(this);a.abort();if(c.collapsible)if(f.hasClass("ui-tabs-selected")){c.selected= --1;c.cookie&&a._cookie(c.selected,c.cookie);a.element.queue("tabs",function(){s(g,i)}).dequeue("tabs");this.blur();return false}else if(!i.length){c.cookie&&a._cookie(c.selected,c.cookie);a.element.queue("tabs",function(){r(g,l)});a.load(a.anchors.index(this));this.blur();return false}c.cookie&&a._cookie(c.selected,c.cookie);if(l.length){i.length&&a.element.queue("tabs",function(){s(g,i)});a.element.queue("tabs",function(){r(g,l)});a.load(a.anchors.index(this))}else throw"jQuery UI Tabs: Mismatching fragment identifier."; -d.browser.msie&&this.blur()});this.anchors.bind("click.tabs",function(){return false})},_getIndex:function(b){if(typeof b=="string")b=this.anchors.index(this.anchors.filter("[href$="+b+"]"));return b},destroy:function(){var b=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var e= -d.data(this,"href.tabs");if(e)this.href=e;var a=d(this).unbind(".tabs");d.each(["href","load","cache"],function(c,h){a.removeData(h+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){d.data(this,"destroy.tabs")?d(this).remove():d(this).removeClass("ui-state-default ui-corner-top ui-tabs-selected ui-state-active ui-state-hover ui-state-focus ui-state-disabled ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide")});b.cookie&&this._cookie(null,b.cookie);return this},add:function(b, -e,a){if(a===p)a=this.anchors.length;var c=this,h=this.options;e=d(h.tabTemplate.replace(/#\{href\}/g,b).replace(/#\{label\}/g,e));b=!b.indexOf("#")?b.replace("#",""):this._tabId(d("a",e)[0]);e.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var j=c.element.find("#"+b);j.length||(j=d(h.panelTemplate).attr("id",b).data("destroy.tabs",true));j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(a>=this.lis.length){e.appendTo(this.list);j.appendTo(this.list[0].parentNode)}else{e.insertBefore(this.lis[a]); -j.insertBefore(this.panels[a])}h.disabled=d.map(h.disabled,function(k){return k>=a?++k:k});this._tabify();if(this.anchors.length==1){h.selected=0;e.addClass("ui-tabs-selected ui-state-active");j.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){c._trigger("show",null,c._ui(c.anchors[0],c.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[a],this.panels[a]));return this},remove:function(b){b=this._getIndex(b);var e=this.options,a=this.lis.eq(b).remove(),c=this.panels.eq(b).remove(); -if(a.hasClass("ui-tabs-selected")&&this.anchors.length>1)this.select(b+(b+1=b?--h:h});this._tabify();this._trigger("remove",null,this._ui(a.find("a")[0],c[0]));return this},enable:function(b){b=this._getIndex(b);var e=this.options;if(d.inArray(b,e.disabled)!=-1){this.lis.eq(b).removeClass("ui-state-disabled");e.disabled=d.grep(e.disabled,function(a){return a!=b});this._trigger("enable",null, -this._ui(this.anchors[b],this.panels[b]));return this}},disable:function(b){b=this._getIndex(b);var e=this.options;if(b!=e.selected){this.lis.eq(b).addClass("ui-state-disabled");e.disabled.push(b);e.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[b],this.panels[b]))}return this},select:function(b){b=this._getIndex(b);if(b==-1)if(this.options.collapsible&&this.options.selected!=-1)b=this.options.selected;else return this;this.anchors.eq(b).trigger(this.options.event+".tabs");return this}, -load:function(b){b=this._getIndex(b);var e=this,a=this.options,c=this.anchors.eq(b)[0],h=d.data(c,"load.tabs");this.abort();if(!h||this.element.queue("tabs").length!==0&&d.data(c,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(b).addClass("ui-state-processing");if(a.spinner){var j=d("span",c);j.data("label.tabs",j.html()).html(a.spinner)}this.xhr=d.ajax(d.extend({},a.ajaxOptions,{url:h,success:function(k,n){e.element.find(e._sanitizeSelector(c.hash)).html(k);e._cleanup();a.cache&&d.data(c, -"cache.tabs",true);e._trigger("load",null,e._ui(e.anchors[b],e.panels[b]));try{a.ajaxOptions.success(k,n)}catch(m){}},error:function(k,n){e._cleanup();e._trigger("load",null,e._ui(e.anchors[b],e.panels[b]));try{a.ajaxOptions.error(k,n,b,c)}catch(m){}}}));e.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]);this.panels.stop(false,true);this.element.queue("tabs",this.element.queue("tabs").splice(-2,2));if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup();return this}, -url:function(b,e){this.anchors.eq(b).removeData("cache.tabs").data("load.tabs",e);return this},length:function(){return this.anchors.length}});d.extend(d.ui.tabs,{version:"1.8.16"});d.extend(d.ui.tabs.prototype,{rotation:null,rotate:function(b,e){var a=this,c=this.options,h=a._rotate||(a._rotate=function(j){clearTimeout(a.rotation);a.rotation=setTimeout(function(){var k=c.selected;a.select(++k
      '))}function N(a){return a.bind("mouseout", -function(b){b=d(b.target).closest("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a");b.length&&b.removeClass("ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover")}).bind("mouseover",function(b){b=d(b.target).closest("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a");if(!(d.datepicker._isDisabledDatepicker(J.inline?a.parent()[0]:J.input[0])||!b.length)){b.parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); -b.addClass("ui-state-hover");b.hasClass("ui-datepicker-prev")&&b.addClass("ui-datepicker-prev-hover");b.hasClass("ui-datepicker-next")&&b.addClass("ui-datepicker-next-hover")}})}function H(a,b){d.extend(a,b);for(var c in b)if(b[c]==null||b[c]==C)a[c]=b[c];return a}d.extend(d.ui,{datepicker:{version:"1.8.16"}});var B=(new Date).getTime(),J;d.extend(M.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv}, -setDefaults:function(a){H(this._defaults,a||{});return this},_attachDatepicker:function(a,b){var c=null;for(var e in this._defaults){var f=a.getAttribute("date:"+e);if(f){c=c||{};try{c[e]=eval(f)}catch(h){c[e]=f}}}e=a.nodeName.toLowerCase();f=e=="div"||e=="span";if(!a.id){this.uuid+=1;a.id="dp"+this.uuid}var i=this._newInst(d(a),f);i.settings=d.extend({},b||{},c||{});if(e=="input")this._connectDatepicker(a,i);else f&&this._inlineDatepicker(a,i)},_newInst:function(a,b){return{id:a[0].id.replace(/([^A-Za-z0-9_-])/g, -"\\\\$1"),input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:!b?this.dpDiv:N(d('
      '))}},_connectDatepicker:function(a,b){var c=d(a);b.append=d([]);b.trigger=d([]);if(!c.hasClass(this.markerClassName)){this._attachments(c,b);c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker", -function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});this._autoSize(b);d.data(a,"datepicker",b);b.settings.disabled&&this._disableDatepicker(a)}},_attachments:function(a,b){var c=this._get(b,"appendText"),e=this._get(b,"isRTL");b.append&&b.append.remove();if(c){b.append=d(''+c+"");a[e?"before":"after"](b.append)}a.unbind("focus",this._showDatepicker);b.trigger&&b.trigger.remove();c=this._get(b,"showOn");if(c== -"focus"||c=="both")a.focus(this._showDatepicker);if(c=="button"||c=="both"){c=this._get(b,"buttonText");var f=this._get(b,"buttonImage");b.trigger=d(this._get(b,"buttonImageOnly")?d("").addClass(this._triggerClass).attr({src:f,alt:c,title:c}):d('').addClass(this._triggerClass).html(f==""?c:d("").attr({src:f,alt:c,title:c})));a[e?"before":"after"](b.trigger);b.trigger.click(function(){d.datepicker._datepickerShowing&&d.datepicker._lastInput==a[0]?d.datepicker._hideDatepicker(): -d.datepicker._showDatepicker(a[0]);return false})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var e=function(f){for(var h=0,i=0,g=0;gh){h=f[g].length;i=g}return i};b.setMonth(e(this._get(a,c.match(/MM/)?"monthNames":"monthNamesShort")));b.setDate(e(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a, -b){var c=d(a);if(!c.hasClass(this.markerClassName)){c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});d.data(a,"datepicker",b);this._setDate(b,this._getDefaultDate(b),true);this._updateDatepicker(b);this._updateAlternate(b);b.settings.disabled&&this._disableDatepicker(a);b.dpDiv.css("display","block")}},_dialogDatepicker:function(a,b,c,e,f){a=this._dialogInst;if(!a){this.uuid+= -1;this._dialogInput=d('');this._dialogInput.keydown(this._doKeyDown);d("body").append(this._dialogInput);a=this._dialogInst=this._newInst(this._dialogInput,false);a.settings={};d.data(this._dialogInput[0],"datepicker",a)}H(a.settings,e||{});b=b&&b.constructor==Date?this._formatDate(a,b):b;this._dialogInput.val(b);this._pos=f?f.length?f:[f.pageX,f.pageY]:null;if(!this._pos)this._pos=[document.documentElement.clientWidth/ -2-100+(document.documentElement.scrollLeft||document.body.scrollLeft),document.documentElement.clientHeight/2-150+(document.documentElement.scrollTop||document.body.scrollTop)];this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px");a.settings.onSelect=c;this._inDialog=true;this.dpDiv.addClass(this._dialogClass);this._showDatepicker(this._dialogInput[0]);d.blockUI&&d.blockUI(this.dpDiv);d.data(this._dialogInput[0],"datepicker",a);return this},_destroyDatepicker:function(a){var b= -d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();d.removeData(a,"datepicker");if(e=="input"){c.append.remove();c.trigger.remove();b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)}else if(e=="div"||e=="span")b.removeClass(this.markerClassName).empty()}},_enableDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e= -a.nodeName.toLowerCase();if(e=="input"){a.disabled=false;c.trigger.filter("button").each(function(){this.disabled=false}).end().filter("img").css({opacity:"1.0",cursor:""})}else if(e=="div"||e=="span"){b=b.children("."+this._inlineClass);b.children().removeClass("ui-state-disabled");b.find("select.ui-datepicker-month, select.ui-datepicker-year").removeAttr("disabled")}this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f})}},_disableDatepicker:function(a){var b=d(a),c=d.data(a, -"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=true;c.trigger.filter("button").each(function(){this.disabled=true}).end().filter("img").css({opacity:"0.5",cursor:"default"})}else if(e=="div"||e=="span"){b=b.children("."+this._inlineClass);b.children().addClass("ui-state-disabled");b.find("select.ui-datepicker-month, select.ui-datepicker-year").attr("disabled","disabled")}this._disabledInputs=d.map(this._disabledInputs,function(f){return f== -a?null:f});this._disabledInputs[this._disabledInputs.length]=a}},_isDisabledDatepicker:function(a){if(!a)return false;for(var b=0;b-1}},_doKeyUp:function(a){a=d.datepicker._getInst(a.target);if(a.input.val()!=a.lastVal)try{if(d.datepicker.parseDate(d.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,d.datepicker._getFormatConfig(a))){d.datepicker._setDateFromField(a);d.datepicker._updateAlternate(a);d.datepicker._updateDatepicker(a)}}catch(b){d.datepicker.log(b)}return true},_showDatepicker:function(a){a=a.target||a;if(a.nodeName.toLowerCase()!="input")a=d("input", -a.parentNode)[0];if(!(d.datepicker._isDisabledDatepicker(a)||d.datepicker._lastInput==a)){var b=d.datepicker._getInst(a);if(d.datepicker._curInst&&d.datepicker._curInst!=b){d.datepicker._datepickerShowing&&d.datepicker._triggerOnClose(d.datepicker._curInst);d.datepicker._curInst.dpDiv.stop(true,true)}var c=d.datepicker._get(b,"beforeShow");c=c?c.apply(a,[a,b]):{};if(c!==false){H(b.settings,c);b.lastVal=null;d.datepicker._lastInput=a;d.datepicker._setDateFromField(b);if(d.datepicker._inDialog)a.value= -"";if(!d.datepicker._pos){d.datepicker._pos=d.datepicker._findPos(a);d.datepicker._pos[1]+=a.offsetHeight}var e=false;d(a).parents().each(function(){e|=d(this).css("position")=="fixed";return!e});if(e&&d.browser.opera){d.datepicker._pos[0]-=document.documentElement.scrollLeft;d.datepicker._pos[1]-=document.documentElement.scrollTop}c={left:d.datepicker._pos[0],top:d.datepicker._pos[1]};d.datepicker._pos=null;b.dpDiv.empty();b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"});d.datepicker._updateDatepicker(b); -c=d.datepicker._checkOffset(b,c,e);b.dpDiv.css({position:d.datepicker._inDialog&&d.blockUI?"static":e?"fixed":"absolute",display:"none",left:c.left+"px",top:c.top+"px"});if(!b.inline){c=d.datepicker._get(b,"showAnim");var f=d.datepicker._get(b,"duration"),h=function(){var i=b.dpDiv.find("iframe.ui-datepicker-cover");if(i.length){var g=d.datepicker._getBorders(b.dpDiv);i.css({left:-g[0],top:-g[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})}};b.dpDiv.zIndex(d(a).zIndex()+1);d.datepicker._datepickerShowing= -true;d.effects&&d.effects[c]?b.dpDiv.show(c,d.datepicker._get(b,"showOptions"),f,h):b.dpDiv[c||"show"](c?f:null,h);if(!c||!f)h();b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus();d.datepicker._curInst=b}}}},_updateDatepicker:function(a){this.maxRows=4;var b=d.datepicker._getBorders(a.dpDiv);J=a;a.dpDiv.empty().append(this._generateHTML(a));var c=a.dpDiv.find("iframe.ui-datepicker-cover");c.length&&c.css({left:-b[0],top:-b[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}); -a.dpDiv.find("."+this._dayOverClass+" a").mouseover();b=this._getNumberOfMonths(a);c=b[1];a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");c>1&&a.dpDiv.addClass("ui-datepicker-multi-"+c).css("width",17*c+"em");a.dpDiv[(b[0]!=1||b[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi");a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl");a==d.datepicker._curInst&&d.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&& -!a.input.is(":disabled")&&a.input[0]!=document.activeElement&&a.input.focus();if(a.yearshtml){var e=a.yearshtml;setTimeout(function(){e===a.yearshtml&&a.yearshtml&&a.dpDiv.find("select.ui-datepicker-year:first").replaceWith(a.yearshtml);e=a.yearshtml=null},0)}},_getBorders:function(a){var b=function(c){return{thin:1,medium:2,thick:3}[c]||c};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var e=a.dpDiv.outerWidth(),f=a.dpDiv.outerHeight(), -h=a.input?a.input.outerWidth():0,i=a.input?a.input.outerHeight():0,g=document.documentElement.clientWidth+d(document).scrollLeft(),j=document.documentElement.clientHeight+d(document).scrollTop();b.left-=this._get(a,"isRTL")?e-h:0;b.left-=c&&b.left==a.input.offset().left?d(document).scrollLeft():0;b.top-=c&&b.top==a.input.offset().top+i?d(document).scrollTop():0;b.left-=Math.min(b.left,b.left+e>g&&g>e?Math.abs(b.left+e-g):0);b.top-=Math.min(b.top,b.top+f>j&&j>f?Math.abs(f+i):0);return b},_findPos:function(a){for(var b= -this._get(this._getInst(a),"isRTL");a&&(a.type=="hidden"||a.nodeType!=1||d.expr.filters.hidden(a));)a=a[b?"previousSibling":"nextSibling"];a=d(a).offset();return[a.left,a.top]},_triggerOnClose:function(a){var b=this._get(a,"onClose");if(b)b.apply(a.input?a.input[0]:null,[a.input?a.input.val():"",a])},_hideDatepicker:function(a){var b=this._curInst;if(!(!b||a&&b!=d.data(a,"datepicker")))if(this._datepickerShowing){a=this._get(b,"showAnim");var c=this._get(b,"duration"),e=function(){d.datepicker._tidyDialog(b); -this._curInst=null};d.effects&&d.effects[a]?b.dpDiv.hide(a,d.datepicker._get(b,"showOptions"),c,e):b.dpDiv[a=="slideDown"?"slideUp":a=="fadeIn"?"fadeOut":"hide"](a?c:null,e);a||e();d.datepicker._triggerOnClose(b);this._datepickerShowing=false;this._lastInput=null;if(this._inDialog){this._dialogInput.css({position:"absolute",left:"0",top:"-100px"});if(d.blockUI){d.unblockUI();d("body").append(this.dpDiv)}}this._inDialog=false}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")}, -_checkExternalClick:function(a){if(d.datepicker._curInst){a=d(a.target);a[0].id!=d.datepicker._mainDivId&&a.parents("#"+d.datepicker._mainDivId).length==0&&!a.hasClass(d.datepicker.markerClassName)&&!a.hasClass(d.datepicker._triggerClass)&&d.datepicker._datepickerShowing&&!(d.datepicker._inDialog&&d.blockUI)&&d.datepicker._hideDatepicker()}},_adjustDate:function(a,b,c){a=d(a);var e=this._getInst(a[0]);if(!this._isDisabledDatepicker(a[0])){this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"): -0),c);this._updateDatepicker(e)}},_gotoToday:function(a){a=d(a);var b=this._getInst(a[0]);if(this._get(b,"gotoCurrent")&&b.currentDay){b.selectedDay=b.currentDay;b.drawMonth=b.selectedMonth=b.currentMonth;b.drawYear=b.selectedYear=b.currentYear}else{var c=new Date;b.selectedDay=c.getDate();b.drawMonth=b.selectedMonth=c.getMonth();b.drawYear=b.selectedYear=c.getFullYear()}this._notifyChange(b);this._adjustDate(a)},_selectMonthYear:function(a,b,c){a=d(a);var e=this._getInst(a[0]);e["selected"+(c=="M"? -"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10);this._notifyChange(e);this._adjustDate(a)},_selectDay:function(a,b,c,e){var f=d(a);if(!(d(e).hasClass(this._unselectableClass)||this._isDisabledDatepicker(f[0]))){f=this._getInst(f[0]);f.selectedDay=f.currentDay=d("a",e).html();f.selectedMonth=f.currentMonth=b;f.selectedYear=f.currentYear=c;this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))}},_clearDate:function(a){a=d(a); -this._getInst(a[0]);this._selectDate(a,"")},_selectDate:function(a,b){a=this._getInst(d(a)[0]);b=b!=null?b:this._formatDate(a);a.input&&a.input.val(b);this._updateAlternate(a);var c=this._get(a,"onSelect");if(c)c.apply(a.input?a.input[0]:null,[b,a]);else a.input&&a.input.trigger("change");if(a.inline)this._updateDatepicker(a);else{this._hideDatepicker();this._lastInput=a.input[0];typeof a.input[0]!="object"&&a.input.focus();this._lastInput=null}},_updateAlternate:function(a){var b=this._get(a,"altField"); -if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),e=this._getDate(a),f=this.formatDate(c,e,this._getFormatConfig(a));d(b).each(function(){d(this).val(f)})}},noWeekends:function(a){a=a.getDay();return[a>0&&a<6,""]},iso8601Week:function(a){a=new Date(a.getTime());a.setDate(a.getDate()+4-(a.getDay()||7));var b=a.getTime();a.setMonth(0);a.setDate(1);return Math.floor(Math.round((b-a)/864E5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b=="object"? -b.toString():b+"";if(b=="")return null;var e=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff;e=typeof e!="string"?e:(new Date).getFullYear()%100+parseInt(e,10);for(var f=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,h=(c?c.dayNames:null)||this._defaults.dayNames,i=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,j=c=-1,l=-1,u=-1,k=false,o=function(p){(p=A+1-1){j=1;l=u;do{e=this._getDaysInMonth(c,j-1);if(l<=e)break;j++;l-=e}while(1)}v=this._daylightSavingAdjust(new Date(c,j-1,l));if(v.getFullYear()!=c||v.getMonth()+1!=j||v.getDate()!=l)throw"Invalid date";return v},ATOM:"yy-mm-dd", -COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1E7,formatDate:function(a,b,c){if(!b)return"";var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,h=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort;c=(c?c.monthNames: -null)||this._defaults.monthNames;var i=function(o){(o=k+1 -12?a.getHours()+2:0);return a},_setDate:function(a,b,c){var e=!b,f=a.selectedMonth,h=a.selectedYear;b=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=b.getDate();a.drawMonth=a.selectedMonth=a.currentMonth=b.getMonth();a.drawYear=a.selectedYear=a.currentYear=b.getFullYear();if((f!=a.selectedMonth||h!=a.selectedYear)&&!c)this._notifyChange(a);this._adjustInstDate(a);if(a.input)a.input.val(e?"":this._formatDate(a))},_getDate:function(a){return!a.currentYear||a.input&& -a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay))},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),e=this._get(a,"showButtonPanel"),f=this._get(a,"hideIfNoPrevNext"),h=this._get(a,"navigationAsDateFormat"),i=this._getNumberOfMonths(a),g=this._get(a,"showCurrentAtPos"),j=this._get(a,"stepMonths"),l=i[0]!=1||i[1]!=1,u=this._daylightSavingAdjust(!a.currentDay? -new Date(9999,9,9):new Date(a.currentYear,a.currentMonth,a.currentDay)),k=this._getMinMaxDate(a,"min"),o=this._getMinMaxDate(a,"max");g=a.drawMonth-g;var m=a.drawYear;if(g<0){g+=12;m--}if(o){var n=this._daylightSavingAdjust(new Date(o.getFullYear(),o.getMonth()-i[0]*i[1]+1,o.getDate()));for(n=k&&nn;){g--;if(g<0){g=11;m--}}}a.drawMonth=g;a.drawYear=m;n=this._get(a,"prevText");n=!h?n:this.formatDate(n,this._daylightSavingAdjust(new Date(m,g-j,1)),this._getFormatConfig(a)); -n=this._canAdjustMonth(a,-1,m,g)?''+n+"":f?"":''+n+"";var s=this._get(a,"nextText");s=!h?s:this.formatDate(s,this._daylightSavingAdjust(new Date(m, -g+j,1)),this._getFormatConfig(a));f=this._canAdjustMonth(a,+1,m,g)?''+s+"":f?"":''+s+"";j=this._get(a,"currentText");s=this._get(a,"gotoCurrent")&& -a.currentDay?u:b;j=!h?j:this.formatDate(j,s,this._getFormatConfig(a));h=!a.inline?'":"";e=e?'
      '+(c?h:"")+(this._isInRange(a,s)?'":"")+(c?"":h)+"
      ":"";h=parseInt(this._get(a,"firstDay"),10);h=isNaN(h)?0:h;j=this._get(a,"showWeek");s=this._get(a,"dayNames");this._get(a,"dayNamesShort");var q=this._get(a,"dayNamesMin"),A=this._get(a,"monthNames"),v=this._get(a,"monthNamesShort"),p=this._get(a,"beforeShowDay"),D=this._get(a,"showOtherMonths"),K=this._get(a,"selectOtherMonths");this._get(a,"calculateWeek");for(var E=this._getDefaultDate(a),w="",x=0;x1)switch(G){case 0:y+=" ui-datepicker-group-first";t=" ui-corner-"+(c?"right":"left");break;case i[1]-1:y+=" ui-datepicker-group-last";t=" ui-corner-"+(c?"left":"right");break;default:y+=" ui-datepicker-group-middle";t="";break}y+='">'}y+='
      '+(/all|left/.test(t)&& -x==0?c?f:n:"")+(/all|right/.test(t)&&x==0?c?n:f:"")+this._generateMonthYearHeader(a,g,m,k,o,x>0||G>0,A,v)+'
      ';var z=j?'":"";for(t=0;t<7;t++){var r=(t+h)%7;z+="=5?' class="ui-datepicker-week-end"':"")+'>'+q[r]+""}y+=z+"";z=this._getDaysInMonth(m,g);if(m==a.selectedYear&&g==a.selectedMonth)a.selectedDay=Math.min(a.selectedDay, -z);t=(this._getFirstDayOfMonth(m,g)-h+7)%7;z=Math.ceil((t+z)/7);this.maxRows=z=l?this.maxRows>z?this.maxRows:z:z;r=this._daylightSavingAdjust(new Date(m,g,1-t));for(var Q=0;Q";var R=!j?"":'";for(t=0;t<7;t++){var I=p?p.apply(a.input?a.input[0]:null,[r]):[true,""],F=r.getMonth()!=g,L=F&&!K||!I[0]||k&&ro;R+='";r.setDate(r.getDate()+1);r=this._daylightSavingAdjust(r)}y+=R+""}g++;if(g>11){g=0;m++}y+="
      '+this._get(a,"weekHeader")+"
      '+this._get(a,"calculateWeek")(r)+""+(F&&!D?" ":L?''+ -r.getDate()+"":''+r.getDate()+"")+"
      "+(l?"
      "+(i[0]>0&&G==i[1]-1?'
      ':""):"");O+=y}w+=O}w+=e+(d.browser.msie&&parseInt(d.browser.version,10)<7&&!a.inline?'': -"");a._keyEvent=false;return w},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var j=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),k='
      ',o="";if(h||!j)o+=''+i[b]+"";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='"}u||(k+=o+(h||!(j&&l)?" ":""));if(!a.yearshtml){a.yearshtml="";if(h||!l)k+=''+c+"";else{g=this._get(a,"yearRange").split(":");var s=(new Date).getFullYear();i=function(q){q=q.match(/c[+-].*/)?c+parseInt(q.substring(1),10):q.match(/[+-].*/)?s+parseInt(q,10):parseInt(q,10);return isNaN(q)?s:q};b=i(g[0]);g=Math.max(b,i(g[1]||""));b=e?Math.max(b, -e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(a.yearshtml+='";k+=a.yearshtml;a.yearshtml=null}}k+=this._get(a,"yearSuffix");if(u)k+=(h||!(j&&l)?" ":"")+o;k+="
      ";return k},_adjustInstDate:function(a,b,c){var e=a.drawYear+(c=="Y"?b:0),f=a.drawMonth+ -(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&ba?a:b},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");if(b)b.apply(a.input? -a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a);c=this._daylightSavingAdjust(new Date(c, -e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a, -"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker=function(a){if(!this.length)return this; -if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));return this.each(function(){typeof a== -"string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new M;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.16";window["DP_jQuery_"+B]=d})(jQuery); -;/* - * jQuery UI Progressbar 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Progressbar - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - */ -(function(b,d){b.widget("ui.progressbar",{options:{value:0,max:100},min:0,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.options.max,"aria-valuenow":this._value()});this.valueDiv=b("
      ").appendTo(this.element);this.oldValue=this._value();this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"); -this.valueDiv.remove();b.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===d)return this._value();this._setOption("value",a);return this},_setOption:function(a,c){if(a==="value"){this.options.value=c;this._refreshValue();this._value()===this.options.max&&this._trigger("complete")}b.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;if(typeof a!=="number")a=0;return Math.min(this.options.max,Math.max(this.min,a))},_percentage:function(){return 100* -this._value()/this.options.max},_refreshValue:function(){var a=this.value(),c=this._percentage();if(this.oldValue!==a){this.oldValue=a;this._trigger("change")}this.valueDiv.toggle(a>this.min).toggleClass("ui-corner-right",a===this.options.max).width(c.toFixed(0)+"%");this.element.attr("aria-valuenow",a)}});b.extend(b.ui.progressbar,{version:"1.8.16"})})(jQuery); -;/* - * jQuery UI Effects 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/ - */ -jQuery.effects||function(f,j){function m(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1], -16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return n.transparent;return n[f.trim(c).toLowerCase()]}function s(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return m(b)}function o(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle, -a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function p(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in t||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function u(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function k(c,a,b,d){if(typeof c=="object"){d= -a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:b in f.fx.speeds?f.fx.speeds[b]:f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}function l(c){if(!c||typeof c==="number"||f.fx.speeds[c])return true;if(typeof c==="string"&&!f.effects[c])return true;return false}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor", -"borderTopColor","borderColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=s(b.elem,a);b.end=m(b.end);b.colorInit=true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var n={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0, -0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211, -211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},q=["add","remove","toggle"],t={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b, -d){if(f.isFunction(b)){d=b;b=null}return this.queue(function(){var e=f(this),g=e.attr("style")||" ",h=p(o.call(this)),r,v=e.attr("class");f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});r=p(o.call(this));e.attr("class",v);e.animate(u(h,r),{queue:false,duration:a,easing:b,complete:function(){f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments);f.dequeue(this)}})})}; -f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this, -[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.16",save:function(c,a){for(var b=0;b").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}), -d=document.activeElement;c.wrap(b);if(c[0]===d||f.contains(c[0],d))f(d).focus();b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(e,g){a[g]=c.css(g);if(isNaN(parseInt(a[g],10)))a[g]="auto"});c.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})}return b.css(a).show()},removeWrapper:function(c){var a,b=document.activeElement; -if(c.parent().is(".ui-effects-wrapper")){a=c.parent().replaceWith(c);if(c[0]===b||f.contains(c[0],b))f(b).focus();return a}return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments),b={options:a[1],duration:a[2],callback:a[3]};a=b.options.mode;var d=f.effects[c];if(f.fx.off||!d)return a?this[a](b.duration,b.callback):this.each(function(){b.callback&&b.callback.call(this)}); -return d.call(this,b)},_show:f.fn.show,show:function(c){if(l(c))return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(l(c))return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(l(c)||typeof c==="boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this, -arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/ -2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b, -d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c, -a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b, -d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ -e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); -;/* - * jQuery UI Effects Fade 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Fade - * - * Depends: - * jquery.effects.core.js - */ -(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); -;/* - * jQuery UI Effects Fold 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Fold - * - * Depends: - * jquery.effects.core.js - */ -(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","bottom","left","right"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1], -10)/100*f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); -;/* - * jQuery UI Effects Highlight 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Highlight - * - * Depends: - * jquery.effects.core.js - */ -(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& -this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); -;/* - * jQuery UI Effects Pulsate 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Pulsate - * - * Depends: - * jquery.effects.core.js - */ -(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); -b.dequeue()})})}})(jQuery); -; \ No newline at end of file diff --git a/public/site_assets/test/js/dist/examples/jquery-ui/js/jquery.effects.blind.min.js b/public/site_assets/test/js/dist/examples/jquery-ui/js/jquery.effects.blind.min.js deleted file mode 100644 index 101c15d4..00000000 --- a/public/site_assets/test/js/dist/examples/jquery-ui/js/jquery.effects.blind.min.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * jQuery UI Effects Blind 1.9pre - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Blind - * - * Depends: - * jquery.effects.core.js - */ -(function(b){var n=/up|down|vertical/,o=/up|left|vertical|horizontal/;b.effects.effect.blind=function(g,p){var a=b(this),i=["position","top","bottom","left","right","height","width"],l=b.effects.setMode(a,g.mode||"hide"),e=g.direction||"up",f=n.test(e),h=f?"height":"width",m=f?"top":"left";e=o.test(e);var j={},k=l==="show",c,d;a.parent().is(".ui-effects-wrapper")?b.effects.save(a.parent(),i):b.effects.save(a,i);a.show();d=parseInt(a.css("top"),10);c=b.effects.createWrapper(a).css({overflow:"hidden"}); -d=f?c[h]()+d:c[h]();j[h]=k?d:0;if(!e){a.css(f?"bottom":"right",0).css(f?"top":"left","").css({position:"absolute"});j[m]=k?0:d}if(k){c.css(h,0);e||c.css(m,d)}c.animate(j,{duration:g.duration,easing:g.easing,queue:false,complete:function(){l==="hide"&&a.hide();b.effects.restore(a,i);b.effects.removeWrapper(a);p()}})}})(jQuery); diff --git a/public/site_assets/test/js/dist/examples/jquery-ui/js/jquery.effects.core.min.js b/public/site_assets/test/js/dist/examples/jquery-ui/js/jquery.effects.core.min.js deleted file mode 100644 index 9e92123a..00000000 --- a/public/site_assets/test/js/dist/examples/jquery-ui/js/jquery.effects.core.min.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * jQuery UI Effects 1.9pre - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/ - */ -jQuery.effects||function(f,m){function r(c){var a;if(c&&c.constructor===Array&&c.length===3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1], -16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return s.transparent;return s[f.trim(c).toLowerCase()]}function t(){var c=this.ownerDocument.defaultView?this.ownerDocument.defaultView.getComputedStyle(this,null):this.currentStyle,a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(d=c.length;d--;){b=c[d];if(typeof c[b]==="string")a[f.camelCase(b)]=c[b]}else for(b in c)if(typeof c[b]=== -"string")a[b]=c[b];return a}function o(c,a,b,d){if(f.isPlainObject(c))return c;c={effect:c};if(a===m)a={};if(f.isFunction(a)){d=a;b=null;a={}}if(f.type(a)==="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a&&f.extend(c,a);b=b||a.duration;c.duration=f.fx.off?0:typeof b==="number"?b:b in f.fx.speeds?f.fx.speeds[b]:f.fx.speeds._default;c.complete=d||a.complete;return c}function q(c){if(!c||typeof c==="number"||f.fx.speeds[c])return true;if(typeof c==="string"&&!f.effects.effect[c]){if(u&& -f.effects[c])return false;return true}return false}var u=f.uiBackCompat!==false;f.effects={effect:{}};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","borderColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){var d;d=b.elem;var e=a,g;do{g=f.curCSS(d,e);if(g!=""&&g!=="transparent"||f.nodeName(d,"body"))break;e="backgroundColor"}while(d=d.parentNode);d=r(g);b.start=d;b.end=r(b.end);b.colorInit=true}b.elem.style[a]= -"rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var s={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139, -0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192, -203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},w=["add","remove","toggle"],x={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(c,a){f.fx.step[a]=function(b){if(b.end!=="none"&&!b.setAttr||b.pos===1&&!b.setAttr){jQuery.style(b.elem,a,b.end);b.setAttr= -true}}});f.effects.animateClass=function(c,a,b,d){var e=f.speed(a,b,d);return this.queue(function(){var g=f(this),h=g.attr("class")||"",n,j=e.children?g.find("*").andSelf():g;j=j.map(function(){var k=f(this);return{el:k,originalStyleAttr:k.attr("style")||" ",start:t.call(this)}});f.each(w,function(k,i){if(c[i])g[i+"Class"](c[i])});n=g.attr("class");j=j.map(function(){this.end=t.call(this.el[0]);var k=this.start,i=this.end,v={},l,p;for(l in i){p=i[l];if(k[l]!=p)if(!x[l])if(f.fx.step[l]||!isNaN(parseFloat(p)))v[l]= -p}this.diff=v;return this});g.attr("class",h);j=j.map(function(){var k=this,i=f.Deferred();this.el.animate(this.diff,{duration:e.duration,easing:e.easing,queue:false,complete:function(){i.resolve(k)}});return i.promise()});f.when.apply(f,j.get()).done(function(){g.attr("class",n);f.each(arguments,function(){if(typeof this.el.attr("style")==="object"){this.el.attr("style").cssText="";this.el.attr("style").cssText=this.originalStyleAttr}else this.el.attr("style",this.originalStyleAttr)});e.complete.call(g[0])})})}; -f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a==="boolean"||a===m?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this, -[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.9pre",save:function(c,a){for(var b=0;b").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}), -d={width:c.width(),height:c.height()},e=document.activeElement;c.wrap(b);if(c[0]===e||f.contains(c[0],e))f(e).focus();b=c.parent();if(c.css("position")==="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(g,h){a[h]=c.css(h);if(isNaN(parseInt(a[h],10)))a[h]="auto"});c.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})}c.css(d);return b.css(a).show()}, -removeWrapper:function(c){var a=document.activeElement;if(c.parent().is(".ui-effects-wrapper")){c.parent().replaceWith(c);if(c[0]===a||f.contains(c[0],a))f(a).focus()}return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){var h=c.cssUnit(g);if(h[0]>0)d[g]=h[0]*b+h[1]});return d}});f.fn.extend({effect:function(){function c(h){function n(){f.isFunction(k)&&k.call(j[0]);f.isFunction(h)&&h()}var j=f(this),k=a.complete,i=a.mode;(j.is(":hidden")?i==="hide":i==="show")?n():e.call(j[0], -a,n)}var a=o.apply(this,arguments),b=a.mode,d=a.queue,e=f.effects.effect[a.effect],g=!e&&u&&f.effects[a.effect];if(f.fx.off||!(e||g))return b?this[b](a.duration,a.complete):this.each(function(){a.complete&&a.complete.call(this)});return e?d===false?this.each(c):this.queue(d||"fx",c):g.call(this,{options:a,duration:a.duration,callback:a.complete,mode:a.mode})},_show:f.fn.show,show:function(c){if(q(c))return this._show.apply(this,arguments);else{var a=o.apply(this,arguments);a.mode="show";return this.effect.call(this, -a)}},_hide:f.fn.hide,hide:function(c){if(q(c))return this._hide.apply(this,arguments);else{var a=o.apply(this,arguments);a.mode="hide";return this.effect.call(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(q(c)||typeof c==="boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=o.apply(this,arguments);a.mode="toggle";return this.effect.call(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a), -e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a, -b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2* -((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+ -b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=e*0.3,h=d;if(a==0)return b;if((a/=e)==1)return b+d;if(h - - -// Create a jquery plugin that prints the given element. -jQuery.fn.print = function(){ - // NOTE: We are trimming the jQuery collection down to the - // first element in the collection. - if (this.size() > 1){ - this.eq( 0 ).print(); - return; - } else if (!this.size()){ - return; - } - - var chart = $(this).closest('div.quintile-outer-container').find('div.jqplot-target'); - // var imgelem = chart.jqplotToImageElem(); - var imageElemStr = chart.jqplotToImageElemStr(); - // var statsrows = $(this).closest('div.quintile-outer-container').find('table.stats-table tr'); - var statsTable = $('
      ').append($(this).closest('div.quintile-outer-container').find('table.stats-table').clone()); - // var rowstyles = window.getComputedStyle(statsrows.get(0), ''); - - // ASSERT: At this point, we know that the current jQuery - // collection (as defined by THIS), contains only one - // printable element. - - // Create a random name for the print frame. - var strFrameName = ("printer-" + (new Date()).getTime()); - - // Create an iFrame with the new name. - var jFrame = $( "