├── CHANGELOG.md ├── Makefile ├── README.md ├── app ├── code │ └── community │ │ └── HE │ │ └── TwoFactorAuth │ │ ├── Block │ │ ├── Adminhtml │ │ │ └── System │ │ │ │ └── Config │ │ │ │ └── Fieldset │ │ │ │ └── Hint.php │ │ └── Validate.php │ │ ├── Helper │ │ └── Data.php │ │ ├── Model │ │ ├── Observer.php │ │ ├── Resource │ │ │ └── Mysql4 │ │ │ │ └── Setup.php │ │ ├── Sysconfig │ │ │ ├── Appkey.php │ │ │ └── Provider.php │ │ ├── Validate.php │ │ └── Validate │ │ │ ├── Duo.php │ │ │ ├── Duo │ │ │ └── Request.php │ │ │ └── Google.php │ │ ├── controllers │ │ └── Adminhtml │ │ │ └── TwofactorController.php │ │ ├── etc │ │ ├── adminhtml.xml │ │ ├── config.xml │ │ └── system.xml │ │ ├── readme.txt │ │ └── sql │ │ └── he_twofactorauth │ │ └── mysql4-install-1.0.2.php ├── design │ └── adminhtml │ │ └── default │ │ └── default │ │ ├── layout │ │ └── he_twofactor │ │ │ └── auth.xml │ │ └── template │ │ └── he_twofactor │ │ ├── duo │ │ └── auth.phtml │ │ ├── google │ │ └── auth.phtml │ │ └── system │ │ └── config │ │ └── fieldset │ │ └── hint.phtml └── etc │ └── modules │ └── HE_TwoFactorAuth.xml ├── build ├── build_package.py └── mage-package.xml ├── js └── he_twofactor │ ├── Duo-Web-v1.bundled.js │ ├── Duo-Web-v1.bundled.min.js │ ├── Duo-Web-v1.js │ ├── adminpanel.js │ └── readme.txt ├── lib ├── Duo │ └── duo_web.php └── GoogleAuthenticator │ └── PHPGangsta │ └── GoogleAuthenticator.php ├── modman ├── skin └── adminhtml │ └── default │ └── default │ └── he_twofactor │ ├── css │ └── admin.css │ └── images │ ├── he_nexcess_logos.gif │ ├── icon_lock_left_menu.svg │ └── icon_sentry.gif └── util └── get-version.sh /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nexcess/magento-sentry-two-factor-authentication/bffb34f2b883dc4ff5c76027080a9b2dd3c9dd19/CHANGELOG.md -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | .PHONY: connect-desc connect-pkg all clean 3 | 4 | connect-desc: 5 | grep -v 'Build Status' README.md | markdown_py -o html5 -f "build/magento-connect-desc-$(shell ./util/get-version.sh).html" 6 | 7 | connect-changelog: 8 | markdown_py -o html5 -f "build/magento-connect-changelog-$(shell ./util/get-version.sh).html" CHANGELOG.md 9 | 10 | connect-pkg: 11 | ./build/build_package.py -d build/mage-package.xml 12 | 13 | all: connect-desc connect-changelog connect-pkg 14 | 15 | clean: 16 | rm -f ./build/*.tgz ./build/*.html ./package.xml 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sentry 2 | ## Magento Two Factor Authentication Module 3 | 4 | ### Authors 5 | - Greg Croasdill, Human Element, Inc http://www.human-element.com 6 | - Gregg Milligan, Human Element, Inc http://www.human-element.com 7 | - Aric Watson, Nexcess.net https://www.nexcess.net/ 8 | 9 | ### License 10 | - GPL -- https://www.gnu.org/copyleft/gpl.html 11 | 12 | ### Purpose 13 | Sentry Two-Factor Authentication will protect your Magento store and customer data by adding an extra check to authenticate your Admin users before allowing them access. Developed as a partnership between the Human Element Magento Development team and Nexcess Hosting, Sentry Two-Factor Authentication for Magento is easy to setup and admin users can quickly login. 14 | 15 | ### Supported Providers (more to come) 16 | The following __Two Factor Authentication__ providers are supported at this time. 17 | #### Duo Security 18 | For more information on Duo security's API, please see - 19 | - https://www.duosecurity.com 20 | 21 | #### Google Authenticator 22 | For more information on Google Authenticator, please see - 23 | - https://github.com/google/google-authenticator/wiki 24 | - https://support.google.com/accounts/answer/180744?hl=en&ref_topic=1099588 25 | 26 | 27 | ## Support 28 | 29 | If you have an issue, please read the [FAQ](https://github.com/nexcess/magento-sentry-two-factor-authentication/wiki/FAQ) 30 | then if you still need help, open a bug report in GitHub's 31 | [issue tracker](https://github.com/nexcess/magento-sentry-two-factor-authentication/issues). 32 | 33 | Please do not use Magento Connect's Reviews or (especially) the Q&A for support. 34 | There isn't a way for me to reply to reviews and the Q&A moderation is very slow. 35 | 36 | ## Contributing 37 | 38 | If you have a fix or feature for this module, submit a pull request through GitHub 39 | to the **devel** branch. The *master* branch is only for stable releases. Please 40 | make sure the new code follows the same style and conventions as already written 41 | code. 42 | 43 | ### Referenced work 44 | 45 | Some code based on previous work by Jonathan Day jonathan@aligent.com.au 46 | - https://github.com/magento-hackathon/Magento-Two-factor-Authentication 47 | 48 | Some code based on previous work by Michael Kliewe/PHPGangsta 49 | - https://github.com/PHPGangsta/GoogleAuthenticator 50 | - http://www.phpgangsta.de/ 51 | 52 | ---- 53 | ### Notes - 54 | 1. Installing this module will update the admin_user table in the Magento database to add a twofactor_google_secret 55 | field for storing the local GA key. It is safe to remove this field once the module is removed. 56 | 1. If you get locked out of admin because of a settings issue, loss of your provider account or other software related issue, you can *temporarily disable* the second factor authentication - 57 | - Place a file named __tfaoff.flag__ in the root directory of your Magento installation. 58 | - Login to Magento's Admin area without the second factor. 59 | - Update settings or disable Sentry 60 | - Remove the tfaoff.flag file to re-enable two factor authentication. 61 | -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/Block/Adminhtml/System/Config/Fieldset/Hint.php: -------------------------------------------------------------------------------- 1 | toHtml(); 22 | } 23 | 24 | public function getTwoFactorAuthVersion() 25 | { 26 | return (string)Mage::getConfig()->getNode('modules/HE_TwoFactorAuth/version'); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/Block/Validate.php: -------------------------------------------------------------------------------- 1 | getUrl('twofactorauth/interstitial/verify'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/Helper/Data.php: -------------------------------------------------------------------------------- 1 | _provider = Mage::getStoreConfig('he2faconfig/control/provider'); 19 | $this->_logging = Mage::getStoreConfig('he2faconfig/control/logging'); 20 | $this->_logAccess = Mage::getStoreConfig('he2faconfig/control/logaccess'); 21 | $this->_ipWhitelist = $this->getIPWhitelist(); 22 | } 23 | 24 | public function isDisabled() 25 | { 26 | $tfaFlag = Mage::getBaseDir('base') . '/tfaoff.flag'; 27 | 28 | if (file_exists($tfaFlag)) { 29 | if ($this->shouldLog()) { 30 | Mage::log("isDisabled - Found tfaoff.flag, TFA disabled.", 0, "two_factor_auth.log"); 31 | } 32 | 33 | return true; 34 | } 35 | 36 | if (!$this->_provider || $this->_provider == 'disabled') { 37 | return true; 38 | } 39 | 40 | $method = Mage::getSingleton('he_twofactorauth/validate_' . $this->_provider); 41 | 42 | if (!$method) { 43 | return true; 44 | } 45 | 46 | return !$method->isValid(); 47 | } 48 | 49 | public function getProvider() 50 | { 51 | return $this->_provider; 52 | } 53 | 54 | 55 | public function shouldLog() 56 | { 57 | return $this->_logging; 58 | } 59 | 60 | public function shouldLogAccess() 61 | { 62 | return $this->_logAccess; 63 | } 64 | 65 | public function disable2FA() 66 | { 67 | Mage::getModel('core/config')->saveConfig('he2faconfig/control/provider', 'disabled'); 68 | Mage::app()->getStore()->resetConfig(); 69 | } 70 | 71 | private function getIPWhitelist() 72 | { 73 | $return = []; 74 | $ips = preg_split("/\r\n|\n|\r/", trim(Mage::getStoreConfig('he2faconfig/control/ipwhitelist'))); 75 | foreach ($ips as $ip) { 76 | if (filter_var($ip, FILTER_VALIDATE_IP)) { 77 | $return[] = trim($ip); 78 | } 79 | } 80 | return $return; 81 | } 82 | 83 | 84 | public function inWhitelist($ip) 85 | { 86 | if (count($this->_ipWhitelist) == 0) { return false; } 87 | 88 | if (in_array( $ip, $this->_ipWhitelist )) { 89 | if ( $this->shouldLogAccess() ) { 90 | Mage::log("TFA bypassed for IP $ip - whitelisted", 0, "two_factor_auth.log"); 91 | } 92 | return true; 93 | } 94 | else { 95 | return false; 96 | } 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/Model/Observer.php: -------------------------------------------------------------------------------- 1 | _shouldLog = Mage::helper('he_twofactorauth')->shouldLog(); 23 | } 24 | 25 | public function admin_user_authenticate_after($observer) 26 | { 27 | if (Mage::helper('he_twofactorauth')->isDisabled()) { 28 | return; 29 | } 30 | 31 | // check ip-whitelist 32 | if (Mage::helper('he_twofactorauth')->inWhitelist( Mage::helper('core/http')->getRemoteAddr() )) { 33 | Mage::getSingleton('admin/session')->set2faState(HE_TwoFactorAuth_Model_Validate::TFA_STATE_ACTIVE); 34 | } 35 | 36 | if (Mage::getSingleton('admin/session')->get2faState() != HE_TwoFactorAuth_Model_Validate::TFA_STATE_ACTIVE) { 37 | 38 | if ($this->_shouldLog) { 39 | Mage::log("authenticate_after - get2faState is not active", 0, "two_factor_auth.log"); 40 | } 41 | 42 | // set we are processing 2f login 43 | Mage::getSingleton('admin/session')->set2faState(HE_TwoFactorAuth_Model_Validate::TFA_STATE_PROCESSING); 44 | 45 | $provider = Mage::helper('he_twofactorauth/data')->getProvider(); 46 | 47 | //redirect to the 2f login page 48 | $twoFactAuthPage = Mage::helper("adminhtml")->getUrl("adminhtml/twofactor/$provider"); 49 | 50 | if ($this->_shouldLog) { 51 | Mage::log("authenticate_after - redirect to $twoFactAuthPage", 0, "two_factor_auth.log"); 52 | } 53 | 54 | Mage::app()->getResponse() 55 | ->setRedirect($twoFactAuthPage) 56 | ->sendResponse(); 57 | exit(); 58 | } else { 59 | if ($this->_shouldLog) { 60 | Mage::log("authenticate_after - getValid2Fa is true", 0, "two_factor_auth.log"); 61 | } 62 | } 63 | } 64 | 65 | /*** 66 | * controller to check for valid 2fa 67 | * admin states 68 | * 69 | * @param $observer 70 | */ 71 | 72 | public function check_twofactor_active($observer) 73 | { 74 | if (Mage::helper('he_twofactorauth')->isDisabled()) { 75 | return; 76 | } 77 | 78 | $request = $observer->getControllerAction()->getRequest(); 79 | $tfaState = Mage::getSingleton('admin/session')->get2faState(); 80 | $action = Mage::app()->getRequest()->getActionName(); 81 | 82 | switch ($tfaState) { 83 | case HE_TwoFactorAuth_Model_Validate::TFA_STATE_NONE: 84 | if ($this->_shouldLog) { 85 | Mage::log("check_twofactor_active - tfa state none", 0, "two_factor_auth.log"); 86 | } 87 | break; 88 | case HE_TwoFactorAuth_Model_Validate::TFA_STATE_PROCESSING: 89 | if ($this->_shouldLog) { 90 | Mage::log("check_twofactor_active - tfa state processing", 0, "two_factor_auth.log"); 91 | } 92 | break; 93 | case HE_TwoFactorAuth_Model_Validate::TFA_STATE_ACTIVE: 94 | if ($this->_shouldLog) { 95 | Mage::log("check_twofactor_active - tfa state active", 0, "two_factor_auth.log"); 96 | } 97 | break; 98 | default: 99 | if ($this->_shouldLog) { 100 | Mage::log("check_twofactor_active - tfa state unknown - " . $tfaState, 0, "two_factor_auth.log"); 101 | } 102 | } 103 | 104 | if ($action == 'logout') { 105 | if ($this->_shouldLog) { 106 | Mage::log("check_twofactor_active - logout", 0, "two_factor_auth.log"); 107 | } 108 | Mage::getSingleton('admin/session')->set2faState(HE_TwoFactorAuth_Model_Validate::TFA_STATE_NONE); 109 | 110 | return $this; 111 | } 112 | 113 | if (in_array($action, $this->_allowedActions)) { 114 | return $this; 115 | } 116 | 117 | if ($request->getControllerName() == 'twofactor' 118 | || $tfaState == HE_TwoFactorAuth_Model_Validate::TFA_STATE_ACTIVE 119 | ) { 120 | if ($this->_shouldLog) { 121 | Mage::log( 122 | "check_twofactor_active - return controller twofactor or is active", 0, "two_factor_auth.log" 123 | ); 124 | } 125 | 126 | return $this; 127 | } 128 | 129 | if (Mage::getSingleton('admin/session')->get2faState() != HE_TwoFactorAuth_Model_Validate::TFA_STATE_ACTIVE) { 130 | 131 | if ($this->_shouldLog) { 132 | Mage::log("check_twofactor_active - not active, try again", 0, "two_factor_auth.log"); 133 | } 134 | 135 | $msg = Mage::helper('he_twofactorauth')->__( 136 | 'You must complete Two Factor Authentication before accessing Magento administration' 137 | ); 138 | Mage::getSingleton('adminhtml/session')->addError($msg); 139 | 140 | // set we are processing 2f login 141 | Mage::getSingleton('admin/session')->set2faState(HE_TwoFactorAuth_Model_Validate::TFA_STATE_PROCESSING); 142 | 143 | $provider = Mage::helper('he_twofactorauth')->getProvider(); 144 | $twoFactAuthPage = Mage::helper("adminhtml")->getUrl("adminhtml/twofactor/$provider"); 145 | 146 | //disable the dispatch for now 147 | $request = Mage::app()->getRequest(); 148 | $action = $request->getActionName(); 149 | Mage::app()->getFrontController() 150 | ->getAction() 151 | ->setFlag($action, Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true); 152 | 153 | $response = Mage::app()->getResponse(); 154 | 155 | if ($this->_shouldLog) { 156 | Mage::log("check_twofactor_active - redirect to $twoFactAuthPage", 0, "two_factor_auth.log"); 157 | } 158 | 159 | $response->setRedirect($twoFactAuthPage)->sendResponse(); 160 | exit(); 161 | } 162 | } 163 | 164 | /* 165 | * Add a fieldset and field to the admin edit user form 166 | * in order to allow selective clearing of a users shared secret (google) 167 | */ 168 | 169 | public function googleClearSecretCheck(Varien_Event_Observer $observer) 170 | { 171 | $block = $observer->getEvent()->getBlock(); 172 | 173 | if (!isset($block)) { 174 | return $this; 175 | } 176 | 177 | if ($block->getType() == 'adminhtml/permissions_user_edit_form') { 178 | 179 | // check that google is set for twofactor authentication 180 | if (Mage::helper('he_twofactorauth')->getProvider() == 'google') { 181 | //create new custom fieldset 'website' 182 | $form = $block->getForm(); 183 | $fieldset = $form->addFieldset( 184 | 'website_field', array( 185 | 'legend' => 'Google Authenticator', 186 | 'class' => 'fieldset-wide' 187 | ) 188 | ); 189 | 190 | $fieldset->addField( 191 | 'checkbox', 'checkbox', array( 192 | 'label' => Mage::helper('he_twofactorauth')->__( 193 | 'Reset Google Authenticator' 194 | ), 195 | 'name' => 'clear_google_secret', 196 | 'checked' => false, 197 | 'onclick' => "", 198 | 'onchange' => "", 199 | 'value' => '1', 200 | 'disabled' => false, 201 | 'after_element_html' => 'Check this and save to reset this user\'s Google Authenticator.
They will need to use the QR code to reconnect their device after their next successful login.
', 202 | 'tabindex' => 1 203 | ) 204 | ); 205 | } 206 | } 207 | } 208 | 209 | 210 | /* 211 | * Clear a user's google secret field if request 212 | * 213 | */ 214 | public function googleSaveClear(Varien_Event_Observer $observer) 215 | { 216 | // check that a user record has been saved 217 | 218 | // if google is turned and 2fa active... 219 | if (Mage::helper('he_twofactorauth')->getProvider() == 'google') { 220 | $params = Mage::app()->getRequest()->getParams(); 221 | if (isset($params['clear_google_secret'])) { 222 | if ($params['clear_google_secret'] == 1) { 223 | $object = $observer->getEvent()->getObject(); 224 | $object->setTwofactorGoogleSecret(''); // just clear the secret 225 | 226 | Mage::log( 227 | "Clearing google secret for admin user (" . $object->getUsername() . ")", 0, 228 | "two_factor_auth.log" 229 | ); 230 | } 231 | } 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/Model/Resource/Mysql4/Setup.php: -------------------------------------------------------------------------------- 1 | getValue(); //get the value from our config 19 | 20 | if(strlen($appkey) < 40) //exit if we're less than 50 characters 21 | { 22 | Mage::throwException("The Duo application key needs to be at least 40 characters long."); 23 | } 24 | return parent::save(); //call original save method so whatever happened 25 | } 26 | 27 | protected function _afterLoad() 28 | { 29 | $value = (string)$this->getValue(); 30 | if (empty($value)) { 31 | $key = $this->generateKey(40); 32 | $this->setValue($key); 33 | } 34 | } 35 | 36 | function generateKey($length=40) 37 | { 38 | $charset='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 39 | $count = strlen($charset); 40 | $str = ''; 41 | while ($length--) { 42 | $str .= $charset[mt_rand(0, $count-1)]; 43 | } 44 | return $str; 45 | } 46 | } -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/Model/Sysconfig/Provider.php: -------------------------------------------------------------------------------- 1 | $node) { 27 | $providers[]=(array('value' => $provider , 'label' => $node['title'])); 28 | } 29 | 30 | return $providers; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/Model/Validate.php: -------------------------------------------------------------------------------- 1 | _helper = Mage::helper('he_twofactorauth'); 19 | 20 | $this->_host = Mage::getStoreConfig('he2faconfig/duo/host'); 21 | $this->_ikey = Mage::helper('core')->decrypt(Mage::getStoreConfig('he2faconfig/duo/ikey')); 22 | $this->_skey = Mage::helper('core')->decrypt(Mage::getStoreConfig('he2faconfig/duo/skey')); 23 | $this->_akey = Mage::helper('core')->decrypt(Mage::getStoreConfig('he2faconfig/duo/akey')); 24 | 25 | if (!($this->_host && $this->_ikey && $this->_skey && $this->_akey)) { 26 | $this->_helper->disable2FA(); 27 | $msg = $this->_helper->__( 28 | 'Duo Twofactor Authentication is missing one or more settings. Please configure HE Two Factor Authentication.' 29 | ); 30 | Mage::getSingleton('adminhtml/session')->addError($msg); 31 | } 32 | 33 | $this->_shouldLog = Mage::helper('he_twofactorauth')->shouldLog(); 34 | } 35 | 36 | public function signRequest($user) 37 | { 38 | if ($this->_shouldLog) { 39 | Mage::log("in signRequest with $user", 0, "two_factor_auth.log"); 40 | } 41 | $sig_request = Duo::signRequest($this->_ikey, $this->_skey, $this->_akey, $user); 42 | 43 | return $sig_request; 44 | } 45 | 46 | public function verifyResponse($response) 47 | { 48 | $verified = Duo::verifyResponse($this->_ikey, $this->_skey, $this->_akey, $response); 49 | 50 | return ($verified != null); 51 | } 52 | 53 | public function getHost() 54 | { 55 | return $this->_host; 56 | } 57 | 58 | public function check() 59 | { 60 | return Mage::getModel('he_twofactorauth/validate_duo_request')->check(); 61 | } 62 | 63 | public function isValid() 64 | { 65 | 66 | $status = HE_TwoFactorAuth_Model_Validate::TFA_CHECK_FAIL; 67 | 68 | //TODO - Use provider based checks instead of hardcoding for Duo 69 | if (!Mage::getModel('he_twofactorauth/validate_duo_request')->ping()) { 70 | $msg = $this->_helper->__('Can not connect to specified Duo API server - TFA settings not validated'); 71 | } elseif (!$this->check()) { 72 | $msg = $this->_helper->__( 73 | 'Credentials for Duo API server not accepted, please check - TFA settings not validated' 74 | ); 75 | } else { 76 | $status = HE_TwoFactorAuth_Model_Validate::TFA_CHECK_SUCCESS; 77 | $msg = $this->_helper->__('Credentials for Duo API server accepted - TFA settings validated'); 78 | } 79 | 80 | //let the user know the status 81 | if ($status == HE_TwoFactorAuth_Model_Validate::TFA_CHECK_SUCCESS) { 82 | //Mage::getSingleton('adminhtml/session')->addSuccess($msg); 83 | if ($this->_shouldLog) { 84 | Mage::log("isValid - $msg.", Zend_Log::ERR, "two_factor_auth.log"); 85 | } 86 | $newMode = $this->_helper->__('VALID'); 87 | } else { 88 | Mage::getSingleton('adminhtml/session')->addError($msg); 89 | if ($this->_shouldLog) { 90 | Mage::log("isValid - $msg.", Zend_Log::INFO, "two_factor_auth.log"); 91 | } 92 | $newMode = $this->_helper->__('NOT VALID'); 93 | } 94 | 95 | //if mode changed, update config 96 | if ($newMode <> Mage::getStoreConfig('he2faconfig/duo/validated')) { 97 | Mage::getModel('core/config')->saveConfig('he2faconfig/duo/validated', $newMode); 98 | Mage::app()->getStore()->resetConfig(); 99 | } 100 | 101 | return $status; 102 | } 103 | } -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/Model/Validate/Duo/Request.php: -------------------------------------------------------------------------------- 1 | _params = array(); 47 | $this->_path = ""; 48 | $this->_method = "GET"; 49 | $this->_date = date("r"); 50 | 51 | $this->_host = Mage::getStoreConfig('he2faconfig/duo/host'); 52 | $this->_ikey = Mage::helper('core')->decrypt(Mage::getStoreConfig('he2faconfig/duo/ikey')); 53 | $this->_skey = Mage::helper('core')->decrypt(Mage::getStoreConfig('he2faconfig/duo/skey')); 54 | $this->_akey = Mage::helper('core')->decrypt(Mage::getStoreConfig('he2faconfig/duo/akey')); 55 | 56 | $this->_logoFile = Mage::getBaseDir('media') . "/duo_logo.png"; 57 | 58 | } 59 | 60 | /*** 61 | * create the request cannon string to be used in the password hash. Data must 62 | * match the request parameters 63 | * 64 | * @return string 65 | */ 66 | protected function _makeCannon() 67 | { 68 | $cannon = array( 69 | "date" => $this->_date, 70 | "method" => $this->_method, 71 | "host" => $this->_host, 72 | "path" => $this->_path, 73 | ); 74 | 75 | if (count($this->_params)) { 76 | $cannon["params"] = ksort($this->_params); 77 | } else { 78 | $cannon["params"] = ""; 79 | } 80 | 81 | return implode("\n", $cannon); 82 | } 83 | 84 | /*** 85 | * TBD 86 | */ 87 | protected function _getPostfields() 88 | { 89 | 90 | } 91 | 92 | /*** 93 | * Create a hash string to be used as the password for the request 94 | * Must be sha1 encrypted 95 | * 96 | * @return string 97 | */ 98 | protected function _makeHash() 99 | { 100 | return hash_hmac('sha1', $this->_makeCannon(), $this->_skey); 101 | } 102 | 103 | /*** 104 | * Add the date to the request headers, required for validation of authentication request 105 | * 106 | * @return array 107 | */ 108 | protected function _getHeaders() 109 | { 110 | return array("Date: $this->_date"); 111 | } 112 | 113 | /*** 114 | * Format parameters for GET calls and return full URL with query string 115 | * 116 | * @param string $url 117 | * 118 | * @return string 119 | */ 120 | protected function _addUrlParams($url) 121 | { 122 | if (count($this->_params) > 0) { 123 | return $url . "?" . http_build_query(ksort($this->_params)); 124 | } else { 125 | return $url; 126 | } 127 | } 128 | 129 | /*** 130 | * Call the DUO rest service and return the results 131 | * 132 | * @param bool $raw 133 | * @param bool $debug 134 | * 135 | * @return array|bool|mixed 136 | */ 137 | 138 | protected function _doRequest($raw = false, $debug = false) 139 | { 140 | if ($this->_path == "") { 141 | return false; 142 | } 143 | 144 | $url = "https://" . $this->_host . $this->_path; 145 | 146 | $headers = $this->_getHeaders(); 147 | 148 | $curl = curl_init(); 149 | if ($this->_method == "POST") { 150 | curl_setopt($curl, CURLOPT_POST, 1); //defaults to get 151 | curl_setopt($curl, CURLOPT_POSTFIELDS, $this->_getPostfields()); 152 | curl_setopt($curl, CURLOPT_URL, $url); 153 | } else { 154 | $url = $this->_addUrlParams($url); 155 | curl_setopt($curl, CURLOPT_URL, $url); 156 | } 157 | 158 | curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); 159 | curl_setopt($curl, CURLOPT_USERPWD, $this->_ikey . ":" . $this->_makeHash()); 160 | curl_setopt($curl, CURLOPT_FRESH_CONNECT, true); 161 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 162 | 163 | curl_setopt($curl, CURLOPT_VERBOSE, $debug); 164 | curl_setopt($curl, CURLOPT_BINARYTRANSFER, $raw); 165 | 166 | if (!$result = curl_exec($curl)) { 167 | $error = curl_error($curl); 168 | } else { 169 | $error = false; 170 | } 171 | $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); 172 | 173 | curl_close($curl); 174 | 175 | if ($error || $responseCode <> '200') { 176 | //todo - put in error logging 177 | return false; 178 | } else { 179 | if ($raw) { 180 | return $result; 181 | } else { 182 | return json_decode($result, true); 183 | } 184 | } 185 | } 186 | 187 | /*** 188 | * Check to see if the DUO service is reachable 189 | * 190 | * @return bool 191 | */ 192 | 193 | public function ping() 194 | { 195 | $this->_path = "/auth/v2/ping"; 196 | $result = $this->_doRequest(); 197 | 198 | if (!$result || $result['stat'] <> "OK") { 199 | return false; 200 | } else { 201 | return true; 202 | } 203 | } 204 | 205 | /*** 206 | * Check to see if the integration settings are valid 207 | * 208 | * @return bool 209 | */ 210 | 211 | public function check() 212 | { 213 | $this->_path = "/auth/v2/check"; 214 | $result = $this->_doRequest(); 215 | 216 | if (!$result || $result['stat'] <> "OK") { 217 | return false; 218 | } else { 219 | return true; 220 | } 221 | } 222 | 223 | /*** 224 | * Get the logo, if one is registered with DUO, for the integration 225 | * 226 | * @return bool 227 | */ 228 | public function logo() 229 | { 230 | $this->_path = "/auth/v2/logo"; 231 | $result = $this->_doRequest(true); 232 | 233 | if (!$result) { 234 | return false; 235 | } else { 236 | if (file_put_contents($this->_logoFile, $result)) { 237 | return $this->_logoFile; 238 | } else { 239 | return false; 240 | } 241 | } 242 | } 243 | 244 | // TODO - the remainder of the DUO protocol will be filled out later if needed 245 | 246 | public function enroll() 247 | { 248 | } 249 | 250 | public function enroll_status() 251 | { 252 | } 253 | 254 | public function preauth() 255 | { 256 | } 257 | 258 | public function auth() 259 | { 260 | } 261 | 262 | public function auth_status() 263 | { 264 | } 265 | } -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/Model/Validate/Google.php: -------------------------------------------------------------------------------- 1 | _shouldLog = Mage::helper('he_twofactorauth')->shouldLog(); 24 | } 25 | 26 | /* 27 | * HOTP - counter based 28 | * TOTP - time based 29 | */ 30 | public function getToken($username, $tokentype = "TOTP") 31 | { 32 | $token = $this->setUser($username, $tokentype); 33 | if ($this->_shouldLog) { 34 | Mage::log("token = " . var_export($token, true)); 35 | } 36 | 37 | $user = Mage::getModel('admin/user')->loadByUsername($username); 38 | $user->setTwofactorauthToken($token); 39 | //$user->save(); //password gets messed up after saving?! 40 | } 41 | 42 | 43 | public function isValid() 44 | { 45 | return true; 46 | } 47 | 48 | /* 49 | * generates and returns a new shared secret 50 | */ 51 | public function generateSecret() 52 | { 53 | $ga = new PHPGangsta_GoogleAuthenticator(); 54 | $secret = $ga->createSecret(); 55 | 56 | return $secret; 57 | } 58 | 59 | 60 | /* 61 | * generates and returns QR code URL from google 62 | */ 63 | public function generateQRCodeUrl($secret, $username) 64 | { 65 | if ((empty($secret)) || (empty($username))) { 66 | return; 67 | } 68 | 69 | $ga = new PHPGangsta_GoogleAuthenticator(); 70 | $url = $ga->getQRCodeGoogleUrl($username, $secret); 71 | 72 | return $url; 73 | } 74 | 75 | 76 | /* 77 | * verifies the code using TOTP 78 | */ 79 | 80 | public function validateCode($code) 81 | { 82 | if (empty($code)) { 83 | return; 84 | } 85 | Mage::log("Google - validateCode: " . $code, 0, "two_factor_auth.log"); 86 | 87 | // get user's shared secret 88 | $user = Mage::getSingleton('admin/session')->getUser(); 89 | $admin_user = Mage::getModel('admin/user')->load($user->getId()); 90 | 91 | $ga = new PHPGangsta_GoogleAuthenticator(); 92 | $secret = Mage::helper('core')->decrypt($admin_user->getTwofactorGoogleSecret()); 93 | 94 | return $ga->verifyCode($secret, $code, 1); 95 | } 96 | 97 | 98 | /* 99 | * abstract function in GoogleAuthenticator, needs to be defined here TODO 100 | */ 101 | function getDataBad($username, $index = null) // this was causing problems, not sure why... 102 | { 103 | $user = Mage::getModel('admin/user')->loadByUsername($username); 104 | 105 | return $user->getTwofactorauthToken() == null ? false : $user->getTwofactorauthToken(); 106 | } 107 | 108 | /* 109 | * abstract function in GoogleAuthenticator, needs to be defined here 110 | */ 111 | function putData($username, $data) 112 | { 113 | $user = Mage::getModel('admin/user')->loadByUsername($username); 114 | $user->setTwofactorauthToken("test"); 115 | $user->save(); 116 | } 117 | 118 | /* 119 | * abstract function in GoogleAuthenticator, needs to be defined here 120 | */ 121 | function getUsers() 122 | { 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/controllers/Adminhtml/TwofactorController.php: -------------------------------------------------------------------------------- 1 | _shouldLog = Mage::helper('he_twofactorauth')->shouldLog(); 26 | $this->_shouldLogAccess = Mage::helper('he_twofactorauth')->shouldLogAccess(); 27 | parent::_construct(); 28 | } 29 | 30 | /** 31 | * Allow all admin users to access the 2fa forms 32 | */ 33 | protected function _isAllowed() 34 | { 35 | return true; 36 | } 37 | 38 | //need an action per provider so that we can load the correct 2fa form 39 | 40 | public function duoAction() 41 | { 42 | if ($this->_shouldLog) { 43 | Mage::log("duoAction start", 0, "two_factor_auth.log"); 44 | } 45 | $msg = Mage::helper('he_twofactorauth')->__('Please complete the DUO two factor authentication'); 46 | Mage::getSingleton('adminhtml/session')->addNotice($msg); 47 | 48 | $this->loadLayout(); 49 | $this->renderLayout(); 50 | } 51 | 52 | 53 | public function googleAction() 54 | { 55 | if ($this->_shouldLog) { 56 | Mage::log("googleAction start", 0, "two_factor_auth.log"); 57 | } 58 | $this->loadLayout(); 59 | $this->renderLayout(); 60 | } 61 | 62 | 63 | /*** 64 | * verify is a generic action, looks at the current config to get provider, then dispatches correct verify method 65 | * 66 | * @return $this 67 | */ 68 | public function verifyAction() 69 | { 70 | if ($this->_shouldLog) { 71 | Mage::log("verifyAction start", 0, "two_factor_auth.log"); 72 | } 73 | 74 | if ($this->_shouldLogAccess) { 75 | $ipAddress = Mage::helper('core/http')->getRemoteAddr(); 76 | $adminName = Mage::getSingleton('admin/session')->getUser()->getUsername(); 77 | 78 | Mage::log("TFA Verify attempt for admin account $adminName from IP $ipAddress", 0, "two_factor_auth.log"); 79 | } 80 | 81 | $provider = Mage::helper('he_twofactorauth')->getProvider(); 82 | 83 | $verifyProcess = '_verify' . ucfirst($provider); 84 | 85 | if (method_exists($this, $verifyProcess)) { 86 | $this->$verifyProcess(); 87 | } else { 88 | Mage::helper('he_twofactorauth')->disable2FA(); 89 | if ($this->_shouldLog) { 90 | Mage::log( 91 | "verifyAction - Unsupported provider $provider. Two factor Authentication is disabled", 0, 92 | "two_factor_auth.log" 93 | ); 94 | } 95 | } 96 | 97 | return $this; 98 | } 99 | 100 | private function _verifyDuo() 101 | { 102 | $duoSigResp = Mage::app()->getRequest()->getPost('sig_response', null); 103 | 104 | $validate = Mage::getModel('he_twofactorauth/validate_duo'); 105 | 106 | if ($validate->verifyResponse($duoSigResp) === false) { 107 | if ($this->_shouldLog) { 108 | Mage::log("verifyDuo - failed verify", 0, "two_factor_auth.log"); 109 | } 110 | 111 | if ($this->_shouldLogAccess) { 112 | $ipAddress = Mage::helper('core/http')->getRemoteAddr(); 113 | $adminName = Mage::getSingleton('admin/session')->getUser()->getUsername(); 114 | 115 | Mage::log( 116 | "verifyDuo - TFA Verify attempt FAILED for admin account $adminName from IP $ipAddress", 0, 117 | "two_factor_auth.log" 118 | ); 119 | } 120 | 121 | //TODO - make status message area on template 122 | $msg = Mage::helper('he_twofactorauth')->__( 123 | 'verifyDuo - Two Factor Authentication has failed. Please try again or contact an administrator.' 124 | ); 125 | Mage::getSingleton('adminhtml/session')->addError($msg); 126 | 127 | $this->_redirect('adminhtml/twofactor/duo'); 128 | 129 | return $this; 130 | } 131 | 132 | if ($this->_shouldLog) { 133 | Mage::log("verifyDuo - Duo Validated", 0, "two_factor_auth.log"); 134 | } 135 | if ($this->_shouldLogAccess) { 136 | $ipAddress = Mage::helper('core/http')->getRemoteAddr(); 137 | $adminName = Mage::getSingleton('admin/session')->getUser()->getUsername(); 138 | 139 | Mage::log( 140 | "verifyDuo - TFA Verify attempt SUCCEEDED for admin account $adminName from IP $ipAddress", 0, 141 | "two_factor_auth.log" 142 | ); 143 | } 144 | 145 | 146 | Mage::getSingleton('admin/session') 147 | ->set2faState(HE_TwoFactorAuth_Model_Validate::TFA_STATE_ACTIVE); 148 | $this->_redirect('*'); 149 | 150 | return $this; 151 | } 152 | 153 | private function _verifyGoogle() 154 | { 155 | if ($this->_shouldLog) { 156 | Mage::log("verifyAction - start Google validate", 0, "two_factor_auth.log"); 157 | } 158 | $params = $this->getRequest()->getParams(); 159 | 160 | $ipAddress = Mage::helper('core/http')->getRemoteAddr(); 161 | $adminName = Mage::getSingleton('admin/session')->getUser()->getUsername(); 162 | 163 | // save the user's shared secret 164 | if ((!empty($params['google_secret'])) && (strlen($params['google_secret']) == 16)) { 165 | $user = Mage::getSingleton('admin/session')->getUser(); 166 | $admin_user = Mage::getModel('admin/user')->load($user->getId()); 167 | $admin_user->setTwofactorGoogleSecret(Mage::helper('core')->encrypt($params['google_secret'])); 168 | $admin_user->save(); 169 | if (($this->_shouldLog) || ($this->_shouldLogAccess)) { 170 | Mage::log( 171 | "verifyGoogle - new google secret saved for admin account $adminName from IP $ipAddress", 0, 172 | "two_factor_auth.log" 173 | ); 174 | } 175 | 176 | // redirect back to login, now they'll need to enter the code. 177 | $msg = Mage::helper('he_twofactorauth')->__("Please enter your input code."); 178 | Mage::getSingleton('adminhtml/session')->addError($msg); 179 | $this->_redirect('adminhtml/twofactor/google'); 180 | 181 | return $this; 182 | } else { 183 | // check the key 184 | // Test to make sure the parameter exists and remove any spaces 185 | if (array_key_exists('input_code', $params)) { 186 | $gcode = str_replace(' ', '', $params['input_code']); 187 | } else { 188 | $gcode = ''; 189 | } 190 | 191 | // TODO add better error checking and flow! 192 | if ((strlen($gcode) == 6) && (is_numeric($gcode))) { 193 | if ($this->_shouldLog) { 194 | Mage::log("verifyGoogle - checking input code '" . $gcode . "'", 0, "two_factor_auth.log"); 195 | } 196 | $g2fa = Mage::getModel("he_twofactorauth/validate_google"); 197 | $goodCode = $g2fa->validateCode($gcode); 198 | if ($goodCode) { 199 | if ($this->_shouldLogAccess) { 200 | 201 | Mage::log( 202 | "verifyGoogle - TFA Verify attempt SUCCESSFUL for admin account $adminName from IP $ipAddress", 203 | 0, "two_factor_auth.log" 204 | ); 205 | } 206 | 207 | $msg = Mage::helper('he_twofactorauth')->__("Valid code entered"); 208 | Mage::getSingleton('adminhtml/session')->addSuccess($msg); 209 | Mage::getSingleton('admin/session')->set2faState(HE_TwoFactorAuth_Model_Validate::TFA_STATE_ACTIVE); 210 | $this->_redirect('*'); 211 | 212 | return $this; 213 | } else { 214 | if ($this->_shouldLogAccess) { 215 | Mage::log( 216 | "verifyGoogle - TFA Verify attempt FAILED for admin account $adminName from IP $ipAddress", 217 | 0, "two_factor_auth.log" 218 | ); 219 | } 220 | $msg = Mage::helper('he_twofactorauth')->__("Invalid code entered"); 221 | Mage::getSingleton('adminhtml/session')->addError($msg); 222 | $this->_redirect('adminhtml/twofactor/google'); 223 | 224 | return $this; 225 | } 226 | } else { 227 | if ($this->_shouldLogAccess) { 228 | Mage::log( 229 | "verifyGoogle - TFA Verify attempt FAILED for admin account $adminName from IP $ipAddress", 0, 230 | "two_factor_auth.log" 231 | ); 232 | } 233 | $msg = Mage::helper('he_twofactorauth')->__("Invalid code entered"); 234 | Mage::getSingleton('adminhtml/session')->addError($msg); 235 | $this->_redirect('adminhtml/twofactor/google'); 236 | 237 | return $this; 238 | } 239 | } 240 | } 241 | 242 | /*** 243 | * verify is a generic action, looks at the current config to get provider, then dispatches correct verify method 244 | * 245 | * @return $this 246 | */ 247 | public function validateAction() 248 | { 249 | if ($this->_shouldLog) { 250 | Mage::log("validateAction start", 0, "two_factor_auth.log"); 251 | } 252 | $provider = Mage::helper('he_twofactorauth')->getProvider(); 253 | 254 | $validateProcess = '_validate' . ucfirst($provider); 255 | 256 | if (method_exists($this, $validateProcess)) { 257 | $this->$validateProcess(); 258 | } else { 259 | Mage::helper('he_twofactorauth')->disable2FA(); 260 | if ($this->_shouldLog) { 261 | Mage::log( 262 | "validateAction - Unsupported provider $provider. Two factor Authentication is disabled", 0, 263 | "two_factor_auth.log" 264 | ); 265 | } 266 | } 267 | 268 | return $this; 269 | } 270 | 271 | private function _validateDuo() 272 | { 273 | if ($this->_shouldLog) { 274 | Mage::log("validateAction starting", 0, "two_factor_auth.log"); 275 | } 276 | 277 | $validate = Mage::getModel('he_twofactorauth/validate_duo_request'); 278 | 279 | if ($validate->ping() == false) { 280 | if ($this->_shouldLog) { 281 | Mage::log( 282 | "validateDuo - ValidateAction ping fail - can not communicate with Duo auth server", 0, 283 | "two_factor_auth.log" 284 | ); 285 | } 286 | 287 | $msg = Mage::helper('he_twofactorauth')->__( 288 | 'Can not connect to authentication server. Two Factor Authentication has been disabled.' 289 | ); 290 | Mage::getSingleton('adminhtml/session')->addError($msg); 291 | 292 | } elseif ($validate->check() == false) { 293 | if ($this->_shouldLog) { 294 | Mage::log( 295 | "validateDuo - ValidateAction check fail - can not communicate with Duo auth server", 0, 296 | "two_factor_auth.log" 297 | ); 298 | } 299 | 300 | $msg = Mage::helper('he_twofactorauth')->__( 301 | 'Can not connect to authentication server. Two Factor Authentication has been disabled.' 302 | ); 303 | Mage::getSingleton('adminhtml/session')->addError($msg); 304 | } 305 | 306 | $this->_redirect('*'); 307 | 308 | return $this; 309 | } 310 | } -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/etc/adminhtml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Configure Two Factor Security 13 | 0 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1.0.5 6 | 7 | 8 | 9 | 10 | 11 | HE_TwoFactorAuth_Model 12 | he_twofactorauth_resource 13 | 14 | 15 | HE_TwoFactorAuth_Model_Resource 16 | 17 | 18 | 19 | 20 | HE_TwoFactorAuth_Block 21 | 22 | 23 | 24 | 25 | HE_TwoFactorAuth_Helper 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | singleton 34 | he_twofactorauth/observer 35 | check_twofactor_active 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | HE_TwoFactorAuth 45 | HE_TwoFactorAuth_Model_Resource_Mysql4_Setup 46 | 47 | 48 | core_setup 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | HE_TwoFactorAuth_Adminhtml 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | singleton 73 | he_twofactorauth/observer 74 | admin_user_authenticate_after 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | model 83 | he_twofactorauth/observer 84 | googleClearSecretCheck 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | model 93 | he_twofactorauth/observer 94 | googleSaveClear 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | he_twofactor/auth.xml 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | HE_TwoFactorAuth.csv 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | HE_TwoFactorAuth.csv 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | http://www.human-element.com 134 | https://www.nexcess.net 135 | https://github.com/nexcess/magento-sentry-two-factor-authentication/wiki 136 | https://github.com/nexcess/magento-sentry-two-factor-authentication/issues 137 | http://en.wikipedia.org/wiki/Multi-factor_authentication 138 | http://www.human-element.com/magento-support-page 139 | http://www.human-element.com/contact/#contact-form 140 | 141 | 142 | 143 | 144 | Disable multi-factor authentication 145 | 146 | 147 | Duo Security 148 | https://www.duosecurity.com/ 149 | 150 | 151 | Google Authenticator 152 | https://support.google.com/accounts/answer/1066447?hl=en 153 | 154 | 155 | 156 | NOT VALID 157 | 158 | 159 | 1 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/etc/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | sentry-section 7 | 99999 8 | 9 | 10 | 11 | 12 | 13 | sentry-item sentry-settings 14 | he2faconfig 15 | 16 | 20 17 | 1 18 | 1 19 | 0 20 | 21 | 22 | he_twofactorauth/adminhtml_system_config_fieldset_hint 23 | 0 24 | 1 25 | 1 26 | 0 27 | 28 | 29 | 30 | 31 | 100 32 | 1 33 | 1 34 | 35 | 36 | 37 | 38 | 39 | 40 | select 41 | he_twofactorauth/sysconfig_provider 42 | 10 43 | 1 44 | 1 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | textarea 53 | 15 54 | 1 55 | 1 56 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | select 66 | adminhtml/system_config_source_yesno 67 | 20 68 | 1 69 | 1 70 | 71 | 73 | 74 | 75 | 76 | 77 | 78 | select 79 | adminhtml/system_config_source_yesno 80 | 30 81 | 1 82 | 1 83 | 84 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | text 95 | 200 96 | 1 97 | 1 98 | 0 99 | 100 | About Duo Security
Duo Security is a leading provider of online 101 | Two Factor authentication. For more information, please visit their 102 | website.

103 |

SETUP For information on setting up a new account or adding an integration please see one 104 | of the following: 105 |

112 |

When setting up a new integration select either "Magento" or "Web SDK" as the type of integration.

113 |

NOTE! You must have a DUO account before enabling this module.

114 | ]]> 115 | 116 | 117 | 118 | 119 | obscure 120 | adminhtml/system_config_backend_encrypted 121 | 10 122 | 1 123 | 1 124 | 0 125 | 126 | 127 | 128 | 129 | 130 | 131 | obscure 132 | adminhtml/system_config_backend_encrypted 133 | 20 134 | 1 135 | 1 136 | 0 137 | 138 | 139 | 140 | 141 | 142 | 143 | text 144 | 30 145 | 1 146 | 1 147 | 0 148 | 149 | 150 | 151 | 152 | 153 | 154 | text 155 | he_twofactorauth/sysconfig_appkey 156 | 40 157 | 1 158 | 1 159 | 0 160 | 163 | 164 | 165 | 166 | 167 | label 168 | 1000 169 | 1 170 | 1 171 | 0 172 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | text 181 | 300 182 | 1 183 | 1 184 | 0 185 | 186 | About Google Authenticate
187 | Google Authenticate is a popular two-factor authentication solution. It is free to use, but requires use of a compatible app (freely avaialble from Google).

188 | 189 |

After enabling this option, the first time a user successfully logs in they will be required scan a QR code with their Google Authenticator app in order to setup their Google Authenticator secret code.

190 | 191 |

To login with Google Authenticator, users must use the Google Authenticator app:

192 | 196 | 197 | ]]> 198 |
199 | 200 |
201 |
202 |
203 |
204 | -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/readme.txt: -------------------------------------------------------------------------------- 1 | # Sentry 2 | ## Magento Two Factor Authentication Module 3 | 4 | ### Authors 5 | - Greg Croasdill, Human Element, Inc http://www.human-element.com 6 | - Gregg Milligan, Human Element, Inc http://www.human-element.com 7 | - Aric Watson, Nexcess.net https://www.nexcess.net/ 8 | 9 | ### License 10 | - GPL -- https://www.gnu.org/copyleft/gpl.html 11 | 12 | ### Purpose 13 | Sentry Two-Factor Authentication will protect your Magento store and customer data by adding an extra check to authenticate 14 | your Admin users before allowing them access. Developed as a partnership between the Human Element Magento Development team 15 | and Nexcess Hosting, Sentry Two-Factor Authentication for Magento is easy to setup and admin users can quickly login. 16 | 17 | ### Supported Providers (more to come) 18 | The following __Two Factor Authentication__ providers are supported at this time. 19 | 20 | #### Duo Security 21 | For more information on Duo security's API, please see - 22 | - https://www.duosecurity.com 23 | 24 | #### Google Authenticator 25 | For more information on Google Authenticator, please see - 26 | - https://github.com/google/google-authenticator/wiki 27 | - https://support.google.com/accounts/answer/180744?hl=en&ref_topic=1099588 28 | 29 | ### Referanced work 30 | 31 | Some code based on previous work by Jonathan Day jonathan@aligent.com.au 32 | - https://github.com/magento-hackathon/Magento-Two-factor-Authentication 33 | 34 | Some code based on previous work by Michael Kliewe/PHPGangsta 35 | - https://github.com/PHPGangsta/GoogleAuthenticator 36 | - http://www.phpgangsta.de/ 37 | 38 | ---- 39 | ### Notes - 40 | 1. Installing this module will update the AdminUser table in the Magento database to add a twofactor_google_secret 41 | field for storing the local GA key. It is safe to remove this field once the module is removed. 42 | 43 | 2. If you get locked out of admin because of a settings issue, loss of your provider account or other software related issue, you can *temporarily disable* the second factor authentication - 44 | - Place a file named __tfaoff.flag__ in the root directory of your Magento installation. 45 | - Login to Magento's Admin area without the second factor. 46 | - Update settings or disable Sentry 47 | - Remove the tfaoff.flag file to re-enable two factor authentication. 48 | -------------------------------------------------------------------------------- /app/code/community/HE/TwoFactorAuth/sql/he_twofactorauth/mysql4-install-1.0.2.php: -------------------------------------------------------------------------------- 1 | startSetup(); 6 | $installer->getConnection() 7 | ->addColumn($installer->getTable('admin/user'), 8 | 'twofactor_google_secret', 9 | array( 10 | 'type' => Varien_Db_Ddl_Table::TYPE_TEXT, 11 | 'nullable' => true, 12 | 'length' => 255, 13 | 'default' => null, 14 | 'comment' => 'Google Secret' 15 | ) 16 | ); 17 | $installer->endSetup(); 18 | 19 | // TODO add an uninstall script for users who remove module - apparently no automatic way to do this -------------------------------------------------------------------------------- /app/design/adminhtml/default/default/layout/he_twofactor/auth.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | skin_csshe_twofactor/css/admin.css 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/design/adminhtml/default/default/template/he_twofactor/duo/auth.phtml: -------------------------------------------------------------------------------- 1 | getUser()->getUsername(); 4 | $sign_request = Mage::getModel("he_twofactorauth/validate_duo")->signRequest($user); 5 | $auth_host = Mage::getModel("he_twofactorauth/validate_duo")->getHost(); 6 | 7 | $ready = ($user && $sign_request && $auth_host); // all have to be set 8 | 9 | ?> 10 | 11 | 12 | 13 | 14 | <?php echo Mage::helper('adminhtml')->__('Duo Two Factor Authentication'); ?> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 52 |
53 |
54 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/design/adminhtml/default/default/template/he_twofactor/google/auth.phtml: -------------------------------------------------------------------------------- 1 | 27 | getUser(); 31 | $admin_user = Mage::getModel('admin/user')->load($user->getId()); 32 | $secret = $admin_user->getTwofactorGoogleSecret(); 33 | 34 | if (!empty($secret)) { 35 | // $msg = Mage::helper('he_twofactorauth')->__('Please complete the Google two factor authentication'); 36 | // Mage::getSingleton('adminhtml/session')->addNotice($msg); 37 | } 38 | else { 39 | $g2fa = Mage::getModel("he_twofactorauth/validate_google"); 40 | $secret = $g2fa->generateSecret(); 41 | $qr_url = $g2fa->generateQRCodeUrl($secret, $user->getUsername()); 42 | $msg = Mage::helper('he_twofactorauth')->__('You must set up your Google Authenticator app using the QR code below.'); 43 | Mage::getSingleton('adminhtml/session')->addNotice($msg); 44 | // TODO figure out message weirdness 45 | } 46 | 47 | ?> 48 | 49 | 50 | 51 | 52 | <?php echo Mage::helper('adminhtml')->__('Google Two Factor Authentication'); ?> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 |
69 | 111 | 112 |
113 |
114 | 117 |
118 |
119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /app/design/adminhtml/default/default/template/he_twofactor/system/config/fieldset/hint.phtml: -------------------------------------------------------------------------------- 1 | getTwoFactorAuthVersion(); 4 | //config init 5 | $_configLinks='default/he2falinks/'; $_config=Mage::getConfig(); 6 | //links from translate CSV file 7 | $_heLink=$_config->getNode($_configLinks.'human-element-link'); 8 | $_nexcessLink=$_config->getNode($_configLinks.'nexcess-link'); 9 | $_docsLink=$_config->getNode($_configLinks.'docs-link'); 10 | $_submitBugLink=$_config->getNode($_configLinks.'submit-bug-link'); 11 | //$_multiAuthLink=$_config->getNode($_configLinks.'multi-auth-link'); 12 | $_mageSupportLink=$_config->getNode($_configLinks.'mage-support-link'); 13 | $_contactLink=$_config->getNode($_configLinks.'contact-link'); 14 | 15 | //remove http... from start of link so it can be used as a label in the paragraph 16 | function simplifyLinkLabel($link){ 17 | $link=str_replace('https://www.', '', $link); 18 | $link=str_replace('http://www.', '', $link); 19 | return $link; 20 | } 21 | ?> 22 | 23 |
24 |
25 | __('Sentry Two-Factor Authentication for Magento v%s', $_moduleVersion); ?> 26 |
27 |
28 | __('Developed as a Partnership between Human Element and Nexcess Hosting',$_heLink,$_nexcessLink); ?> 29 |
30 |
31 |
32 |
33 | __('Sentry Two-factor Authentication for Magento'); ?> 34 | 38 |
39 |

Multifactor authentication provides an additional check when logging into your Magento administration system. 40 | Once configured, an additional challenge screen is added to the admin login process. This screen will interact 41 | with an additional method of contact, usually a cellphone or email, that will prove that the user attempting login is the real user.

42 |

NOTE - when using multifactor authentication it is highly advised that each admin user have their own account.

43 |

To find out more about Human Element and Magento Support Services, please visit our website at 44 | 45 | or fill in our contact form. 46 | To find out more about Nexcess Hosting, visit their website at 47 | .

48 |
49 | -------------------------------------------------------------------------------- /app/etc/modules/HE_TwoFactorAuth.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | community 7 | 8 | 9 | -------------------------------------------------------------------------------- /build/build_package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | 4 | # Nexcess.net Turpentine Extension for Magento 5 | # Copyright (C) 2012 Nexcess.net L.L.C. 6 | 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | 21 | """Script to generate Magento Extension package files 22 | 23 | Run as: build_package.py 24 | 25 | You can override the xmllint and PHP binaries used for syntax checks with the 26 | TURPENTINE_BIN_PHP and TURPENTINE_BIN_XMLLINT environment variables. Useful for 27 | checking with a non-default version of PHP. 28 | """ 29 | 30 | __title__ = 'build_package.py' 31 | __version__ = '0.0.3' 32 | __author__ = 'Alex Headley ' 33 | __license__ = 'GPLv2' 34 | __copyright__ = 'Copyright (C) 2012 Nexcess.net L.L.C.' 35 | 36 | import os 37 | import xml.etree.ElementTree as ElementTree 38 | import logging 39 | import datetime 40 | import hashlib 41 | import re 42 | import tarfile 43 | import subprocess 44 | 45 | class Magento_Packager(object): 46 | BIN_PHP = os.environ.get('TURPENTINE_BIN_PHP', 'php') 47 | BIN_XMLLINT = os.environ.get('TURPENTINE_BIN_XMLLINT', 'xmllint') 48 | BIN_BASH = os.environ.get('TURPENTINE_BIN_BASH', 'bash') 49 | BIN_GCC = os.environ.get('TURPENTINE_BIN_GCC', 'gcc') 50 | 51 | TARGET_DIRS = { 52 | 'magelocal': 'app/code/local', 53 | 'magejs': 'js', 54 | 'magelib': 'lib', 55 | 'magecommunity': 'app/code/community', 56 | 'magecore': 'app/code/core', 57 | 'magedesign': 'app/design', 58 | 'mageetc': 'app/etc', 59 | } 60 | MAGE_PKG_XML_FILENAME = 'package.xml' 61 | 62 | def __init__(self, base_dir, debug=False): 63 | self._base_dir = base_dir 64 | self._logger = logging.getLogger(self.__class__.__name__) 65 | if debug: 66 | self._logger.setLevel(logging.DEBUG) 67 | else: 68 | self._logger.setLevel(logging.INFO) 69 | self._file_list = [] 70 | self._logger.debug('Packager init with base dir: %s', self._base_dir) 71 | self._logger.debug('Using PHP binary: %s', self.BIN_PHP) 72 | self._logger.debug('Using xmllint binary: %s', self.BIN_XMLLINT) 73 | 74 | def do_syntax_check(self): 75 | self._logger.info('Running syntax check on %d files', len(self._file_list)) 76 | result = True 77 | syntax_map = { 78 | '.php': self._php_syntax_check, 79 | '.phtml': self._php_syntax_check, 80 | '.xml': self._xml_syntax_check, 81 | '.sh': self._bash_syntax_check, 82 | '.bash': self._bash_syntax_check, 83 | '.c': self._gcc_syntax_check, 84 | } 85 | def unsupported_syntax_check(filename): 86 | self._logger.debug('Skipping syntax check for unsupported file: %s', 87 | filename) 88 | return True 89 | 90 | for filename in self._file_list: 91 | syntax_check = syntax_map.get(os.path.splitext(filename)[1].lower(), 92 | unsupported_syntax_check) 93 | if not syntax_check(filename): 94 | self._logger.warning('Syntax check failed for file: %s', filename) 95 | result = False 96 | return result 97 | 98 | def build_package_xml(self, connect_file): 99 | self._logger.info('Building package from connect file: %s', connect_file) 100 | connect_dom = ElementTree.parse(connect_file) 101 | ext_name = connect_dom.find('name').text 102 | self._logger.debug('Using "%s" as extension name', ext_name) 103 | config_dom = self._get_config_dom(ext_name, connect_dom.find('channel').text) 104 | module_dom = self._get_module_dom(ext_name) 105 | 106 | self._logger.info('Building extension %s version %s', ext_name, 107 | config_dom.find('modules/%s/version' % ext_name).text) 108 | 109 | if connect_dom.find('channel').text != \ 110 | module_dom.find('modules/%s/codePool' % ext_name).text: 111 | self._logger.warning('Connect file code pool (%s) does not match module code pool (%s)', 112 | connect_dom.find('channel').text, 113 | module_dom.find('modules/%s/codePool' % ext_name).text) 114 | 115 | pkg_dom = self._build_package_dom(ElementTree.Element('package'), 116 | connect_dom, config_dom, module_dom) 117 | 118 | self._logger.info('Finished building extension package XML') 119 | 120 | return pkg_dom 121 | 122 | def build_tarball(self, pkg_xml, tarball_name=None, keep_pkg_xml=False): 123 | manifest_filename = '%s/build/manifest-%s.xml' % \ 124 | (self._base_dir, pkg_xml.findtext('./version')) 125 | if tarball_name is None: 126 | tarball_name = '%s/build/%s-%s.tgz' % (self._base_dir, 127 | pkg_xml.findtext('./name'), pkg_xml.findtext('./version')) 128 | self._logger.info('Writing tarball to: %s', tarball_name) 129 | cdir = os.getcwd() 130 | os.chdir(self._base_dir) 131 | with open(manifest_filename, 'w') as xml_file: 132 | ElementTree.ElementTree(pkg_xml).write(xml_file, 'utf-8', True) 133 | self._logger.debug('Wrote package XML') 134 | with tarfile.open(tarball_name, 'w:gz') as tarball: 135 | for filename in self._file_list: 136 | alt_filename = filename.replace(self._base_dir + '/', '') 137 | self._logger.debug('Adding file to tarball: %s', alt_filename) 138 | tarball.add(filename, alt_filename) 139 | self._logger.debug('Adding file to tarball: %s', 140 | self.MAGE_PKG_XML_FILENAME) 141 | tarball.add(manifest_filename, self.MAGE_PKG_XML_FILENAME) 142 | self._logger.info('Finished writing tarball') 143 | if not keep_pkg_xml: 144 | os.unlink(manifest_filename) 145 | os.chdir(cdir) 146 | return tarball_name 147 | 148 | def _build_package_dom(self, pkg_dom, connect_dom, config_dom, module_dom): 149 | ext_name = connect_dom.find('name').text 150 | now = datetime.datetime.now() 151 | commit_hash = self._get_git_hash() 152 | self._logger.debug('Using commit hash: %s', commit_hash) 153 | extension = { 154 | 'name': ext_name, 155 | 'version': config_dom.find('modules/%s/version' % ext_name).text, 156 | 'stability': connect_dom.find('stability').text, 157 | 'license': connect_dom.find('license').text, 158 | 'channel': connect_dom.find('channel').text, 159 | 'extends': None, 160 | 'summary': connect_dom.find('summary').text, 161 | 'description': connect_dom.find('description').text, 162 | 'notes': connect_dom.find('notes').text, 163 | 'authors': None, 164 | 'date': now.date().isoformat(), 165 | 'time': now.time().strftime('%H:%M:%S'), 166 | 'contents': None, 167 | 'compatibile': None, 168 | 'dependencies': None, 169 | '__packager': '%s v%s' % (__title__, __version__), 170 | '__commit_hash': commit_hash, 171 | } 172 | for key, value in extension.iteritems(): 173 | tag = ElementTree.SubElement(pkg_dom, key) 174 | if value: 175 | tag.text = value 176 | self._logger.debug('Added package element <%s> = "%s"', key, value) 177 | 178 | pkg_dom.find('license').set('uri', connect_dom.find('license_uri').text) 179 | self._build_authors_tag(pkg_dom.find('authors'), connect_dom) 180 | self._build_contents_tag(pkg_dom.find('contents'), connect_dom) 181 | self._build_dependencies_tag(pkg_dom.find('dependencies'), connect_dom) 182 | return pkg_dom 183 | 184 | def _build_authors_tag(self, authors_tag, connect_dom): 185 | for i, _ in enumerate(connect_dom.findall('authors/name/name')): 186 | author_tag = ElementTree.SubElement(authors_tag, 'author') 187 | name_tag = ElementTree.SubElement(author_tag, 'name') 188 | name_tag.text = list(connect_dom.findall('authors/name/name'))[i].text 189 | user_tag = ElementTree.SubElement(author_tag, 'user') 190 | user_tag.text = list(connect_dom.findall('authors/user/user'))[i].text 191 | email_tag = ElementTree.SubElement(author_tag, 'email') 192 | email_tag.text = list(connect_dom.findall('authors/email/email'))[i].text 193 | self._logger.info('Added author %s (%s) <%s>', name_tag.text, 194 | user_tag.text, email_tag.text) 195 | return authors_tag 196 | 197 | def _build_contents_tag(self, contents_tag, connect_dom): 198 | used_target_paths = list(set(el.text for el in connect_dom.findall('contents/target/target'))) 199 | targets = list(self._iterate_targets(connect_dom)) 200 | for target_path_name in used_target_paths: 201 | target_tag = ElementTree.SubElement(contents_tag, 'target') 202 | target_tag.set('name', target_path_name) 203 | self._logger.debug('Adding objects for target: %s', target_path_name) 204 | for target in (t for t in targets if t['target'] == target_path_name): 205 | if target['type'] == 'dir': 206 | self._logger.info('Recursively adding dir: %s::%s', 207 | target['target'], target['path']) 208 | for obj_path, obj_name, obj_hash in self._walk_path(os.path.join( 209 | self._base_dir, self.TARGET_DIRS[target['target']], target['path']), 210 | target['include'], target['ignore']): 211 | parent_tag = self._make_parent_tags(target_tag, obj_path.replace( 212 | os.path.join(self._base_dir, self.TARGET_DIRS[target['target']]), '').strip('/')) 213 | if obj_hash is None: 214 | obj_tag = ElementTree.SubElement(parent_tag, 'dir') 215 | obj_tag.set('name', obj_name) 216 | self._logger.debug('Added directory: %s', obj_name) 217 | else: 218 | obj_tag = ElementTree.SubElement(parent_tag, 'file') 219 | obj_tag.set('name', obj_name) 220 | obj_tag.set('hash', obj_hash) 221 | self._file_list.append(os.path.join(obj_path, obj_name)) 222 | self._logger.debug('Added file: %s (%s)', obj_name, obj_hash) 223 | else: 224 | parent_tag = self._make_parent_tags(target_tag, os.path.dirname(target['path'])) 225 | obj_name = os.path.basename(target['path']) 226 | obj_hash = self._get_file_hash(os.path.join( 227 | self._base_dir, self.TARGET_DIRS[target['target']], 228 | target['path'])) 229 | obj_tag = ElementTree.SubElement(parent_tag, 'file') 230 | obj_tag.set('name', obj_name) 231 | obj_tag.set('hash', obj_hash) 232 | self._file_list.append(os.path.join(self._base_dir, 233 | self.TARGET_DIRS[target['target']], target['path'])) 234 | self._logger.info('Added single file: %s::%s (%s)', 235 | target['target'], target['path'], obj_hash) 236 | self._logger.debug('Finished adding targets') 237 | return contents_tag 238 | 239 | def _make_parent_tags(self, target_tag, tag_path): 240 | if tag_path: 241 | parts = tag_path.split('/') 242 | current_node = target_tag 243 | for part in parts: 244 | new_node = current_node.find('dir[@name=\'%s\']' % part) 245 | if new_node is None: 246 | new_node = ElementTree.SubElement(current_node, 'dir') 247 | new_node.set('name', part) 248 | current_node = new_node 249 | return current_node 250 | else: 251 | return target_tag 252 | 253 | def _iterate_targets(self, connect_dom): 254 | for i, el in enumerate(connect_dom.findall('contents/target/target')): 255 | yield { 256 | 'target': connect_dom.find('contents/target').getchildren()[i].text, 257 | 'path': connect_dom.find('contents/path').getchildren()[i].text, 258 | 'type': connect_dom.find('contents/type').getchildren()[i].text, 259 | 'include': connect_dom.find('contents/include').getchildren()[i].text, 260 | 'ignore': connect_dom.find('contents/ignore').getchildren()[i].text, 261 | } 262 | 263 | def _get_file_hash(self, filename): 264 | with open(filename, 'rb') as f: 265 | return hashlib.md5(f.read()).hexdigest() 266 | 267 | def _walk_path(self, path, include, ignore): 268 | for dirpath, dirnames, filenames in os.walk(path): 269 | for filename in filenames: 270 | if (include and re.match(include[1:-1], filename) and not \ 271 | (ignore and re.match(ignore[1:-1], filename))): 272 | yield dirpath, filename, self._get_file_hash(os.path.join(dirpath, filename)) 273 | for dirname in dirnames: 274 | if (include and re.match(include[1:-1], dirname) and not \ 275 | (ignore and re.match(ignore[1:-1], dirname))): 276 | yield dirpath, dirname, None 277 | 278 | def _build_dependencies_tag(self, dependencies_tag, connect_dom): 279 | req_tag = ElementTree.SubElement(dependencies_tag, 'required') 280 | php_tag = ElementTree.SubElement(req_tag, 'php') 281 | min_tag = ElementTree.SubElement(php_tag, 'min') 282 | min_tag.text = connect_dom.findtext('depends_php_min') 283 | max_tag = ElementTree.SubElement(php_tag, 'max') 284 | max_tag.text = connect_dom.findtext('depends_php_max') 285 | self._logger.debug('Finished adding dependencies') 286 | return dependencies_tag 287 | 288 | def _get_module_dom(self, ext_name): 289 | fn = os.path.join(self._base_dir, 'app/etc/modules', ext_name + '.xml') 290 | self._logger.debug('Using extension config file: %s', fn) 291 | return ElementTree.parse(fn) 292 | 293 | def _get_config_dom(self, ext_name, codepool): 294 | ns, ext = ext_name.split('_', 2) 295 | fn = os.path.join(self._base_dir, 'app/code', codepool, ns, ext, 'etc', 'config.xml') 296 | self._logger.debug('Using extension module file: %s', fn) 297 | return ElementTree.parse(fn) 298 | 299 | def _get_git_hash(self): 300 | """Get the current git commit hash 301 | 302 | Blatently stolen from: 303 | https://github.com/overviewer/Minecraft-Overviewer/blob/master/overviewer_core/util.py#L40 304 | """ 305 | try: 306 | with open(os.path.join(self._base_dir, '.git', 'HEAD'), 'r') as head_file: 307 | ref = head_file.read().strip() 308 | if ref[:5] == 'ref: ': 309 | with open(os.path.join(self._base_dir, '.git', ref[5:]), 'r') as commit_file: 310 | return commit_file.read().strip() 311 | else: 312 | return ref[5:] 313 | except Exception as err: 314 | self._logger.warning('Couldnt read the git commit hash: %s :: %s', 315 | err.__class__.__name__, err) 316 | return 'UNKNOWN' 317 | 318 | def _php_syntax_check(self, filename): 319 | self._logger.debug('Checking PHP syntax for file: %s', filename) 320 | return self._run_quiet(self.BIN_PHP, '-l', filename) 321 | 322 | def _xml_syntax_check(self, filename): 323 | self._logger.debug('Checking XML syntax for file: %s', filename) 324 | return self._run_quiet(self.BIN_XMLLINT, '--format', filename) 325 | 326 | def _bash_syntax_check(self, filename): 327 | self._logger.debug('Checking Bash syntax for file: %s', filename) 328 | return self._run_quiet(self.BIN_BASH, '-n', filename) 329 | 330 | def _gcc_syntax_check(self, filename): 331 | self._logger.debug('Checking C syntax for file: %s', filename) 332 | return self._run_quiet(self.BIN_GCC, '-fsyntax-only', filename) 333 | 334 | def _run_quiet(self, *pargs): 335 | with open('/dev/null', 'w') as dev_null: 336 | return not bool(subprocess.call(pargs, stdin=None, stdout=dev_null, 337 | stderr=dev_null)) 338 | 339 | def main(base_path, pkg_desc_file, skip_tarball=False, tarball=None, keep_package_xml=False, 340 | debug=False, skip_syntax_check=False, **kwargs): 341 | pkgr = Magento_Packager(base_path, debug=debug) 342 | pkg_xml = pkgr.build_package_xml(pkg_desc_file) 343 | if not skip_syntax_check: 344 | if not pkgr.do_syntax_check(): 345 | raise SystemExit('Syntax check failed!') 346 | if not skip_tarball: 347 | pkgr.build_tarball(pkg_xml, tarball_name=tarball, 348 | keep_pkg_xml=keep_package_xml) 349 | 350 | if __name__ == '__main__': 351 | import sys 352 | import optparse 353 | logging.basicConfig() 354 | parser = optparse.OptionParser() 355 | parser.add_option('-d', '--debug', action='store_true', 356 | default=os.environ.get('MPKG_DEV', False)) 357 | parser.add_option('-p', '--keep-package-xml', action='store_true', default=False) 358 | parser.add_option('-t', '--tarball', action='store', default=None) 359 | parser.add_option('-T', '--skip-tarball', action='store_true', default=False) 360 | parser.add_option('-S', '--skip-syntax-check', action='store_true', default=False) 361 | opts, args = parser.parse_args() 362 | base_path = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))) 363 | if len(args): 364 | main(base_path, args[0], **vars(opts)) 365 | else: 366 | print 'Missing package definition file argument (mage-package.xml)!' 367 | -------------------------------------------------------------------------------- /build/mage-package.xml: -------------------------------------------------------------------------------- 1 | <_> 2 | HE_TwoFactorAuth 3 | community 4 | 5 | 2 6 | 7 | Magento Two Factor Authentication Module 8 | Sentry Two-Factor Authentication will protect your Magento store and customer data by adding an extra check to authenticate your Admin users before allowing them access. Developed as a partnership between the Human Element Magento Development team and Nexcess Hosting, Sentry Two-Factor Authentication for Magento is easy to setup and admin users can quickly login. 9 | GPLv2 10 | http://opensource.org/licenses/GPL-2.0 11 | 0.0.3 12 | stable 13 | Supports Magento v1.6 and later 14 | 15 | 16 | Aric Watson 17 | Gregg Milligan 18 | Greg Croasdill 19 | 20 | 21 | aricwatson 22 | - 23 | - 24 | 25 | 26 | awatson@nexcess.net 27 | - 28 | - 29 | 30 | 31 | 5.2.13 32 | 7.1.0 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Core 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | magecommunity 66 | magedesign 67 | magedesign 68 | mageetc 69 | magejs 70 | magelib 71 | magelib 72 | 73 | 74 | HE/TwoFactorAuth 75 | adminhtml/default/default/layout/he_twofactor/auth.xml 76 | adminhtml/default/default/template/he_twofactor 77 | modules/HE_TwoFactorAuth.xml 78 | he_twofactor 79 | Duo 80 | GoogleAuthenticator/PHPGangsta 81 | 82 | 83 | dir 84 | file 85 | dir 86 | file 87 | dir 88 | dir 89 | dir 90 | 91 | 92 | ~.*~ 93 | 94 | ~.*~ 95 | 96 | ~.*~ 97 | ~.*~ 98 | ~.*~ 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /js/he_twofactor/Duo-Web-v1.bundled.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery JavaScript Library v1.4.2 3 | * http://jquery.com/ 4 | * 5 | * Copyright 2010, John Resig 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | * http://jquery.org/license 8 | * 9 | * Includes Sizzle.js 10 | * http://sizzlejs.com/ 11 | * Copyright 2010, The Dojo Foundation 12 | * Released under the MIT, BSD, and GPL Licenses. 13 | * 14 | * Date: Sat Feb 13 22:33:48 2010 -0500 15 | */ 16 | (function(aN,C){var a=function(aZ,a0){return new a.fn.init(aZ,a0)},n=aN.jQuery,S=aN.$,ac=aN.document,Y,Q=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,aX=/^.[^:#\[\.,]*$/,ay=/\S/,N=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,e=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,b=navigator.userAgent,u,L=false,ae=[],aH,au=Object.prototype.toString,aq=Object.prototype.hasOwnProperty,g=Array.prototype.push,G=Array.prototype.slice,s=Array.prototype.indexOf;a.fn=a.prototype={init:function(aZ,a2){var a1,a3,a0,a4;if(!aZ){return this}if(aZ.nodeType){this.context=this[0]=aZ;this.length=1;return this}if(aZ==="body"&&!a2){this.context=ac;this[0]=ac.body;this.selector="body";this.length=1;return this}if(typeof aZ==="string"){a1=Q.exec(aZ);if(a1&&(a1[1]||!a2)){if(a1[1]){a4=(a2?a2.ownerDocument||a2:ac);a0=e.exec(aZ);if(a0){if(a.isPlainObject(a2)){aZ=[ac.createElement(a0[1])];a.fn.attr.call(aZ,a2,true)}else{aZ=[a4.createElement(a0[1])]}}else{a0=K([a1[1]],[a4]);aZ=(a0.cacheable?a0.fragment.cloneNode(true):a0.fragment).childNodes}return a.merge(this,aZ)}else{a3=ac.getElementById(a1[2]);if(a3){if(a3.id!==a1[2]){return Y.find(aZ)}this.length=1;this[0]=a3}this.context=ac;this.selector=aZ;return this}}else{if(!a2&&/^\w+$/.test(aZ)){this.selector=aZ;this.context=ac;aZ=ac.getElementsByTagName(aZ);return a.merge(this,aZ)}else{if(!a2||a2.jquery){return(a2||Y).find(aZ)}else{return a(a2).find(aZ)}}}}else{if(a.isFunction(aZ)){return Y.ready(aZ)}}if(aZ.selector!==C){this.selector=aZ.selector;this.context=aZ.context}return a.makeArray(aZ,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return G.call(this,0)},get:function(aZ){return aZ==null?this.toArray():(aZ<0?this.slice(aZ)[0]:this[aZ])},pushStack:function(a0,a2,aZ){var a1=a();if(a.isArray(a0)){g.apply(a1,a0)}else{a.merge(a1,a0)}a1.prevObject=this;a1.context=this.context;if(a2==="find"){a1.selector=this.selector+(this.selector?" ":"")+aZ}else{if(a2){a1.selector=this.selector+"."+a2+"("+aZ+")"}}return a1},each:function(a0,aZ){return a.each(this,a0,aZ)},ready:function(aZ){a.bindReady();if(a.isReady){aZ.call(ac,a)}else{if(ae){ae.push(aZ)}}return this},eq:function(aZ){return aZ===-1?this.slice(aZ):this.slice(aZ,+aZ+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(G.apply(this,arguments),"slice",G.call(arguments).join(","))},map:function(aZ){return this.pushStack(a.map(this,function(a1,a0){return aZ.call(a1,a0,a1)}))},end:function(){return this.prevObject||a(null)},push:g,sort:[].sort,splice:[].splice};a.fn.init.prototype=a.fn;a.extend=a.fn.extend=function(){var a4=arguments[0]||{},a3=1,a2=arguments.length,a6=false,a7,a1,aZ,a0;if(typeof a4==="boolean"){a6=a4;a4=arguments[1]||{};a3=2}if(typeof a4!=="object"&&!a.isFunction(a4)){a4={}}if(a2===a3){a4=this;--a3}for(;a3
a";var a7=aZ.getElementsByTagName("*"),a6=aZ.getElementsByTagName("a")[0];if(!a7||!a7.length||!a6){return}a.support={leadingWhitespace:aZ.firstChild.nodeType===3,tbody:!aZ.getElementsByTagName("tbody").length,htmlSerialize:!!aZ.getElementsByTagName("link").length,style:/red/.test(a6.getAttribute("style")),hrefNormalized:a6.getAttribute("href")==="/a",opacity:/^0.55$/.test(a6.style.opacity),cssFloat:!!a6.style.cssFloat,checkOn:aZ.getElementsByTagName("input")[0].value==="on",optSelected:ac.createElement("select").appendChild(ac.createElement("option")).selected,parentNode:aZ.removeChild(aZ.appendChild(ac.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};a4.type="text/javascript";try{a4.appendChild(ac.createTextNode("window."+a0+"=1;"))}catch(a2){}a5.insertBefore(a4,a5.firstChild);if(aN[a0]){a.support.scriptEval=true;delete aN[a0]}try{delete a4.test}catch(a2){a.support.deleteExpando=false}a5.removeChild(a4);if(aZ.attachEvent&&aZ.fireEvent){aZ.attachEvent("onclick",function a8(){a.support.noCloneEvent=false;aZ.detachEvent("onclick",a8)});aZ.cloneNode(true).fireEvent("onclick")}aZ=ac.createElement("div");aZ.innerHTML="";var a1=ac.createDocumentFragment();a1.appendChild(aZ.firstChild);a.support.checkClone=a1.cloneNode(true).cloneNode(true).lastChild.checked;a(function(){var a9=ac.createElement("div");a9.style.width=a9.style.paddingLeft="1px";ac.body.appendChild(a9);a.boxModel=a.support.boxModel=a9.offsetWidth===2;ac.body.removeChild(a9).style.display="none";a9=null});var a3=function(a9){var bb=ac.createElement("div");a9="on"+a9;var ba=(a9 in bb);if(!ba){bb.setAttribute(a9,"return;");ba=typeof bb[a9]==="function"}bb=null;return ba};a.support.submitBubbles=a3("submit");a.support.changeBubbles=a3("change");a5=a4=aZ=a7=a6=null})();a.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var aJ="jQuery"+aQ(),aI=0,aU={};a.extend({cache:{},expando:aJ,noData:{embed:true,object:true,applet:true},data:function(a1,a0,a3){if(a1.nodeName&&a.noData[a1.nodeName.toLowerCase()]){return}a1=a1==aN?aU:a1;var a4=a1[aJ],aZ=a.cache,a2;if(!a4&&typeof a0==="string"&&a3===C){return null}if(!a4){a4=++aI}if(typeof a0==="object"){a1[aJ]=a4;a2=aZ[a4]=a.extend(true,{},a0)}else{if(!aZ[a4]){a1[aJ]=a4;aZ[a4]={}}}a2=aZ[a4];if(a3!==C){a2[a0]=a3}return typeof a0==="string"?a2[a0]:a2},removeData:function(a1,a0){if(a1.nodeName&&a.noData[a1.nodeName.toLowerCase()]){return}a1=a1==aN?aU:a1;var a3=a1[aJ],aZ=a.cache,a2=aZ[a3];if(a0){if(a2){delete a2[a0];if(a.isEmptyObject(a2)){a.removeData(a1)}}}else{if(a.support.deleteExpando){delete a1[a.expando]}else{if(a1.removeAttribute){a1.removeAttribute(a.expando)}}delete aZ[a3]}}});a.fn.extend({data:function(aZ,a1){if(typeof aZ==="undefined"&&this.length){return a.data(this[0])}else{if(typeof aZ==="object"){return this.each(function(){a.data(this,aZ)})}}var a2=aZ.split(".");a2[1]=a2[1]?"."+a2[1]:"";if(a1===C){var a0=this.triggerHandler("getData"+a2[1]+"!",[a2[0]]);if(a0===C&&this.length){a0=a.data(this[0],aZ)}return a0===C&&a2[1]?this.data(a2[0]):a0}else{return this.trigger("setData"+a2[1]+"!",[a2[0],a1]).each(function(){a.data(this,aZ,a1)})}},removeData:function(aZ){return this.each(function(){a.removeData(this,aZ)})}});a.extend({queue:function(a0,aZ,a2){if(!a0){return}aZ=(aZ||"fx")+"queue";var a1=a.data(a0,aZ);if(!a2){return a1||[]}if(!a1||a.isArray(a2)){a1=a.data(a0,aZ,a.makeArray(a2))}else{a1.push(a2)}return a1},dequeue:function(a2,a1){a1=a1||"fx";var aZ=a.queue(a2,a1),a0=aZ.shift();if(a0==="inprogress"){a0=aZ.shift()}if(a0){if(a1==="fx"){aZ.unshift("inprogress")}a0.call(a2,function(){a.dequeue(a2,a1)})}}});a.fn.extend({queue:function(aZ,a0){if(typeof aZ!=="string"){a0=aZ;aZ="fx"}if(a0===C){return a.queue(this[0],aZ)}return this.each(function(a2,a3){var a1=a.queue(this,aZ,a0);if(aZ==="fx"&&a1[0]!=="inprogress"){a.dequeue(this,aZ)}})},dequeue:function(aZ){return this.each(function(){a.dequeue(this,aZ)})},delay:function(a0,aZ){a0=a.fx?a.fx.speeds[a0]||a0:a0;aZ=aZ||"fx";return this.queue(aZ,function(){var a1=this;setTimeout(function(){a.dequeue(a1,aZ)},a0)})},clearQueue:function(aZ){return this.queue(aZ||"fx",[])}});var ap=/[\n\t]/g,T=/\s+/,aw=/\r/g,aR=/href|src|style/,d=/(button|input)/i,z=/(button|input|object|select|textarea)/i,j=/^(a|area)$/i,J=/radio|checkbox/;a.fn.extend({attr:function(aZ,a0){return ao(this,aZ,a0,true,a.attr)},removeAttr:function(aZ,a0){return this.each(function(){a.attr(this,aZ,"");if(this.nodeType===1){this.removeAttribute(aZ)}})},addClass:function(a6){if(a.isFunction(a6)){return this.each(function(a9){var a8=a(this);a8.addClass(a6.call(this,a9,a8.attr("class")))})}if(a6&&typeof a6==="string"){var aZ=(a6||"").split(T);for(var a2=0,a1=this.length;a2-1){return true}}return false},val:function(a6){if(a6===C){var a0=this[0];if(a0){if(a.nodeName(a0,"option")){return(a0.attributes.value||{}).specified?a0.value:a0.text}if(a.nodeName(a0,"select")){var a4=a0.selectedIndex,a7=[],a8=a0.options,a3=a0.type==="select-one";if(a4<0){return null}for(var a1=a3?a4:0,a5=a3?a4+1:a8.length;a1=0}else{if(a.nodeName(this,"select")){var a9=a.makeArray(bc);a("option",this).each(function(){this.selected=a.inArray(a(this).val(),a9)>=0});if(!a9.length){this.selectedIndex=-1}}else{this.value=bc}}})}});a.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a0,aZ,a5,a8){if(!a0||a0.nodeType===3||a0.nodeType===8){return C}if(a8&&aZ in a.attrFn){return a(a0)[aZ](a5)}var a1=a0.nodeType!==1||!a.isXMLDoc(a0),a4=a5!==C;aZ=a1&&a.props[aZ]||aZ;if(a0.nodeType===1){var a3=aR.test(aZ);if(aZ==="selected"&&!a.support.optSelected){var a6=a0.parentNode;if(a6){a6.selectedIndex;if(a6.parentNode){a6.parentNode.selectedIndex}}}if(aZ in a0&&a1&&!a3){if(a4){if(aZ==="type"&&d.test(a0.nodeName)&&a0.parentNode){a.error("type property can't be changed")}a0[aZ]=a5}if(a.nodeName(a0,"form")&&a0.getAttributeNode(aZ)){return a0.getAttributeNode(aZ).nodeValue}if(aZ==="tabIndex"){var a7=a0.getAttributeNode("tabIndex");return a7&&a7.specified?a7.value:z.test(a0.nodeName)||j.test(a0.nodeName)&&a0.href?0:C}return a0[aZ]}if(!a.support.style&&a1&&aZ==="style"){if(a4){a0.style.cssText=""+a5}return a0.style.cssText}if(a4){a0.setAttribute(aZ,""+a5)}var a2=!a.support.hrefNormalized&&a1&&a3?a0.getAttribute(aZ,2):a0.getAttribute(aZ);return a2===null?C:a2}return a.style(a0,aZ,a5)}});var aD=/\.(.*)$/,A=function(aZ){return aZ.replace(/[^\w\s\.\|`]/g,function(a0){return"\\"+a0})};a.event={add:function(a2,a6,bb,a4){if(a2.nodeType===3||a2.nodeType===8){return}if(a2.setInterval&&(a2!==aN&&!a2.frameElement)){a2=aN}var a0,ba;if(bb.handler){a0=bb;bb=a0.handler}if(!bb.guid){bb.guid=a.guid++}var a7=a.data(a2);if(!a7){return}var bc=a7.events=a7.events||{},a5=a7.handle,a5;if(!a5){a7.handle=a5=function(){return typeof a!=="undefined"&&!a.event.triggered?a.event.handle.apply(a5.elem,arguments):C}}a5.elem=a2;a6=a6.split(" ");var a9,a3=0,aZ;while((a9=a6[a3++])){ba=a0?a.extend({},a0):{handler:bb,data:a4};if(a9.indexOf(".")>-1){aZ=a9.split(".");a9=aZ.shift();ba.namespace=aZ.slice(0).sort().join(".")}else{aZ=[];ba.namespace=""}ba.type=a9;ba.guid=bb.guid;var a1=bc[a9],a8=a.event.special[a9]||{};if(!a1){a1=bc[a9]=[];if(!a8.setup||a8.setup.call(a2,a4,aZ,a5)===false){if(a2.addEventListener){a2.addEventListener(a9,a5,false)}else{if(a2.attachEvent){a2.attachEvent("on"+a9,a5)}}}}if(a8.add){a8.add.call(a2,ba);if(!ba.handler.guid){ba.handler.guid=bb.guid}}a1.push(ba);a.event.global[a9]=true}a2=null},global:{},remove:function(be,a9,a0,a5){if(be.nodeType===3||be.nodeType===8){return}var bh,a4,a6,bc=0,a2,a7,ba,a3,a8,aZ,bg,bd=a.data(be),a1=bd&&bd.events;if(!bd||!a1){return}if(a9&&a9.type){a0=a9.handler;a9=a9.type}if(!a9||typeof a9==="string"&&a9.charAt(0)==="."){a9=a9||"";for(a4 in a1){a.event.remove(be,a4+a9)}return}a9=a9.split(" ");while((a4=a9[bc++])){bg=a4;aZ=null;a2=a4.indexOf(".")<0;a7=[];if(!a2){a7=a4.split(".");a4=a7.shift();ba=new RegExp("(^|\\.)"+a.map(a7.slice(0).sort(),A).join("\\.(?:.*\\.)?")+"(\\.|$)")}a8=a1[a4];if(!a8){continue}if(!a0){for(var bb=0;bb=0){aZ.type=a8=a8.slice(0,-1);aZ.exclusive=true}if(!a1){aZ.stopPropagation();if(a.event.global[a8]){a.each(a.cache,function(){if(this.events&&this.events[a8]){a.event.trigger(aZ,a3,this.handle.elem)}})}}if(!a1||a1.nodeType===3||a1.nodeType===8){return C}aZ.result=C;aZ.target=a1;a3=a.makeArray(a3);a3.unshift(aZ)}aZ.currentTarget=a1;var a4=a.data(a1,"handle");if(a4){a4.apply(a1,a3)}var a9=a1.parentNode||a1.ownerDocument;try{if(!(a1&&a1.nodeName&&a.noData[a1.nodeName.toLowerCase()])){if(a1["on"+a8]&&a1["on"+a8].apply(a1,a3)===false){aZ.result=false}}}catch(a6){}if(!aZ.isPropagationStopped()&&a9){a.event.trigger(aZ,a3,a9,true)}else{if(!aZ.isDefaultPrevented()){var a5=aZ.target,a0,ba=a.nodeName(a5,"a")&&a8==="click",a7=a.event.special[a8]||{};if((!a7._default||a7._default.call(a1,aZ)===false)&&!ba&&!(a5&&a5.nodeName&&a.noData[a5.nodeName.toLowerCase()])){try{if(a5[a8]){a0=a5["on"+a8];if(a0){a5["on"+a8]=null}a.event.triggered=true;a5[a8]()}}catch(a6){}if(a0){a5["on"+a8]=a0}a.event.triggered=false}}}},handle:function(aZ){var a7,a1,a0,a2,a8;aZ=arguments[0]=a.event.fix(aZ||aN.event);aZ.currentTarget=this;a7=aZ.type.indexOf(".")<0&&!aZ.exclusive;if(!a7){a0=aZ.type.split(".");aZ.type=a0.shift();a2=new RegExp("(^|\\.)"+a0.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}var a8=a.data(this,"events"),a1=a8[aZ.type];if(a8&&a1){a1=a1.slice(0);for(var a4=0,a3=a1.length;a4-1?a.map(a0.options,function(a2){return a2.selected}).join("-"):""}else{if(a0.nodeName.toLowerCase()==="select"){a1=a0.selectedIndex}}}return a1},P=function P(a1){var aZ=a1.target,a0,a2;if(!ar.test(aZ.nodeName)||aZ.readOnly){return}a0=a.data(aZ,"_change_data");a2=i(aZ);if(a1.type!=="focusout"||aZ.type!=="radio"){a.data(aZ,"_change_data",a2)}if(a0===C||a2===a0){return}if(a0!=null||a2){a1.type="change";return a.event.trigger(a1,arguments[1],aZ)}};a.event.special.change={filters:{focusout:P,click:function(a1){var a0=a1.target,aZ=a0.type;if(aZ==="radio"||aZ==="checkbox"||a0.nodeName.toLowerCase()==="select"){return P.call(this,a1)}},keydown:function(a1){var a0=a1.target,aZ=a0.type;if((a1.keyCode===13&&a0.nodeName.toLowerCase()!=="textarea")||(a1.keyCode===32&&(aZ==="checkbox"||aZ==="radio"))||aZ==="select-multiple"){return P.call(this,a1)}},beforeactivate:function(a0){var aZ=a0.target;a.data(aZ,"_change_data",i(aZ))}},setup:function(a1,a0){if(this.type==="file"){return false}for(var aZ in aT){a.event.add(this,aZ+".specialChange",aT[aZ])}return ar.test(this.nodeName)},teardown:function(aZ){a.event.remove(this,".specialChange");return ar.test(this.nodeName)}};aT=a.event.special.change.filters}function aB(a0,a1,aZ){aZ[0].type=a0;return a.event.handle.apply(a1,aZ)}if(ac.addEventListener){a.each({focus:"focusin",blur:"focusout"},function(a1,aZ){a.event.special[aZ]={setup:function(){this.addEventListener(a1,a0,true)},teardown:function(){this.removeEventListener(a1,a0,true)}};function a0(a2){a2=a.event.fix(a2);a2.type=aZ;return a.event.handle.call(this,a2)}})}a.each(["bind","one"],function(a0,aZ){a.fn[aZ]=function(a6,a7,a5){if(typeof a6==="object"){for(var a3 in a6){this[aZ](a3,a7,a6[a3],a5)}return this}if(a.isFunction(a7)){a5=a7;a7=C}var a4=aZ==="one"?a.proxy(a5,function(a8){a(this).unbind(a8,a4);return a5.apply(this,arguments)}):a5;if(a6==="unload"&&aZ!=="one"){this.one(a6,a7,a5)}else{for(var a2=0,a1=this.length;a2+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,bb=0,bd=Object.prototype.toString,a5=false,a4=true;[0,0].sort(function(){a4=false;return 0});var a1=function(bm,bh,bp,bq){bp=bp||[];var bs=bh=bh||ac;if(bh.nodeType!==1&&bh.nodeType!==9){return[]}if(!bm||typeof bm!=="string"){return bp}var bn=[],bj,bu,bx,bi,bl=true,bk=a2(bh),br=bm;while((ba.exec(""),bj=ba.exec(br))!==null){br=bj[3];bn.push(bj[1]);if(bj[2]){bi=bj[3];break}}if(bn.length>1&&a6.exec(bm)){if(bn.length===2&&a7.relative[bn[0]]){bu=be(bn[0]+bn[1],bh)}else{bu=a7.relative[bn[0]]?[bh]:a1(bn.shift(),bh);while(bn.length){bm=bn.shift();if(a7.relative[bm]){bm+=bn.shift()}bu=be(bm,bu)}}}else{if(!bq&&bn.length>1&&bh.nodeType===9&&!bk&&a7.match.ID.test(bn[0])&&!a7.match.ID.test(bn[bn.length-1])){var bt=a1.find(bn.shift(),bh,bk);bh=bt.expr?a1.filter(bt.expr,bt.set)[0]:bt.set[0]}if(bh){var bt=bq?{expr:bn.pop(),set:a9(bq)}:a1.find(bn.pop(),bn.length===1&&(bn[0]==="~"||bn[0]==="+")&&bh.parentNode?bh.parentNode:bh,bk);bu=bt.expr?a1.filter(bt.expr,bt.set):bt.set;if(bn.length>0){bx=a9(bu)}else{bl=false}while(bn.length){var bw=bn.pop(),bv=bw;if(!a7.relative[bw]){bw=""}else{bv=bn.pop()}if(bv==null){bv=bh}a7.relative[bw](bx,bv,bk)}}else{bx=bn=[]}}if(!bx){bx=bu}if(!bx){a1.error(bw||bm)}if(bd.call(bx)==="[object Array]"){if(!bl){bp.push.apply(bp,bx)}else{if(bh&&bh.nodeType===1){for(var bo=0;bx[bo]!=null;bo++){if(bx[bo]&&(bx[bo]===true||bx[bo].nodeType===1&&a8(bh,bx[bo]))){bp.push(bu[bo])}}}else{for(var bo=0;bx[bo]!=null;bo++){if(bx[bo]&&bx[bo].nodeType===1){bp.push(bu[bo])}}}}}else{a9(bx,bp)}if(bi){a1(bi,bs,bp,bq);a1.uniqueSort(bp)}return bp};a1.uniqueSort=function(bi){if(bc){a5=a4;bi.sort(bc);if(a5){for(var bh=1;bh":function(bn,bi){var bl=typeof bi==="string";if(bl&&!/\W/.test(bi)){bi=bi.toLowerCase();for(var bj=0,bh=bn.length;bj=0)){if(!bj){bh.push(bm)}}else{if(bj){bi[bl]=false}}}}return false},ID:function(bh){return bh[1].replace(/\\/g,"")},TAG:function(bi,bh){return bi[1].toLowerCase()},CHILD:function(bh){if(bh[1]==="nth"){var bi=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(bh[2]==="even"&&"2n"||bh[2]==="odd"&&"2n+1"||!/\D/.test(bh[2])&&"0n+"+bh[2]||bh[2]);bh[2]=(bi[1]+(bi[2]||1))-0;bh[3]=bi[3]-0}bh[0]=bb++;return bh},ATTR:function(bl,bi,bj,bh,bm,bn){var bk=bl[1].replace(/\\/g,"");if(!bn&&a7.attrMap[bk]){bl[1]=a7.attrMap[bk]}if(bl[2]==="~="){bl[4]=" "+bl[4]+" "}return bl},PSEUDO:function(bl,bi,bj,bh,bm){if(bl[1]==="not"){if((ba.exec(bl[3])||"").length>1||/^\w/.test(bl[3])){bl[3]=a1(bl[3],null,null,bi)}else{var bk=a1.filter(bl[3],bi,bj,true^bm);if(!bj){bh.push.apply(bh,bk)}return false}}else{if(a7.match.POS.test(bl[0])||a7.match.CHILD.test(bl[0])){return true}}return bl},POS:function(bh){bh.unshift(true);return bh}},filters:{enabled:function(bh){return bh.disabled===false&&bh.type!=="hidden"},disabled:function(bh){return bh.disabled===true},checked:function(bh){return bh.checked===true},selected:function(bh){bh.parentNode.selectedIndex;return bh.selected===true},parent:function(bh){return !!bh.firstChild},empty:function(bh){return !bh.firstChild},has:function(bj,bi,bh){return !!a1(bh[3],bj).length},header:function(bh){return/h\d/i.test(bh.nodeName)},text:function(bh){return"text"===bh.type},radio:function(bh){return"radio"===bh.type},checkbox:function(bh){return"checkbox"===bh.type},file:function(bh){return"file"===bh.type},password:function(bh){return"password"===bh.type},submit:function(bh){return"submit"===bh.type},image:function(bh){return"image"===bh.type},reset:function(bh){return"reset"===bh.type},button:function(bh){return"button"===bh.type||bh.nodeName.toLowerCase()==="button"},input:function(bh){return/input|select|textarea|button/i.test(bh.nodeName)}},setFilters:{first:function(bi,bh){return bh===0},last:function(bj,bi,bh,bk){return bi===bk.length-1},even:function(bi,bh){return bh%2===0},odd:function(bi,bh){return bh%2===1},lt:function(bj,bi,bh){return bibh[3]-0},nth:function(bj,bi,bh){return bh[3]-0===bi},eq:function(bj,bi,bh){return bh[3]-0===bi}},filter:{PSEUDO:function(bn,bj,bk,bo){var bi=bj[1],bl=a7.filters[bi];if(bl){return bl(bn,bk,bj,bo)}else{if(bi==="contains"){return(bn.textContent||bn.innerText||a0([bn])||"").indexOf(bj[3])>=0}else{if(bi==="not"){var bm=bj[3];for(var bk=0,bh=bm.length;bk=0)}}},ID:function(bi,bh){return bi.nodeType===1&&bi.getAttribute("id")===bh},TAG:function(bi,bh){return(bh==="*"&&bi.nodeType===1)||bi.nodeName.toLowerCase()===bh},CLASS:function(bi,bh){return(" "+(bi.className||bi.getAttribute("class"))+" ").indexOf(bh)>-1},ATTR:function(bm,bk){var bj=bk[1],bh=a7.attrHandle[bj]?a7.attrHandle[bj](bm):bm[bj]!=null?bm[bj]:bm.getAttribute(bj),bn=bh+"",bl=bk[2],bi=bk[4];return bh==null?bl==="!=":bl==="="?bn===bi:bl==="*="?bn.indexOf(bi)>=0:bl==="~="?(" "+bn+" ").indexOf(bi)>=0:!bi?bn&&bh!==false:bl==="!="?bn!==bi:bl==="^="?bn.indexOf(bi)===0:bl==="$="?bn.substr(bn.length-bi.length)===bi:bl==="|="?bn===bi||bn.substr(0,bi.length+1)===bi+"-":false},POS:function(bl,bi,bj,bm){var bh=bi[2],bk=a7.setFilters[bh];if(bk){return bk(bl,bj,bi,bm)}}}};var a6=a7.match.POS;for(var a3 in a7.match){a7.match[a3]=new RegExp(a7.match[a3].source+/(?![^\[]*\])(?![^\(]*\))/.source);a7.leftMatch[a3]=new RegExp(/(^(?:.|\r|\n)*?)/.source+a7.match[a3].source.replace(/\\(\d+)/g,function(bi,bh){return"\\"+(bh-0+1)}))}var a9=function(bi,bh){bi=Array.prototype.slice.call(bi,0);if(bh){bh.push.apply(bh,bi);return bh}return bi};try{Array.prototype.slice.call(ac.documentElement.childNodes,0)[0].nodeType}catch(bg){a9=function(bl,bk){var bi=bk||[];if(bd.call(bl)==="[object Array]"){Array.prototype.push.apply(bi,bl)}else{if(typeof bl.length==="number"){for(var bj=0,bh=bl.length;bj";var bh=ac.documentElement;bh.insertBefore(bi,bh.firstChild);if(ac.getElementById(bj)){a7.find.ID=function(bl,bm,bn){if(typeof bm.getElementById!=="undefined"&&!bn){var bk=bm.getElementById(bl[1]);return bk?bk.id===bl[1]||typeof bk.getAttributeNode!=="undefined"&&bk.getAttributeNode("id").nodeValue===bl[1]?[bk]:C:[]}};a7.filter.ID=function(bm,bk){var bl=typeof bm.getAttributeNode!=="undefined"&&bm.getAttributeNode("id");return bm.nodeType===1&&bl&&bl.nodeValue===bk}}bh.removeChild(bi);bh=bi=null})();(function(){var bh=ac.createElement("div");bh.appendChild(ac.createComment(""));if(bh.getElementsByTagName("*").length>0){a7.find.TAG=function(bi,bm){var bl=bm.getElementsByTagName(bi[1]);if(bi[1]==="*"){var bk=[];for(var bj=0;bl[bj];bj++){if(bl[bj].nodeType===1){bk.push(bl[bj])}}bl=bk}return bl}}bh.innerHTML="";if(bh.firstChild&&typeof bh.firstChild.getAttribute!=="undefined"&&bh.firstChild.getAttribute("href")!=="#"){a7.attrHandle.href=function(bi){return bi.getAttribute("href",2)}}bh=null})();if(ac.querySelectorAll){(function(){var bh=a1,bj=ac.createElement("div");bj.innerHTML="

";if(bj.querySelectorAll&&bj.querySelectorAll(".TEST").length===0){return}a1=function(bn,bm,bk,bl){bm=bm||ac;if(!bl&&bm.nodeType===9&&!a2(bm)){try{return a9(bm.querySelectorAll(bn),bk)}catch(bo){}}return bh(bn,bm,bk,bl)};for(var bi in bh){a1[bi]=bh[bi]}bj=null})()}(function(){var bh=ac.createElement("div");bh.innerHTML="
";if(!bh.getElementsByClassName||bh.getElementsByClassName("e").length===0){return}bh.lastChild.className="e";if(bh.getElementsByClassName("e").length===1){return}a7.order.splice(1,0,"CLASS");a7.find.CLASS=function(bi,bj,bk){if(typeof bj.getElementsByClassName!=="undefined"&&!bk){return bj.getElementsByClassName(bi[1])}};bh=null})();function aZ(bi,bn,bm,bq,bo,bp){for(var bk=0,bj=bq.length;bk0){bl=bh;break}}}bh=bh[bi]}bq[bk]=bl}}}var a8=ac.compareDocumentPosition?function(bi,bh){return !!(bi.compareDocumentPosition(bh)&16)}:function(bi,bh){return bi!==bh&&(bi.contains?bi.contains(bh):true)};var a2=function(bh){var bi=(bh?bh.ownerDocument||bh:0).documentElement;return bi?bi.nodeName!=="HTML":false};var be=function(bh,bo){var bk=[],bl="",bm,bj=bo.nodeType?[bo]:bo;while((bm=a7.match.PSEUDO.exec(bh))){bl+=bm[0];bh=bh.replace(a7.match.PSEUDO,"")}bh=a7.relative[bh]?bh+"*":bh;for(var bn=0,bi=bj.length;bn=0)===aZ})};a.fn.extend({find:function(aZ){var a1=this.pushStack("","find",aZ),a4=0;for(var a2=0,a0=this.length;a20){for(var a5=a4;a50},closest:function(a8,aZ){if(a.isArray(a8)){var a5=[],a7=this[0],a4,a3={},a1;if(a7&&a8.length){for(var a2=0,a0=a8.length;a2-1:a(a7).is(a4)){a5.push({selector:a1,elem:a7});delete a3[a1]}}a7=a7.parentNode}}return a5}var a6=a.expr.match.POS.test(a8)?a(a8,aZ||this.context):null;return this.map(function(a9,ba){while(ba&&ba.ownerDocument&&ba!==aZ){if(a6?a6.index(ba)>-1:a(ba).is(a8)){return ba}ba=ba.parentNode}return null})},index:function(aZ){if(!aZ||typeof aZ==="string"){return a.inArray(this[0],aZ?a(aZ):this.parent().children())}return a.inArray(aZ.jquery?aZ[0]:aZ,this)},add:function(aZ,a0){var a2=typeof aZ==="string"?a(aZ,a0||this.context):a.makeArray(aZ),a1=a.merge(this.get(),a2);return this.pushStack(y(a2[0])||y(a1[0])?a1:a.unique(a1))},andSelf:function(){return this.add(this.prevObject)}});function y(aZ){return !aZ||!aZ.parentNode||aZ.parentNode.nodeType===11}a.each({parent:function(a0){var aZ=a0.parentNode;return aZ&&aZ.nodeType!==11?aZ:null},parents:function(aZ){return a.dir(aZ,"parentNode")},parentsUntil:function(a0,aZ,a1){return a.dir(a0,"parentNode",a1)},next:function(aZ){return a.nth(aZ,2,"nextSibling")},prev:function(aZ){return a.nth(aZ,2,"previousSibling")},nextAll:function(aZ){return a.dir(aZ,"nextSibling")},prevAll:function(aZ){return a.dir(aZ,"previousSibling")},nextUntil:function(a0,aZ,a1){return a.dir(a0,"nextSibling",a1)},prevUntil:function(a0,aZ,a1){return a.dir(a0,"previousSibling",a1)},siblings:function(aZ){return a.sibling(aZ.parentNode.firstChild,aZ)},children:function(aZ){return a.sibling(aZ.firstChild)},contents:function(aZ){return a.nodeName(aZ,"iframe")?aZ.contentDocument||aZ.contentWindow.document:a.makeArray(aZ.childNodes)}},function(aZ,a0){a.fn[aZ]=function(a3,a1){var a2=a.map(this,a0,a3);if(!O.test(aZ)){a1=a3}if(a1&&typeof a1==="string"){a2=a.filter(a1,a2)}a2=this.length>1?a.unique(a2):a2;if((this.length>1||aM.test(a1))&&Z.test(aZ)){a2=a2.reverse()}return this.pushStack(a2,aZ,G.call(arguments).join(","))}});a.extend({filter:function(a1,aZ,a0){if(a0){a1=":not("+a1+")"}return a.find.matches(a1,aZ)},dir:function(a1,a0,a3){var aZ=[],a2=a1[a0];while(a2&&a2.nodeType!==9&&(a3===C||a2.nodeType!==1||!a(a2).is(a3))){if(a2.nodeType===1){aZ.push(a2)}a2=a2[a0]}return aZ},nth:function(a3,aZ,a1,a2){aZ=aZ||1;var a0=0;for(;a3;a3=a3[a1]){if(a3.nodeType===1&&++a0===aZ){break}}return a3},sibling:function(a1,a0){var aZ=[];for(;a1;a1=a1.nextSibling){if(a1.nodeType===1&&a1!==a0){aZ.push(a1)}}return aZ}});var U=/ jQuery\d+="(?:\d+|null)"/g,aa=/^\s+/,I=/(<([\w:]+)[^>]*?)\/>/g,am=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,c=/<([\w:]+)/,t=/"},ad={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};ad.optgroup=ad.option;ad.tbody=ad.tfoot=ad.colgroup=ad.caption=ad.thead;ad.th=ad.td;if(!a.support.htmlSerialize){ad._default=[1,"div
","
"]}a.fn.extend({text:function(aZ){if(a.isFunction(aZ)){return this.each(function(a1){var a0=a(this);a0.text(aZ.call(this,a1,a0.text()))})}if(typeof aZ!=="object"&&aZ!==C){return this.empty().append((this[0]&&this[0].ownerDocument||ac).createTextNode(aZ))}return a.text(this)},wrapAll:function(aZ){if(a.isFunction(aZ)){return this.each(function(a1){a(this).wrapAll(aZ.call(this,a1))})}if(this[0]){var a0=a(aZ,this[0].ownerDocument).eq(0).clone(true);if(this[0].parentNode){a0.insertBefore(this[0])}a0.map(function(){var a1=this;while(a1.firstChild&&a1.firstChild.nodeType===1){a1=a1.firstChild}return a1}).append(this)}return this},wrapInner:function(aZ){if(a.isFunction(aZ)){return this.each(function(a0){a(this).wrapInner(aZ.call(this,a0))})}return this.each(function(){var a0=a(this),a1=a0.contents();if(a1.length){a1.wrapAll(aZ)}else{a0.append(aZ)}})},wrap:function(aZ){return this.each(function(){a(this).wrapAll(aZ)})},unwrap:function(){return this.parent().each(function(){if(!a.nodeName(this,"body")){a(this).replaceWith(this.childNodes)}}).end()},append:function(){return this.domManip(arguments,true,function(aZ){if(this.nodeType===1){this.appendChild(aZ)}})},prepend:function(){return this.domManip(arguments,true,function(aZ){if(this.nodeType===1){this.insertBefore(aZ,this.firstChild)}})},before:function(){if(this[0]&&this[0].parentNode){return this.domManip(arguments,false,function(a0){this.parentNode.insertBefore(a0,this)})}else{if(arguments.length){var aZ=a(arguments[0]);aZ.push.apply(aZ,this.toArray());return this.pushStack(aZ,"before",arguments)}}},after:function(){if(this[0]&&this[0].parentNode){return this.domManip(arguments,false,function(a0){this.parentNode.insertBefore(a0,this.nextSibling)})}else{if(arguments.length){var aZ=this.pushStack(this,"after",arguments);aZ.push.apply(aZ,a(arguments[0]).toArray());return aZ}}},remove:function(aZ,a2){for(var a0=0,a1;(a1=this[a0])!=null;a0++){if(!aZ||a.filter(aZ,[a1]).length){if(!a2&&a1.nodeType===1){a.cleanData(a1.getElementsByTagName("*"));a.cleanData([a1])}if(a1.parentNode){a1.parentNode.removeChild(a1)}}}return this},empty:function(){for(var aZ=0,a0;(a0=this[aZ])!=null;aZ++){if(a0.nodeType===1){a.cleanData(a0.getElementsByTagName("*"))}while(a0.firstChild){a0.removeChild(a0.firstChild)}}return this},clone:function(a0){var aZ=this.map(function(){if(!a.support.noCloneEvent&&!a.isXMLDoc(this)){var a2=this.outerHTML,a1=this.ownerDocument;if(!a2){var a3=a1.createElement("div");a3.appendChild(this.cloneNode(true));a2=a3.innerHTML}return a.clean([a2.replace(U,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(aa,"")],a1)[0]}else{return this.cloneNode(true)}});if(a0===true){q(this,aZ);q(this.find("*"),aZ.find("*"))}return aZ},html:function(a1){if(a1===C){return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(U,""):null}else{if(typeof a1==="string"&&!F.test(a1)&&(a.support.leadingWhitespace||!aa.test(a1))&&!ad[(c.exec(a1)||["",""])[1].toLowerCase()]){a1=a1.replace(I,p);try{for(var a0=0,aZ=this.length;a00||a2.cacheable||this.length>1?a4.cloneNode(true):a4)}}if(a0.length){a.each(a0,aW)}}return this;function a6(bb,bc){return a.nodeName(bb,"table")?(bb.getElementsByTagName("tbody")[0]||bb.appendChild(bb.ownerDocument.createElement("tbody"))):bb}}});function q(a1,aZ){var a0=0;aZ.each(function(){if(this.nodeName!==(a1[a0]&&a1[a0].nodeName)){return}var a6=a.data(a1[a0++]),a5=a.data(this,a6),a2=a6&&a6.events;if(a2){delete a5.handle;a5.events={};for(var a4 in a2){for(var a3 in a2[a4]){a.event.add(this,a4,a2[a4][a3],a2[a4][a3].data)}}}})}function K(a4,a2,a0){var a3,aZ,a1,a5=(a2&&a2[0]?a2[0].ownerDocument||a2[0]:ac);if(a4.length===1&&typeof a4[0]==="string"&&a4[0].length<512&&a5===ac&&!F.test(a4[0])&&(a.support.checkClone||!l.test(a4[0]))){aZ=true;a1=a.fragments[a4[0]];if(a1){if(a1!==1){a3=a1}}}if(!a3){a3=a5.createDocumentFragment();a.clean(a4,a5,a3,a0)}if(aZ){a.fragments[a4[0]]=a1?a3:1}return{fragment:a3,cacheable:aZ}}a.fragments={};a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(aZ,a0){a.fn[aZ]=function(a1){var a4=[],a7=a(a1),a6=this.length===1&&this[0].parentNode;if(a6&&a6.nodeType===11&&a6.childNodes.length===1&&a7.length===1){a7[a0](this[0]);return this}else{for(var a5=0,a2=a7.length;a50?this.clone(true):this).get();a.fn[a0].apply(a(a7[a5]),a3);a4=a4.concat(a3)}return this.pushStack(a4,aZ,a7.selector)}}});a.extend({clean:function(a1,a3,ba,a5){a3=a3||ac;if(typeof a3.createElement==="undefined"){a3=a3.ownerDocument||a3[0]&&a3[0].ownerDocument||ac}var bb=[];for(var a9=0,a4;(a4=a1[a9])!=null;a9++){if(typeof a4==="number"){a4+=""}if(!a4){continue}if(typeof a4==="string"&&!M.test(a4)){a4=a3.createTextNode(a4)}else{if(typeof a4==="string"){a4=a4.replace(I,p);var bc=(c.exec(a4)||["",""])[1].toLowerCase(),a2=ad[bc]||ad._default,a8=a2[0],a0=a3.createElement("div");a0.innerHTML=a2[1]+a4+a2[2];while(a8--){a0=a0.lastChild}if(!a.support.tbody){var aZ=t.test(a4),a7=bc==="table"&&!aZ?a0.firstChild&&a0.firstChild.childNodes:a2[1]===""&&!aZ?a0.childNodes:[];for(var a6=a7.length-1;a6>=0;--a6){if(a.nodeName(a7[a6],"tbody")&&!a7[a6].childNodes.length){a7[a6].parentNode.removeChild(a7[a6])}}}if(!a.support.leadingWhitespace&&aa.test(a4)){a0.insertBefore(a3.createTextNode(aa.exec(a4)[0]),a0.firstChild)}a4=a0.childNodes}}if(a4.nodeType){bb.push(a4)}else{bb=a.merge(bb,a4)}}if(ba){for(var a9=0;bb[a9];a9++){if(a5&&a.nodeName(bb[a9],"script")&&(!bb[a9].type||bb[a9].type.toLowerCase()==="text/javascript")){a5.push(bb[a9].parentNode?bb[a9].parentNode.removeChild(bb[a9]):bb[a9])}else{if(bb[a9].nodeType===1){bb.splice.apply(bb,[a9+1,0].concat(a.makeArray(bb[a9].getElementsByTagName("script"))))}ba.appendChild(bb[a9])}}}return bb},cleanData:function(a0){var a3,a1,aZ=a.cache,a6=a.event.special,a5=a.support.deleteExpando;for(var a4=0,a2;(a2=a0[a4])!=null;a4++){a1=a2[a.expando];if(a1){a3=aZ[a1];if(a3.events){for(var a7 in a3.events){if(a6[a7]){a.event.remove(a2,a7)}else{ah(a2,a7,a3.handle)}}}if(a5){delete a2[a.expando]}else{if(a2.removeAttribute){a2.removeAttribute(a.expando)}}delete aZ[a1]}}}});var at=/z-?index|font-?weight|opacity|zoom|line-?height/i,V=/alpha\([^)]*\)/,ab=/opacity=([^)]*)/,ai=/float/i,aA=/-([a-z])/ig,v=/([A-Z])/g,aP=/^-?\d+(?:px)?$/i,aV=/^-?\d/,aL={position:"absolute",visibility:"hidden",display:"block"},X=["Left","Right"],aF=["Top","Bottom"],al=ac.defaultView&&ac.defaultView.getComputedStyle,aO=a.support.cssFloat?"cssFloat":"styleFloat",k=function(aZ,a0){return a0.toUpperCase()};a.fn.css=function(aZ,a0){return ao(this,aZ,a0,true,function(a2,a1,a3){if(a3===C){return a.curCSS(a2,a1)}if(typeof a3==="number"&&!at.test(a1)){a3+="px"}a.style(a2,a1,a3)})};a.extend({style:function(a3,a0,a4){if(!a3||a3.nodeType===3||a3.nodeType===8){return C}if((a0==="width"||a0==="height")&&parseFloat(a4)<0){a4=C}var a2=a3.style||a3,a5=a4!==C;if(!a.support.opacity&&a0==="opacity"){if(a5){a2.zoom=1;var aZ=parseInt(a4,10)+""==="NaN"?"":"alpha(opacity="+a4*100+")";var a1=a2.filter||a.curCSS(a3,"filter")||"";a2.filter=V.test(a1)?a1.replace(V,aZ):aZ}return a2.filter&&a2.filter.indexOf("opacity=")>=0?(parseFloat(ab.exec(a2.filter)[1])/100)+"":""}if(ai.test(a0)){a0=aO}a0=a0.replace(aA,k);if(a5){a2[a0]=a4}return a2[a0]},css:function(a2,a0,a4,aZ){if(a0==="width"||a0==="height"){var a6,a1=aL,a5=a0==="width"?X:aF;function a3(){a6=a0==="width"?a2.offsetWidth:a2.offsetHeight;if(aZ==="border"){return}a.each(a5,function(){if(!aZ){a6-=parseFloat(a.curCSS(a2,"padding"+this,true))||0}if(aZ==="margin"){a6+=parseFloat(a.curCSS(a2,"margin"+this,true))||0}else{a6-=parseFloat(a.curCSS(a2,"border"+this+"Width",true))||0}})}if(a2.offsetWidth!==0){a3()}else{a.swap(a2,a1,a3)}return Math.max(0,Math.round(a6))}return a.curCSS(a2,a0,a4)},curCSS:function(a5,a0,a1){var a8,aZ=a5.style,a2;if(!a.support.opacity&&a0==="opacity"&&a5.currentStyle){a8=ab.test(a5.currentStyle.filter||"")?(parseFloat(RegExp.$1)/100)+"":"";return a8===""?"1":a8}if(ai.test(a0)){a0=aO}if(!a1&&aZ&&aZ[a0]){a8=aZ[a0]}else{if(al){if(ai.test(a0)){a0="float"}a0=a0.replace(v,"-$1").toLowerCase();var a7=a5.ownerDocument.defaultView;if(!a7){return null}var a9=a7.getComputedStyle(a5,null);if(a9){a8=a9.getPropertyValue(a0)}if(a0==="opacity"&&a8===""){a8="1"}}else{if(a5.currentStyle){var a4=a0.replace(aA,k);a8=a5.currentStyle[a0]||a5.currentStyle[a4];if(!aP.test(a8)&&aV.test(a8)){var a3=aZ.left,a6=a5.runtimeStyle.left;a5.runtimeStyle.left=a5.currentStyle.left;aZ.left=a4==="fontSize"?"1em":(a8||0);a8=aZ.pixelLeft+"px";aZ.left=a3;a5.runtimeStyle.left=a6}}}}return a8},swap:function(a2,a1,a3){var aZ={};for(var a0 in a1){aZ[a0]=a2.style[a0];a2.style[a0]=a1[a0]}a3.call(a2);for(var a0 in a1){a2.style[a0]=aZ[a0]}}});if(a.expr&&a.expr.filters){a.expr.filters.hidden=function(a2){var a0=a2.offsetWidth,aZ=a2.offsetHeight,a1=a2.nodeName.toLowerCase()==="tr";return a0===0&&aZ===0&&!a1?true:a0>0&&aZ>0&&!a1?false:a.curCSS(a2,"display")==="none"};a.expr.filters.visible=function(aZ){return !a.expr.filters.hidden(aZ)}}var ag=aQ(),aK=//gi,o=/select|textarea/i,aC=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,r=/=\?(&|$)/,E=/\?/,aY=/(\?|&)_=.*?(&|$)/,B=/^(\w+:)?\/\/([^\/?#]+)/,h=/%20/g,w=a.fn.load;a.fn.extend({load:function(a1,a4,a5){if(typeof a1!=="string"){return w.call(this,a1)}else{if(!this.length){return this}}var a3=a1.indexOf(" ");if(a3>=0){var aZ=a1.slice(a3,a1.length);a1=a1.slice(0,a3)}var a2="GET";if(a4){if(a.isFunction(a4)){a5=a4;a4=null}else{if(typeof a4==="object"){a4=a.param(a4,a.ajaxSettings.traditional);a2="POST"}}}var a0=this;a.ajax({url:a1,type:a2,dataType:"html",data:a4,complete:function(a7,a6){if(a6==="success"||a6==="notmodified"){a0.html(aZ?a("
").append(a7.responseText.replace(aK,"")).find(aZ):a7.responseText)}if(a5){a0.each(a5,[a7.responseText,a6,a7])}}});return this},serialize:function(){return a.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?a.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||o.test(this.nodeName)||aC.test(this.type))}).map(function(aZ,a0){var a1=a(this).val();return a1==null?null:a.isArray(a1)?a.map(a1,function(a3,a2){return{name:a0.name,value:a3}}):{name:a0.name,value:a1}}).get()}});a.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(aZ,a0){a.fn[a0]=function(a1){return this.bind(a0,a1)}});a.extend({get:function(aZ,a1,a2,a0){if(a.isFunction(a1)){a0=a0||a2;a2=a1;a1=null}return a.ajax({type:"GET",url:aZ,data:a1,success:a2,dataType:a0})},getScript:function(aZ,a0){return a.get(aZ,null,a0,"script")},getJSON:function(aZ,a0,a1){return a.get(aZ,a0,a1,"json")},post:function(aZ,a1,a2,a0){if(a.isFunction(a1)){a0=a0||a2;a2=a1;a1={}}return a.ajax({type:"POST",url:aZ,data:a1,success:a2,dataType:a0})},ajaxSetup:function(aZ){a.extend(a.ajaxSettings,aZ)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:aN.XMLHttpRequest&&(aN.location.protocol!=="file:"||!aN.ActiveXObject)?function(){return new aN.XMLHttpRequest()}:function(){try{return new aN.ActiveXObject("Microsoft.XMLHTTP")}catch(aZ){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(be){var a9=a.extend(true,{},a.ajaxSettings,be);var bj,bd,bi,bk=be&&be.context||a9,a1=a9.type.toUpperCase();if(a9.data&&a9.processData&&typeof a9.data!=="string"){a9.data=a.param(a9.data,a9.traditional)}if(a9.dataType==="jsonp"){if(a1==="GET"){if(!r.test(a9.url)){a9.url+=(E.test(a9.url)?"&":"?")+(a9.jsonp||"callback")+"=?"}}else{if(!a9.data||!r.test(a9.data)){a9.data=(a9.data?a9.data+"&":"")+(a9.jsonp||"callback")+"=?"}}a9.dataType="json"}if(a9.dataType==="json"&&(a9.data&&r.test(a9.data)||r.test(a9.url))){bj=a9.jsonpCallback||("jsonp"+ag++);if(a9.data){a9.data=(a9.data+"").replace(r,"="+bj+"$1")}a9.url=a9.url.replace(r,"="+bj+"$1");a9.dataType="script";aN[bj]=aN[bj]||function(bl){bi=bl;a4();a7();aN[bj]=C;try{delete aN[bj]}catch(bm){}if(a2){a2.removeChild(bg)}}}if(a9.dataType==="script"&&a9.cache===null){a9.cache=false}if(a9.cache===false&&a1==="GET"){var aZ=aQ();var bh=a9.url.replace(aY,"$1_="+aZ+"$2");a9.url=bh+((bh===a9.url)?(E.test(a9.url)?"&":"?")+"_="+aZ:"")}if(a9.data&&a1==="GET"){a9.url+=(E.test(a9.url)?"&":"?")+a9.data}if(a9.global&&!a.active++){a.event.trigger("ajaxStart")}var bc=B.exec(a9.url),a3=bc&&(bc[1]&&bc[1]!==location.protocol||bc[2]!==location.host);if(a9.dataType==="script"&&a1==="GET"&&a3){var a2=ac.getElementsByTagName("head")[0]||ac.documentElement;var bg=ac.createElement("script");bg.src=a9.url;if(a9.scriptCharset){bg.charset=a9.scriptCharset}if(!bj){var bb=false;bg.onload=bg.onreadystatechange=function(){if(!bb&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){bb=true;a4();a7();bg.onload=bg.onreadystatechange=null;if(a2&&bg.parentNode){a2.removeChild(bg)}}}}a2.insertBefore(bg,a2.firstChild);return C}var a6=false;var a5=a9.xhr();if(!a5){return}if(a9.username){a5.open(a1,a9.url,a9.async,a9.username,a9.password)}else{a5.open(a1,a9.url,a9.async)}try{if(a9.data||be&&be.contentType){a5.setRequestHeader("Content-Type",a9.contentType)}if(a9.ifModified){if(a.lastModified[a9.url]){a5.setRequestHeader("If-Modified-Since",a.lastModified[a9.url])}if(a.etag[a9.url]){a5.setRequestHeader("If-None-Match",a.etag[a9.url])}}if(!a3){a5.setRequestHeader("X-Requested-With","XMLHttpRequest")}a5.setRequestHeader("Accept",a9.dataType&&a9.accepts[a9.dataType]?a9.accepts[a9.dataType]+", */*":a9.accepts._default)}catch(bf){}if(a9.beforeSend&&a9.beforeSend.call(bk,a5,a9)===false){if(a9.global&&!--a.active){a.event.trigger("ajaxStop")}a5.abort();return false}if(a9.global){ba("ajaxSend",[a5,a9])}var a8=a5.onreadystatechange=function(bl){if(!a5||a5.readyState===0||bl==="abort"){if(!a6){a7()}a6=true;if(a5){a5.onreadystatechange=a.noop}}else{if(!a6&&a5&&(a5.readyState===4||bl==="timeout")){a6=true;a5.onreadystatechange=a.noop;bd=bl==="timeout"?"timeout":!a.httpSuccess(a5)?"error":a9.ifModified&&a.httpNotModified(a5,a9.url)?"notmodified":"success";var bn;if(bd==="success"){try{bi=a.httpData(a5,a9.dataType,a9)}catch(bm){bd="parsererror";bn=bm}}if(bd==="success"||bd==="notmodified"){if(!bj){a4()}}else{a.handleError(a9,a5,bd,bn)}a7();if(bl==="timeout"){a5.abort()}if(a9.async){a5=null}}}};try{var a0=a5.abort;a5.abort=function(){if(a5){a0.call(a5)}a8("abort")}}catch(bf){}if(a9.async&&a9.timeout>0){setTimeout(function(){if(a5&&!a6){a8("timeout")}},a9.timeout)}try{a5.send(a1==="POST"||a1==="PUT"||a1==="DELETE"?a9.data:null)}catch(bf){a.handleError(a9,a5,null,bf);a7()}if(!a9.async){a8()}function a4(){if(a9.success){a9.success.call(bk,bi,bd,a5)}if(a9.global){ba("ajaxSuccess",[a5,a9])}}function a7(){if(a9.complete){a9.complete.call(bk,a5,bd)}if(a9.global){ba("ajaxComplete",[a5,a9])}if(a9.global&&!--a.active){a.event.trigger("ajaxStop")}}function ba(bm,bl){(a9.context?a(a9.context):a.event).trigger(bm,bl)}return a5},handleError:function(a0,a2,aZ,a1){if(a0.error){a0.error.call(a0.context||a0,a2,aZ,a1)}if(a0.global){(a0.context?a(a0.context):a.event).trigger("ajaxError",[a2,a0,a1])}},active:0,httpSuccess:function(a0){try{return !a0.status&&location.protocol==="file:"||(a0.status>=200&&a0.status<300)||a0.status===304||a0.status===1223||a0.status===0}catch(aZ){}return false},httpNotModified:function(a2,aZ){var a1=a2.getResponseHeader("Last-Modified"),a0=a2.getResponseHeader("Etag");if(a1){a.lastModified[aZ]=a1}if(a0){a.etag[aZ]=a0}return a2.status===304||a2.status===0},httpData:function(a4,a2,a1){var a0=a4.getResponseHeader("content-type")||"",aZ=a2==="xml"||!a2&&a0.indexOf("xml")>=0,a3=aZ?a4.responseXML:a4.responseText;if(aZ&&a3.documentElement.nodeName==="parsererror"){a.error("parsererror")}if(a1&&a1.dataFilter){a3=a1.dataFilter(a3,a2)}if(typeof a3==="string"){if(a2==="json"||!a2&&a0.indexOf("json")>=0){a3=a.parseJSON(a3)}else{if(a2==="script"||!a2&&a0.indexOf("javascript")>=0){a.globalEval(a3)}}}return a3},param:function(aZ,a2){var a0=[];if(a2===C){a2=a.ajaxSettings.traditional}if(a.isArray(aZ)||aZ.jquery){a.each(aZ,function(){a4(this.name,this.value)})}else{for(var a3 in aZ){a1(a3,aZ[a3])}}return a0.join("&").replace(h,"+");function a1(a5,a6){if(a.isArray(a6)){a.each(a6,function(a8,a7){if(a2||/\[\]$/.test(a5)){a4(a5,a7)}else{a1(a5+"["+(typeof a7==="object"||a.isArray(a7)?a8:"")+"]",a7)}})}else{if(!a2&&a6!=null&&typeof a6==="object"){a.each(a6,function(a8,a7){a1(a5+"["+a8+"]",a7)})}else{a4(a5,a6)}}}function a4(a5,a6){a6=a.isFunction(a6)?a6():a6;a0[a0.length]=encodeURIComponent(a5)+"="+encodeURIComponent(a6)}}});var H={},af=/toggle|show|hide/,av=/^([+-]=)?([\d+-.]+)(.*)$/,aG,ak=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];a.fn.extend({show:function(a0,a8){if(a0||a0===0){return this.animate(aE("show",3),a0,a8)}else{for(var a5=0,a2=this.length;a5").appendTo("body");a6=a1.css("display");if(a6==="none"){a6="block"}a1.remove();H[a7]=a6}a.data(this[a5],"olddisplay",a6)}}for(var a4=0,a3=this.length;a4=0;a2--){if(a1[a2].elem===this){if(aZ){a1[a2](true)}a1.splice(a2,1)}}});if(!aZ){this.dequeue()}return this}});a.each({slideDown:aE("show",1),slideUp:aE("hide",1),slideToggle:aE("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(aZ,a0){a.fn[aZ]=function(a1,a2){return this.animate(a0,a1,a2)}});a.extend({speed:function(a1,a2,a0){var aZ=a1&&typeof a1==="object"?a1:{complete:a0||!a0&&a2||a.isFunction(a1)&&a1,duration:a1,easing:a0&&a2||a2&&!a.isFunction(a2)&&a2};aZ.duration=a.fx.off?0:typeof aZ.duration==="number"?aZ.duration:a.fx.speeds[aZ.duration]||a.fx.speeds._default;aZ.old=aZ.complete;aZ.complete=function(){if(aZ.queue!==false){a(this).dequeue()}if(a.isFunction(aZ.old)){aZ.old.call(this)}};return aZ},easing:{linear:function(a1,a2,aZ,a0){return aZ+a0*a1},swing:function(a1,a2,aZ,a0){return((-Math.cos(a1*Math.PI)/2)+0.5)*a0+aZ}},timers:[],fx:function(a0,aZ,a1){this.options=aZ;this.elem=a0;this.prop=a1;if(!aZ.orig){aZ.orig={}}}});a.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(a.fx.step[this.prop]||a.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(a0){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var aZ=parseFloat(a.css(this.elem,this.prop,a0));return aZ&&aZ>-10000?aZ:parseFloat(a.curCSS(this.elem,this.prop))||0},custom:function(a3,a2,a1){this.startTime=aQ();this.start=a3;this.end=a2;this.unit=a1||this.unit||"px";this.now=this.start;this.pos=this.state=0;var aZ=this;function a0(a4){return aZ.step(a4)}a0.elem=this.elem;if(a0()&&a.timers.push(a0)&&!aG){aG=setInterval(a.fx.tick,13)}},show:function(){this.options.orig[this.prop]=a.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());a(this.elem).show()},hide:function(){this.options.orig[this.prop]=a.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a2){var a7=aQ(),a3=true;if(a2||a7>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var a4 in this.options.curAnim){if(this.options.curAnim[a4]!==true){a3=false}}if(a3){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;var a1=a.data(this.elem,"olddisplay");this.elem.style.display=a1?a1:this.options.display;if(a.css(this.elem,"display")==="none"){this.elem.style.display="block"}}if(this.options.hide){a(this.elem).hide()}if(this.options.hide||this.options.show){for(var aZ in this.options.curAnim){a.style(this.elem,aZ,this.options.orig[aZ])}}this.options.complete.call(this.elem)}return false}else{var a0=a7-this.startTime;this.state=a0/this.options.duration;var a5=this.options.specialEasing&&this.options.specialEasing[this.prop];var a6=this.options.easing||(a.easing.swing?"swing":"linear");this.pos=a.easing[a5||a6](this.state,a0,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};a.extend(a.fx,{tick:function(){var a0=a.timers;for(var aZ=0;aZ
";a.extend(a0.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});a0.innerHTML=a2;aZ.insertBefore(a0,aZ.firstChild);a3=a0.firstChild;a5=a3.firstChild;a6=a3.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(a5.offsetTop!==5);this.doesAddBorderForTableAndCells=(a6.offsetTop===5);a5.style.position="fixed",a5.style.top="20px";this.supportsFixedPosition=(a5.offsetTop===20||a5.offsetTop===15);a5.style.position=a5.style.top="";a3.style.overflow="hidden",a3.style.position="relative";this.subtractsBorderForOverflowNotVisible=(a5.offsetTop===-5);this.doesNotIncludeMarginInBodyOffset=(aZ.offsetTop!==a1);aZ.removeChild(a0);aZ=a0=a3=a5=a4=a6=null;a.offset.initialize=a.noop},bodyOffset:function(aZ){var a1=aZ.offsetTop,a0=aZ.offsetLeft;a.offset.initialize();if(a.offset.doesNotIncludeMarginInBodyOffset){a1+=parseFloat(a.curCSS(aZ,"marginTop",true))||0;a0+=parseFloat(a.curCSS(aZ,"marginLeft",true))||0}return{top:a1,left:a0}},setOffset:function(a4,a0,a1){if(/static/.test(a.curCSS(a4,"position"))){a4.style.position="relative"}var a3=a(a4),a6=a3.offset(),aZ=parseInt(a.curCSS(a4,"top",true),10)||0,a5=parseInt(a.curCSS(a4,"left",true),10)||0;if(a.isFunction(a0)){a0=a0.call(a4,a1,a6)}var a2={top:(a0.top-a6.top)+aZ,left:(a0.left-a6.left)+a5};if("using" in a0){a0.using.call(a4,a2)}else{a3.css(a2)}}};a.fn.extend({position:function(){if(!this[0]){return null}var a1=this[0],a0=this.offsetParent(),a2=this.offset(),aZ=/^body|html$/i.test(a0[0].nodeName)?{top:0,left:0}:a0.offset();a2.top-=parseFloat(a.curCSS(a1,"marginTop",true))||0;a2.left-=parseFloat(a.curCSS(a1,"marginLeft",true))||0;aZ.top+=parseFloat(a.curCSS(a0[0],"borderTopWidth",true))||0;aZ.left+=parseFloat(a.curCSS(a0[0],"borderLeftWidth",true))||0;return{top:a2.top-aZ.top,left:a2.left-aZ.left}},offsetParent:function(){return this.map(function(){var aZ=this.offsetParent||ac.body;while(aZ&&(!/^body|html$/i.test(aZ.nodeName)&&a.css(aZ,"position")==="static")){aZ=aZ.offsetParent}return aZ})}});a.each(["Left","Top"],function(a0,aZ){var a1="scroll"+aZ;a.fn[a1]=function(a4){var a2=this[0],a3;if(!a2){return null}if(a4!==C){return this.each(function(){a3=an(this);if(a3){a3.scrollTo(!a0?a4:a(a3).scrollLeft(),a0?a4:a(a3).scrollTop())}else{this[a1]=a4}})}else{a3=an(a2);return a3?("pageXOffset" in a3)?a3[a0?"pageYOffset":"pageXOffset"]:a.support.boxModel&&a3.document.documentElement[a1]||a3.document.body[a1]:a2[a1]}}});function an(aZ){return("scrollTo" in aZ&&aZ.document)?aZ:aZ.nodeType===9?aZ.defaultView||aZ.parentWindow:false}a.each(["Height","Width"],function(a0,aZ){var a1=aZ.toLowerCase();a.fn["inner"+aZ]=function(){return this[0]?a.css(this[0],a1,false,"padding"):null};a.fn["outer"+aZ]=function(a2){return this[0]?a.css(this[0],a1,false,a2?"margin":"border"):null};a.fn[a1]=function(a2){var a3=this[0];if(!a3){return a2==null?null:this}if(a.isFunction(a2)){return this.each(function(a5){var a4=a(this);a4[a1](a2.call(this,a5,a4[a1]()))})}return("scrollTo" in a3&&a3.document)?a3.document.compatMode==="CSS1Compat"&&a3.document.documentElement["client"+aZ]||a3.document.body["client"+aZ]:(a3.nodeType===9)?Math.max(a3.documentElement["client"+aZ],a3.body["scroll"+aZ],a3.documentElement["scroll"+aZ],a3.body["offset"+aZ],a3.documentElement["offset"+aZ]):a2===C?a.css(a3,a1):this.css(a1,typeof a2==="string"?a2:a2+"px")}});aN.jQuery=aN.$=a})(window); 24 | /*! 25 | * jQuery postMessage - v0.5 - 9/11/2009 26 | * http://benalman.com/projects/jquery-postmessage-plugin/ 27 | * 28 | * Copyright (c) 2009 "Cowboy" Ben Alman 29 | * Dual licensed under the MIT and GPL licenses. 30 | * http://benalman.com/about/license/ 31 | */ 32 | (function($){var b,d,j=1,a,f=this,g=!1,h="postMessage",c="addEventListener",e,i=f[h];$[h]=function(k,m,l){if(!m){return}k=typeof k==="string"?k:$.param(k);l=l||parent;if(i){l[h](k,m.replace(/([^:]+:\/\/[^\/]+).*/,"$1"))}else{if(m){l.location=m.replace(/#.*$/,"")+"#"+(+new Date)+(j++)+"&"+k}}};$.receiveMessage=e=function(m,l,k){if(i){if(m){a&&e();a=function(n){if((typeof l==="string"&&n.origin!==l)||($.isFunction(l)&&l(n.origin)===g)){return g}m(n)}}if(f[c]){f[m?c:"removeEventListener"]("message",a,g)}else{f[m?"attachEvent":"detachEvent"]("onmessage",a)}}else{b&&clearInterval(b);b=null;if(m){k=typeof l==="number"?l:typeof k==="number"?k:100;b=setInterval(function(){var o=document.location.hash,n=/^#?\d+&/;if(o!==d&&n.test(o)){d=o;m({data:o.replace(n,"")})}},k)}}}})(jQuery);var D=$.noConflict();var Duo={init:function(b){if(!b){alert("Error: missing arguments in Duo.init()");return}if(!b.host){alert("Error: missing 'host' argument in Duo.init()");return}Duo._host=b.host;if(!b.sig_request){alert("Error: missing 'sig_request' argument in Duo.init()");return}Duo._sig_request=b.sig_request;if(Duo._sig_request.indexOf("ERR|")==0){var c=Duo._sig_request.split("|");alert("Error: "+c[1]);return}if(Duo._sig_request.indexOf(":")==-1){alert("Invalid sig_request value");return}var a=Duo._sig_request.split(":");if(a.length!=2){alert("Invalid sig_request value");return}Duo._duo_sig=a[0];Duo._app_sig=a[1];if(!b.post_action){b.post_action=""}Duo._post_action=b.post_action;if(!b.post_argument){b.post_argument="sig_response"}Duo._post_argument=b.post_argument;Duo._mage_form_key=b.form_key},ready:function(){var b=D("#duo_iframe");if(!b.length){alert("Error: missing IFRAME element with id 'duo_iframe'");return}var a=D.param({tx:Duo._duo_sig,parent:document.location.href});var c="https://"+Duo._host+"/frame/web/v1/auth?"+a;b.attr("src",c);D.receiveMessage(function(g){var f=g.data+":"+Duo._app_sig;var d=D('').attr("name",Duo._post_argument).val(f);var e=D("#duo_form");if(!e.length){e=D("
");e.insertAfter(b)}e.attr("method","POST");e.attr("action",Duo._post_action);e.append(d);e.append('');e.submit()},"https://"+Duo._host)}};D(document).ready(function(){Duo.ready()}); -------------------------------------------------------------------------------- /js/he_twofactor/Duo-Web-v1.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery postMessage - v0.5 - 9/11/2009 3 | * http://benalman.com/projects/jquery-postmessage-plugin/ 4 | * 5 | * Copyright (c) 2009 "Cowboy" Ben Alman 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://benalman.com/about/license/ 8 | */ 9 | 10 | // Script: jQuery postMessage: Cross-domain scripting goodness 11 | // 12 | // *Version: 0.5, Last updated: 9/11/2009* 13 | // 14 | // Project Home - http://benalman.com/projects/jquery-postmessage-plugin/ 15 | // GitHub - http://github.com/cowboy/jquery-postmessage/ 16 | // Source - http://github.com/cowboy/jquery-postmessage/raw/master/jquery.ba-postmessage.js 17 | // (Minified) - http://github.com/cowboy/jquery-postmessage/raw/master/jquery.ba-postmessage.min.js (0.9kb) 18 | // 19 | // About: License 20 | // 21 | // Copyright (c) 2009 "Cowboy" Ben Alman, 22 | // Dual licensed under the MIT and GPL licenses. 23 | // http://benalman.com/about/license/ 24 | // 25 | // About: Examples 26 | // 27 | // This working example, complete with fully commented code, illustrates one 28 | // way in which this plugin can be used. 29 | // 30 | // Iframe resizing - http://benalman.com/code/projects/jquery-postmessage/examples/iframe/ 31 | // 32 | // About: Support and Testing 33 | // 34 | // Information about what version or versions of jQuery this plugin has been 35 | // tested with and what browsers it has been tested in. 36 | // 37 | // jQuery Versions - 1.3.2 38 | // Browsers Tested - Internet Explorer 6-8, Firefox 3, Safari 3-4, Chrome, Opera 9. 39 | // 40 | // About: Release History 41 | // 42 | // 0.5 - (9/11/2009) Improved cache-busting 43 | // 0.4 - (8/25/2009) Initial release 44 | 45 | (function($){ 46 | '$:nomunge'; // Used by YUI compressor. 47 | 48 | // A few vars used in non-awesome browsers. 49 | var interval_id, 50 | last_hash, 51 | cache_bust = 1, 52 | 53 | // A var used in awesome browsers. 54 | rm_callback, 55 | 56 | // A few convenient shortcuts. 57 | window = this, 58 | FALSE = !1, 59 | 60 | // Reused internal strings. 61 | postMessage = 'postMessage', 62 | addEventListener = 'addEventListener', 63 | 64 | p_receiveMessage, 65 | 66 | has_postMessage = window[postMessage]; 67 | 68 | // Method: jQuery.postMessage 69 | // 70 | // This method will call window.postMessage if available, setting the 71 | // targetOrigin parameter to the base of the target_url parameter for maximum 72 | // security in browsers that support it. If window.postMessage is not available, 73 | // the target window's location.hash will be used to pass the message. If an 74 | // object is passed as the message param, it will be serialized into a string 75 | // using the jQuery.param method. 76 | // 77 | // Usage: 78 | // 79 | // > jQuery.postMessage( message, target_url [, target ] ); 80 | // 81 | // Arguments: 82 | // 83 | // message - (String) A message to be passed to the other frame. 84 | // message - (Object) An object to be serialized into a params string, using 85 | // the jQuery.param method. 86 | // target_url - (String) The URL of the other frame this window is 87 | // attempting to communicate with. This must be the exact URL (including 88 | // any query string) of the other window for this script to work in 89 | // browsers that don't support window.postMessage. 90 | // target - (Object) A reference to the other frame this window is 91 | // attempting to communicate with. If omitted, defaults to `parent`. 92 | // 93 | // Returns: 94 | // 95 | // Nothing. 96 | 97 | $[postMessage] = function( message, target_url, target ) { 98 | if ( !target_url ) { return; } 99 | 100 | // Serialize the message if not a string. Note that this is the only real 101 | // jQuery dependency for this script. If removed, this script could be 102 | // written as very basic JavaScript. 103 | message = typeof message === 'string' ? message : $.param( message ); 104 | 105 | // Default to parent if unspecified. 106 | target = target || parent; 107 | 108 | if ( has_postMessage ) { 109 | // The browser supports window.postMessage, so call it with a targetOrigin 110 | // set appropriately, based on the target_url parameter. 111 | target[postMessage]( message, target_url.replace( /([^:]+:\/\/[^\/]+).*/, '$1' ) ); 112 | 113 | } else if ( target_url ) { 114 | // The browser does not support window.postMessage, so set the location 115 | // of the target to target_url#message. A bit ugly, but it works! A cache 116 | // bust parameter is added to ensure that repeat messages trigger the 117 | // callback. 118 | target.location = target_url.replace( /#.*$/, '' ) + '#' + (+new Date) + (cache_bust++) + '&' + message; 119 | } 120 | }; 121 | 122 | // Method: jQuery.receiveMessage 123 | // 124 | // Register a single callback for either a window.postMessage call, if 125 | // supported, or if unsupported, for any change in the current window 126 | // location.hash. If window.postMessage is supported and source_origin is 127 | // specified, the source window will be checked against this for maximum 128 | // security. If window.postMessage is unsupported, a polling loop will be 129 | // started to watch for changes to the location.hash. 130 | // 131 | // Note that for simplicity's sake, only a single callback can be registered 132 | // at one time. Passing no params will unbind this event (or stop the polling 133 | // loop), and calling this method a second time with another callback will 134 | // unbind the event (or stop the polling loop) first, before binding the new 135 | // callback. 136 | // 137 | // Also note that if window.postMessage is available, the optional 138 | // source_origin param will be used to test the event.origin property. From 139 | // the MDC window.postMessage docs: This string is the concatenation of the 140 | // protocol and "://", the host name if one exists, and ":" followed by a port 141 | // number if a port is present and differs from the default port for the given 142 | // protocol. Examples of typical origins are https://example.org (implying 143 | // port 443), http://example.net (implying port 80), and http://example.com:8080. 144 | // 145 | // Usage: 146 | // 147 | // > jQuery.receiveMessage( callback [, source_origin ] [, delay ] ); 148 | // 149 | // Arguments: 150 | // 151 | // callback - (Function) This callback will execute whenever a 152 | // message is received, provided the source_origin matches. If callback is 153 | // omitted, any existing receiveMessage event bind or polling loop will be 154 | // canceled. 155 | // source_origin - (String) If window.postMessage is available and this value 156 | // is not equal to the event.origin property, the callback will not be 157 | // called. 158 | // source_origin - (Function) If window.postMessage is available and this 159 | // function returns false when passed the event.origin property, the 160 | // callback will not be called. 161 | // delay - (Number) An optional zero-or-greater delay in milliseconds at 162 | // which the polling loop will execute (for browser that don't support 163 | // window.postMessage). If omitted, defaults to 100. 164 | // 165 | // Returns: 166 | // 167 | // Nothing! 168 | 169 | $.receiveMessage = p_receiveMessage = function( callback, source_origin, delay ) { 170 | if ( has_postMessage ) { 171 | // Since the browser supports window.postMessage, the callback will be 172 | // bound to the actual event associated with window.postMessage. 173 | 174 | if ( callback ) { 175 | // Unbind an existing callback if it exists. 176 | rm_callback && p_receiveMessage(); 177 | 178 | // Bind the callback. A reference to the callback is stored for ease of 179 | // unbinding. 180 | rm_callback = function(e) { 181 | if ( ( typeof source_origin === 'string' && e.origin !== source_origin ) 182 | || ( $.isFunction( source_origin ) && source_origin( e.origin ) === FALSE ) ) { 183 | return FALSE; 184 | } 185 | callback( e ); 186 | }; 187 | } 188 | 189 | if ( window[addEventListener] ) { 190 | window[ callback ? addEventListener : 'removeEventListener' ]( 'message', rm_callback, FALSE ); 191 | } else { 192 | window[ callback ? 'attachEvent' : 'detachEvent' ]( 'onmessage', rm_callback ); 193 | } 194 | 195 | } else { 196 | // Since the browser sucks, a polling loop will be started, and the 197 | // callback will be called whenever the location.hash changes. 198 | 199 | interval_id && clearInterval( interval_id ); 200 | interval_id = null; 201 | 202 | if ( callback ) { 203 | delay = typeof source_origin === 'number' 204 | ? source_origin 205 | : typeof delay === 'number' 206 | ? delay 207 | : 100; 208 | 209 | interval_id = setInterval(function(){ 210 | var hash = document.location.hash, 211 | re = /^#?\d+&/; 212 | if ( hash !== last_hash && re.test( hash ) ) { 213 | last_hash = hash; 214 | callback({ data: hash.replace( re, '' ) }); 215 | } 216 | }, delay ); 217 | } 218 | } 219 | }; 220 | 221 | })(jQuery); 222 | 223 | 224 | var D = jQuery; 225 | 226 | 227 | /* 228 | * Duo Web SDK v1 229 | * Copyright 2011, Duo Security 230 | */ 231 | 232 | var Duo = { 233 | init: function(options) { 234 | /* sanity check for argument dict */ 235 | if (!options) { 236 | alert('Error: missing arguments in Duo.init()'); 237 | return; 238 | } 239 | 240 | /* API hostname is a required argument */ 241 | if (!options.host) { 242 | alert('Error: missing \'host\' argument in Duo.init()'); 243 | return; 244 | } 245 | Duo._host = options.host; 246 | 247 | /* sig_request is a required argument */ 248 | if (!options.sig_request) { 249 | alert('Error: missing \'sig_request\' argument in Duo.init()'); 250 | return; 251 | } 252 | Duo._sig_request = options.sig_request; 253 | 254 | /* check for sig_request errors */ 255 | if (Duo._sig_request.indexOf('ERR|') == 0) { 256 | var err_array = Duo._sig_request.split('|'); 257 | alert('Error: ' + err_array[1]); 258 | return; 259 | } 260 | 261 | /* validate and parse the sig_request */ 262 | if (Duo._sig_request.indexOf(':') == -1) { 263 | alert('Invalid sig_request value'); 264 | return; 265 | } 266 | var sig_array = Duo._sig_request.split(':'); 267 | if (sig_array.length != 2) { 268 | alert('Invalid sig_request value'); 269 | return; 270 | } 271 | Duo._duo_sig = sig_array[0]; 272 | Duo._app_sig = sig_array[1]; 273 | 274 | /* allow override of the POST-back action URI */ 275 | if (!options.post_action) { 276 | options.post_action = ''; 277 | } 278 | Duo._post_action = options.post_action; 279 | 280 | /* allow override of the POST-back argument name */ 281 | if (!options.post_argument) { 282 | options.post_argument = 'sig_response'; 283 | } 284 | Duo._post_argument = options.post_argument; 285 | }, 286 | 287 | ready: function() { 288 | var iframe = D('#duo_iframe'); 289 | 290 | /* sanity check for a duo_iframe element */ 291 | if (!iframe.length) { 292 | alert('Error: missing IFRAME element with id \'duo_iframe\''); 293 | return; 294 | } 295 | 296 | var args = D.param({ 297 | 'tx': Duo._duo_sig, 298 | 'parent': document.location.href 299 | }); 300 | 301 | var src = 'https://' + Duo._host + '/frame/web/v1/auth?' + args; 302 | iframe.attr('src', src); 303 | 304 | D.receiveMessage(function(msg) { 305 | var sig_response = msg.data + ':' + Duo._app_sig; 306 | var input = D('').attr('name', Duo._post_argument).val(sig_response); 307 | 308 | var form = D('#duo_form'); 309 | if (!form.length) { 310 | form = D(''); 311 | form.insertAfter(iframe); 312 | } 313 | 314 | form.attr('method', 'POST'); 315 | form.attr('action', Duo._post_action); 316 | form.append(input); 317 | form.append(''); 318 | form.submit(); 319 | }, 'https://' + Duo._host); 320 | } 321 | }; 322 | 323 | D(document).ready(function() { 324 | Duo.ready(); 325 | }); 326 | -------------------------------------------------------------------------------- /js/he_twofactor/adminpanel.js: -------------------------------------------------------------------------------- 1 | //jquery substitute for document ready (no guarantee that either jquery OR prototype will be available) 2 | (function(g,b){function c(){if(!e){e=!0;for(var a=0;a0&&oVal.toLowerCase().indexOf('disable')!=0){ 19 | //get the a link for this toggle-able section (if exists) 20 | var alink=document.getElementById('he2faconfig_'+oVal+'-head'); 21 | //if this link exists 22 | if(alink!=undefined){ 23 | //get the section wrap 24 | var wrap=alink.parentNode.parentNode; 25 | //if the wrap exists 26 | if(wrap!=undefined){ 27 | var hideShowElems=[]; 28 | //if this wrap is the entry-edit div (older version of magento has different html) 29 | if(wrap.className.indexOf('entry-edit')!==-1){ 30 | //toggle hide/show link and fieldset elements separately because there is no common parent element in this version of magento 31 | hideShowElems.push(alink.parentNode); 32 | var fieldset=document.getElementById('he2faconfig_'+oVal); 33 | hideShowElems.push(fieldset); 34 | }else{ 35 | //toggle hiding and showing this wrapper for this version of magento 36 | hideShowElems.push(wrap); 37 | } 38 | //cache the toggle-able elements for this select item 39 | toggleElems.push({'key':oVal,'wrap':wrap,'alink':alink,'toggleList':hideShowElems}); 40 | } 41 | }else{ 42 | console.log('"'+oVal+'" does NOT appear as an admin section although it appears in the provider selection dropdown!'); 43 | } 44 | } 45 | } 46 | }; 47 | //FUNCTION: update which wrapped section is visible, based on current provider selection 48 | var updateVisibleWrap=function(){ 49 | //if the toggle-able wraps are cached 50 | if(toggleElems!=undefined){ 51 | //if providerSelect is loaded (if exists) 52 | if(providerSelect!=undefined){ 53 | //get the selected value 54 | var selKey=providerSelect.value; 55 | //for each toggle-able wrap 56 | for(var w=0;w= intval($exp)) { 46 | return null; 47 | } 48 | 49 | return $user; 50 | } 51 | 52 | public static function signRequest($ikey, $skey, $akey, $username, $time=NULL) { 53 | if (!isset($username) || strlen($username) == 0){ 54 | return self::ERR_USER; 55 | } 56 | if (!isset($ikey) || strlen($ikey) != self::IKEY_LEN) { 57 | return self::ERR_IKEY; 58 | } 59 | if (!isset($skey) || strlen($skey) != self::SKEY_LEN) { 60 | return self::ERR_SKEY; 61 | } 62 | if (!isset($akey) || strlen($akey) < self::AKEY_LEN) { 63 | return self::ERR_AKEY; 64 | } 65 | 66 | $vals = $username . '|' . $ikey; 67 | 68 | $duo_sig = self::sign_vals($skey, $vals, self::DUO_PREFIX, self::DUO_EXPIRE, $time); 69 | $app_sig = self::sign_vals($akey, $vals, self::APP_PREFIX, self::APP_EXPIRE, $time); 70 | 71 | return $duo_sig . ':' . $app_sig; 72 | } 73 | 74 | public static function verifyResponse($ikey, $skey, $akey, $sig_response, $time=NULL) { 75 | list($auth_sig, $app_sig) = explode(':', $sig_response); 76 | 77 | $auth_user = self::parse_vals($skey, $auth_sig, self::AUTH_PREFIX, $time); 78 | $app_user = self::parse_vals($akey, $app_sig, self::APP_PREFIX, $time); 79 | 80 | if ($auth_user != $app_user) { 81 | return null; 82 | } 83 | 84 | return $auth_user; 85 | } 86 | } 87 | 88 | ?> 89 | -------------------------------------------------------------------------------- /lib/GoogleAuthenticator/PHPGangsta/GoogleAuthenticator.php: -------------------------------------------------------------------------------- 1 | _getBase32LookupTable(); 26 | unset($validChars[32]); 27 | 28 | $secret = ''; 29 | for ($i = 0; $i < $secretLength; $i++) { 30 | $secret .= $validChars[array_rand($validChars)]; 31 | } 32 | return $secret; 33 | } 34 | 35 | /** 36 | * Calculate the code, with given secret and point in time 37 | * 38 | * @param string $secret 39 | * @param int|null $timeSlice 40 | * @return string 41 | */ 42 | public function getCode($secret, $timeSlice = null) 43 | { 44 | if ($timeSlice === null) { 45 | $timeSlice = floor(time() / 30); 46 | } 47 | 48 | $secretkey = $this->_base32Decode($secret); 49 | 50 | // Pack time into binary string 51 | $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); 52 | // Hash it with users secret key 53 | $hm = hash_hmac('SHA1', $time, $secretkey, true); 54 | // Use last nipple of result as index/offset 55 | $offset = ord(substr($hm, -1)) & 0x0F; 56 | // grab 4 bytes of the result 57 | $hashpart = substr($hm, $offset, 4); 58 | 59 | // Unpak binary value 60 | $value = unpack('N', $hashpart); 61 | $value = $value[1]; 62 | // Only 32 bits 63 | $value = $value & 0x7FFFFFFF; 64 | 65 | $modulo = pow(10, $this->_codeLength); 66 | return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT); 67 | } 68 | 69 | /** 70 | * Get QR-Code URL for image, from google charts 71 | * 72 | * @param string $name 73 | * @param string $secret 74 | * @param string $title 75 | * @return string 76 | */ 77 | public function getQRCodeGoogleUrl($name, $secret, $title = null) { 78 | $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.''); 79 | if(isset($title)) { 80 | $urlencoded .= urlencode('&issuer='.$title); 81 | } 82 | return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.''; 83 | } 84 | 85 | /** 86 | * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now 87 | * 88 | * @param string $secret 89 | * @param string $code 90 | * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after) 91 | * @return bool 92 | */ 93 | public function verifyCode($secret, $code, $discrepancy = 1) 94 | { 95 | $currentTimeSlice = floor(time() / 30); 96 | 97 | for ($i = -$discrepancy; $i <= $discrepancy; $i++) { 98 | $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i); 99 | if ($calculatedCode == $code ) { 100 | return true; 101 | } 102 | } 103 | 104 | return false; 105 | } 106 | 107 | /** 108 | * Set the code length, should be >=6 109 | * 110 | * @param int $length 111 | * @return PHPGangsta_GoogleAuthenticator 112 | */ 113 | public function setCodeLength($length) 114 | { 115 | $this->_codeLength = $length; 116 | return $this; 117 | } 118 | 119 | /** 120 | * Helper class to decode base32 121 | * 122 | * @param $secret 123 | * @return bool|string 124 | */ 125 | protected function _base32Decode($secret) 126 | { 127 | if (empty($secret)) return ''; 128 | 129 | $base32chars = $this->_getBase32LookupTable(); 130 | $base32charsFlipped = array_flip($base32chars); 131 | 132 | $paddingCharCount = substr_count($secret, $base32chars[32]); 133 | $allowedValues = array(6, 4, 3, 1, 0); 134 | if (!in_array($paddingCharCount, $allowedValues)) return false; 135 | for ($i = 0; $i < 4; $i++){ 136 | if ($paddingCharCount == $allowedValues[$i] && 137 | substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false; 138 | } 139 | $secret = str_replace('=','', $secret); 140 | $secret = str_split($secret); 141 | $binaryString = ""; 142 | for ($i = 0; $i < count($secret); $i = $i+8) { 143 | $x = ""; 144 | if (!in_array($secret[$i], $base32chars)) return false; 145 | for ($j = 0; $j < 8; $j++) { 146 | $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); 147 | } 148 | $eightBits = str_split($x, 8); 149 | for ($z = 0; $z < count($eightBits); $z++) { 150 | $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:""; 151 | } 152 | } 153 | return $binaryString; 154 | } 155 | 156 | /** 157 | * Helper class to encode base32 158 | * 159 | * @param string $secret 160 | * @param bool $padding 161 | * @return string 162 | */ 163 | protected function _base32Encode($secret, $padding = true) 164 | { 165 | if (empty($secret)) return ''; 166 | 167 | $base32chars = $this->_getBase32LookupTable(); 168 | 169 | $secret = str_split($secret); 170 | $binaryString = ""; 171 | for ($i = 0; $i < count($secret); $i++) { 172 | $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT); 173 | } 174 | $fiveBitBinaryArray = str_split($binaryString, 5); 175 | $base32 = ""; 176 | $i = 0; 177 | while ($i < count($fiveBitBinaryArray)) { 178 | $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)]; 179 | $i++; 180 | } 181 | if ($padding && ($x = strlen($binaryString) % 40) != 0) { 182 | if ($x == 8) $base32 .= str_repeat($base32chars[32], 6); 183 | elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4); 184 | elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3); 185 | elseif ($x == 32) $base32 .= $base32chars[32]; 186 | } 187 | return $base32; 188 | } 189 | 190 | /** 191 | * Get array with all 32 characters for decoding from/encoding to base32 192 | * 193 | * @return array 194 | */ 195 | protected function _getBase32LookupTable() 196 | { 197 | return array( 198 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 199 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 200 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 201 | 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 202 | '=' // padding char 203 | ); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /modman: -------------------------------------------------------------------------------- 1 | # HE_TwoFactorAuth 2 | 3 | app/code/community/HE/TwoFactorAuth app/code/community/HE/TwoFactorAuth 4 | app/design/adminhtml/default/default/layout/he_twofactor app/design/adminhtml/default/default/layout/he_twofactor 5 | app/design/adminhtml/default/default/template/he_twofactor app/design/adminhtml/default/default/template/he_twofactor 6 | js/he_twofactor js/he_twofactor 7 | lib/Duo lib/Duo 8 | lib/GoogleAuthenticator lib/GoogleAuthenticator 9 | app/etc/modules/HE_TwoFactorAuth.xml app/etc/modules/HE_TwoFactorAuth.xml 10 | skin/adminhtml/default/default/he_twofactor skin/adminhtml/default/default/he_twofactor 11 | 12 | -------------------------------------------------------------------------------- /skin/adminhtml/default/default/he_twofactor/css/admin.css: -------------------------------------------------------------------------------- 1 | li.sentry-section dl:first-child dt.label{} 2 | 3 | ul.tabs a.sentry-item span, 4 | ul.tabs a.sentry-item:hover span{background-repeat:no-repeat;background-position:4px center;padding-left:1.5rem;background-color:#2e2e2e;color:#fff;} 5 | 6 | ul.tabs a.sentry-settings span,ul.tabs a.sentry-settings:hover span{background-size:17px;background-image:url(../images/icon_lock_left_menu.svg);} 7 | 8 | .sentry-intro{margin-bottom:.5rem;background-image:url(../images/he_nexcess_logos.gif);background-repeat:no-repeat;background-position:right top;min-height:60px;} 9 | .sentry-intro .line{} 10 | 11 | .sentry-intro,.sentry-banner{min-width:975px;} 12 | 13 | .sentry-banner{border:solid .1rem #dbdbdb;background-color:#fbfbfb;} 14 | .sentry-banner .sentry-ribbon{position:relative;background-color:#2e2e2e;color:#dadada;padding:.7rem;padding-left:40px;padding-bottom:0;border-bottom:solid .35rem #2e2e2e;margin-bottom:.5rem;font-size:.8rem; 15 | background-image:url(../images/icon_sentry.gif);background-position:9px bottom;background-repeat:no-repeat;background-size:23px;} 16 | .sentry-banner .sentry-ribbon .btns{position:absolute;right:.4rem;top:0;bottom:0;margin:auto;height:100%;max-width:30%;text-align:right;white-space:nowrap;min-width:300px;} 17 | .sentry-banner .sentry-ribbon .btns .btn{font-size:.8rem;display:inline-block;margin-right:.5rem;margin-top:.45rem;padding:.2rem 2rem;border-radius:3px;color:#fff;background-color:green;box-shadow:-1px 1px 3px rgba(0,0,0,.9); 18 | background: rgb(36,160,206); 19 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzI0YTBjZSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMwMzdlYWMiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); 20 | background: -moz-linear-gradient(top, rgba(36,160,206,1) 0%, rgba(3,126,172,1) 100%); 21 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(36,160,206,1)), color-stop(100%,rgba(3,126,172,1))); 22 | background: -webkit-linear-gradient(top, rgba(36,160,206,1) 0%,rgba(3,126,172,1) 100%); 23 | background: -o-linear-gradient(top, rgba(36,160,206,1) 0%,rgba(3,126,172,1) 100%); 24 | background: -ms-linear-gradient(top, rgba(36,160,206,1) 0%,rgba(3,126,172,1) 100%); 25 | background: linear-gradient(to bottom, rgba(36,160,206,1) 0%,rgba(3,126,172,1) 100%); 26 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#24a0ce', endColorstr='#037eac',GradientType=0 ); 27 | } 28 | .sentry-banner .sentry-ribbon .btns .btn:last-child{margin-right:0;} 29 | .sentry-banner .sentry-ribbon .btns .btn:hover{text-decoration:none;} 30 | .sentry-banner .sentry-ribbon strong{color:#fff;text-transform:uppercase;font-size:1.5rem;font-weight:normal;letter-spacing:-1px;margin-right:.3rem;} 31 | .sentry-banner p{padding:0 .5rem;} 32 | 33 | .sentry-intro .line, .sentry-banner p{font-family:"Arial Regular", Arial, Helvetica, sans-serif;font-size:.8rem;} 34 | .sentry-intro a, .sentry-banner a, 35 | .sentry-intro a:visited, .sentry-banner a:visited 36 | {text-decoration:none;color:#0015dc;font-weight:bold;} 37 | .sentry-intro a:hover, .sentry-banner a:hover 38 | {text-decoration:underline;} 39 | -------------------------------------------------------------------------------- /skin/adminhtml/default/default/he_twofactor/images/he_nexcess_logos.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nexcess/magento-sentry-two-factor-authentication/bffb34f2b883dc4ff5c76027080a9b2dd3c9dd19/skin/adminhtml/default/default/he_twofactor/images/he_nexcess_logos.gif -------------------------------------------------------------------------------- /skin/adminhtml/default/default/he_twofactor/images/icon_lock_left_menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skin/adminhtml/default/default/he_twofactor/images/icon_sentry.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nexcess/magento-sentry-two-factor-authentication/bffb34f2b883dc4ff5c76027080a9b2dd3c9dd19/skin/adminhtml/default/default/he_twofactor/images/icon_sentry.gif -------------------------------------------------------------------------------- /util/get-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Nexcess.net Turpentine Extension for Magento 4 | # Copyright (C) 2012 Nexcess.net L.L.C. 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | BASE_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")" 21 | 22 | XML_PATH_VERSION='/config/modules/HE_TwoFactorAuth/version/text()' 23 | CONFIG_XML_PATH='app/code/community/HE/TwoFactorAuth/etc/config.xml' 24 | 25 | echo '' | xpath -e '*' &>/dev/null 26 | if [ $? -eq 2 ]; then 27 | XPATH_BIN='xpath' 28 | else 29 | XPATH_BIN='xpath -e' 30 | fi 31 | 32 | echo "$($XPATH_BIN "$XML_PATH_VERSION" \ 33 | < "${BASE_DIR}/${CONFIG_XML_PATH}" \ 34 | 2> /dev/null)" 35 | --------------------------------------------------------------------------------