Register.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. <?php
  2. /**
  3. * Login
  4. *
  5. * Copyright 2010 by Shaun McCormick <shaun+login@modx.com>
  6. *
  7. * Login is free software; you can redistribute it and/or modify it under the
  8. * terms of the GNU General Public License as published by the Free Software
  9. * Foundation; either version 2 of the License, or (at your option) any later
  10. * version.
  11. *
  12. * Login is distributed in the hope that it will be useful, but WITHOUT ANY
  13. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  14. * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License along with
  17. * Login; if not, write to the Free Software Foundation, Inc., 59 Temple
  18. * Place, Suite 330, Boston, MA 02111-1307 USA
  19. *
  20. * @package login
  21. */
  22. /**
  23. * Handles registration of users
  24. *
  25. * @package login
  26. * @subpackage controllers
  27. */
  28. class LoginRegisterController extends LoginController {
  29. /** @var boolean $hasPosted */
  30. public $hasPosted = false;
  31. public $success = false;
  32. /**
  33. * Load default properties for this controller
  34. * @return void
  35. */
  36. public function initialize() {
  37. $this->modx->lexicon->load('login:register');
  38. $this->setDefaultProperties(array(
  39. 'activation' => true,
  40. 'activationEmail' => '',
  41. 'activationEmailSubject' => $this->modx->lexicon('register.activation_email_subject'),
  42. 'activationEmailTpl' => 'lgnActivateEmailTpl',
  43. 'activationEmailTplType' => 'modChunk',
  44. 'activationEmailTplAlt' => '',
  45. 'activationResourceId' => '',
  46. 'emailField' => 'email',
  47. 'errTpl' => '<span class="error">[[+error]]</span>',
  48. 'excludeExtended' => '',
  49. 'moderatedResourceId' => '',
  50. 'passwordField' => 'password',
  51. 'persistParams' => '',
  52. 'placeholderPrefix' => '',
  53. 'preHooks' => '',
  54. 'postHooks' => '',
  55. 'redirectBack' => '',
  56. 'redirectBackParams' => '',
  57. 'redirectUnsetDefaultParams' => false,
  58. 'submittedResourceId' => '',
  59. 'submitVar' => 'login-register-btn',
  60. 'successMsg' => '',
  61. 'useExtended' => true,
  62. 'usergroups' => '',
  63. 'usernameField' => 'username',
  64. 'validate' => '',
  65. 'validatePassword' => true,
  66. 'autoLogin' => false,
  67. 'jsonResponse' => false,
  68. 'validationErrorMessage' => 'A form validation error occurred. Please check the values you have entered.',
  69. 'preserveFieldsAfterRegister' => true
  70. ));
  71. }
  72. /**
  73. * Handle the Register snippet business logic
  74. * @return string
  75. */
  76. public function process() {
  77. $this->checkForPost();
  78. $this->preLoad();
  79. if (!$this->hasPosted) {
  80. return '';
  81. }
  82. if (!$this->loadDictionary()) {
  83. return '';
  84. }
  85. $fields = $this->validateFields();
  86. $this->dictionary->reset();
  87. $this->dictionary->fromArray($fields);
  88. $this->validateUsername();
  89. if ($this->getProperty('validatePassword',true,'isset')) {
  90. $this->validatePassword();
  91. }
  92. if ($this->getProperty('ensurePasswordStrength',false,'isset')) {
  93. $this->ensurePasswordStrength();
  94. }
  95. if ($this->getProperty('generatePassword',false,'isset')) {
  96. $this->generatePassword();
  97. }
  98. $this->validateEmail();
  99. $placeholderPrefix = rtrim($this->getProperty('placeholderPrefix', ''), '.');
  100. $errorPrefix = ($placeholderPrefix) ? $placeholderPrefix . '.error' : 'error';
  101. if ($this->validator->hasErrors()) {
  102. $errors = $this->validator->getErrors();
  103. // Return JSON error response if requested.
  104. if ($this->getProperty('jsonResponse')) {
  105. $jsonErrorOutput = array(
  106. 'success' => false,
  107. 'message' => $this->getProperty('validationErrorMessage'),
  108. 'errors' => $errors
  109. );
  110. header('Content-Type: application/json;charset=utf-8');
  111. exit($this->modx->toJSON($jsonErrorOutput));
  112. }
  113. $this->modx->toPlaceholders($errors, $errorPrefix);
  114. $this->modx->toPlaceholder('validation_error', true, $placeholderPrefix);
  115. $this->modx->toPlaceholder('validation_error_message', $this->getProperty('validationErrorMessage'), $placeholderPrefix);
  116. } else {
  117. $this->loadPreHooks();
  118. /* process hooks */
  119. if ($this->preHooks->hasErrors()) {
  120. $this->modx->toPlaceholders($this->preHooks->getErrors(), $errorPrefix);
  121. $errorMsg = $this->preHooks->getErrorMessage();
  122. $this->modx->toPlaceholder('error.message', $errorMsg, $placeholderPrefix);
  123. } else {
  124. /* everything good, go ahead and register */
  125. $result = $this->runProcessor('register');
  126. if ($result !== true) {
  127. $this->modx->toPlaceholder('error.message', $result, $placeholderPrefix);
  128. } else {
  129. // Return JSON success response if requested
  130. if ($this->getProperty('jsonResponse')) {
  131. $jsonSuccessOutput = array(
  132. 'success' => true,
  133. 'message' => $this->getProperty('successMsg','User registration successful.')
  134. );
  135. header('Content-Type: application/json;charset=utf-8');
  136. exit($this->modx->toJSON($jsonSuccessOutput));
  137. }
  138. $this->modx->toPlaceholder('successMsg', $this->getProperty('successMsg','User registration successful.'), $placeholderPrefix);
  139. $this->success = true;
  140. }
  141. }
  142. }
  143. $placeholders = $this->dictionary->toArray();
  144. $placeholders = $this->escapePlaceholders($placeholders);
  145. $this->modx->toPlaceholders($placeholders, $placeholderPrefix);
  146. foreach ($placeholders as $k => $v) {
  147. if (is_array($v)) {
  148. $this->modx->toPlaceholder($k, json_encode($v), $placeholderPrefix);
  149. }
  150. }
  151. if (!$this->success || $this->getProperty('preserveFieldsAfterRegister')) {
  152. $this->modx->setPlaceholders($this->dictionary->toArray(), $placeholderPrefix);
  153. }
  154. return '';
  155. }
  156. /**
  157. * Load any pre-registration hooks
  158. * @return void
  159. */
  160. public function loadPreHooks() {
  161. $preHooks = $this->getProperty('preHooks','');
  162. $this->loadHooks('preHooks');
  163. if (!empty($preHooks)) {
  164. $fields = $this->dictionary->toArray();
  165. /* do pre-register hooks */
  166. $this->preHooks->loadMultiple($preHooks,$fields,array(
  167. 'submitVar' => $this->getProperty('submitVar'),
  168. 'usernameField' => $this->getProperty('usernameField','username'),
  169. ));
  170. $values = $this->preHooks->getValues();
  171. if (!empty($values)) {
  172. $this->dictionary->fromArray($values);
  173. }
  174. }
  175. }
  176. /**
  177. * Validate the fields in the form
  178. * @return array
  179. */
  180. public function validateFields() {
  181. $this->loadValidator();
  182. $fields = $this->validator->validateFields($this->dictionary,$this->getProperty('validate',''));
  183. foreach ($fields as $k => $v) {
  184. $fields[$k] = str_replace(array('[',']'),array('&#91;','&#93;'),$v);
  185. }
  186. return $fields;
  187. }
  188. /**
  189. * Ensure the username field is being sent and the username is not taken
  190. *
  191. * @return boolean
  192. */
  193. public function validateUsername() {
  194. $usernameField = $this->getProperty('usernameField','username');
  195. $username = $this->dictionary->get($usernameField);
  196. $success = true;
  197. /* ensure username field exists and isn't empty */
  198. if (empty($username) && !$this->validator->hasErrorsInField($usernameField)) {
  199. $this->validator->addError($usernameField,$this->modx->lexicon('register.field_required'));
  200. $success = false;
  201. } else {
  202. /* make sure username isnt taken */
  203. /** @var modUser $alreadyExists */
  204. $alreadyExists = $this->modx->getObject('modUser',array('username' => $username));
  205. if ($alreadyExists) {
  206. $cachePwd = $alreadyExists->get('cachepwd');
  207. if ($this->getProperty('removeExpiredRegistrations',true,'isset') && $alreadyExists->get('active') == 0 && !empty($cachePwd)) {
  208. /* if inactive and has a cachepwd, probably an expired
  209. * activation account, so let's remove it
  210. * and let user re-register
  211. */
  212. if (!$alreadyExists->remove()) {
  213. $this->modx->log(modX::LOG_LEVEL_ERROR,'[Login] Could not remove old, deactive user with cachepwd.');
  214. $success = false;
  215. }
  216. } else {
  217. $this->validator->addError($usernameField,$this->modx->lexicon('register.username_taken'));
  218. $success = false;
  219. }
  220. }
  221. }
  222. return $success;
  223. }
  224. public function getPassword() {
  225. $passwordField = $this->getProperty('passwordField','password');
  226. $password = $this->dictionary->get($passwordField);
  227. if ($this->getProperty('trimPassword',true,'isset')) {
  228. $password = trim($password);
  229. }
  230. return $password;
  231. }
  232. /**
  233. * Validate the password field and trim it if specified
  234. *
  235. * @return boolean
  236. */
  237. public function validatePassword() {
  238. $password = $this->getPassword();
  239. $passwordField = $this->getProperty('passwordField','password');
  240. $success = true;
  241. /* ensure password field isn't empty */
  242. if (empty($password) && !$this->validator->hasErrorsInField($passwordField)) {
  243. $this->validator->addError($passwordField,$this->modx->lexicon('register.field_required'));
  244. $success = false;
  245. }
  246. return $success;
  247. }
  248. /**
  249. * Automatically generate a password for the user
  250. * @return string
  251. */
  252. public function generatePassword() {
  253. $classKey = $this->dictionary->get('class_key');
  254. if (empty($classKey)) $classKey = 'modUser';
  255. /** @var modUser $user */
  256. $user = $this->modx->newObject($classKey);
  257. $password = $user->generatePassword();
  258. $this->dictionary->set($this->getProperty('passwordField','password'),$password);
  259. $this->dictionary->set('password_confirm',$password);
  260. return $password;
  261. }
  262. /**
  263. * Validate the email address, and ensure it is not empty or already taken
  264. * @return boolean
  265. */
  266. public function validateEmail() {
  267. $emailField = $this->getProperty('emailField','email');
  268. $email = $this->dictionary->get($emailField);
  269. $success = true;
  270. /* ensure email field isn't empty */
  271. if (empty($email) && !$this->validator->hasErrorsInField($emailField)) {
  272. $this->validator->addError($emailField,$this->modx->lexicon('register.field_required'));
  273. $success = false;
  274. /* ensure if allow_multiple_emails setting is false, prevent duplicate emails */
  275. } else if (!$this->modx->getOption('allow_multiple_emails',null,false)) {
  276. /** @var modUserProfile $emailTaken */
  277. $emailTaken = $this->modx->getObject('modUserProfile',array('email' => $email));
  278. if ($emailTaken) {
  279. $this->validator->addError($emailField,$this->modx->lexicon('register.email_taken',array('email' => $email)));
  280. $success = false;
  281. }
  282. }
  283. return $success;
  284. }
  285. /**
  286. * Check for a POST submission
  287. * @return void
  288. */
  289. public function checkForPost() {
  290. $this->hasPosted = !empty($_POST) && (empty($this->scriptProperties['submitVar']) || !empty($_POST[$this->scriptProperties['submitVar']]));
  291. }
  292. /**
  293. * Do any pre-processing before POST
  294. * @return void
  295. */
  296. public function preLoad() {
  297. $preHooks = $this->getProperty('preHooks','');
  298. /* if using recaptcha, load recaptcha html */
  299. if (strpos($preHooks,'recaptcha') !== false) {
  300. $this->loadReCaptcha();
  301. }
  302. /* if using math hook, load default placeholders */
  303. if (strpos($preHooks,'math') !== false && !$this->hasPosted) {
  304. $this->preLoadMath();
  305. }
  306. }
  307. /**
  308. * Pre-Load the data values for the math hook
  309. * @return void
  310. */
  311. public function preLoadMath() {
  312. $mathMaxRange = $this->getProperty('mathMaxRange',100);
  313. $mathMinRange = $this->getProperty('mathMinRange',10);
  314. $op1 = rand($mathMinRange,$mathMaxRange);
  315. $op2 = rand($mathMinRange,$mathMaxRange);
  316. $placeholderPrefix = rtrim($this->getProperty('placeholderPrefix', ''), '.');
  317. if ($op2 > $op1) { $t = $op2; $op2 = $op1; $op1 = $t; } /* swap so we always get positive #s */
  318. $operators = array('+','-');
  319. $operator = rand(0,1);
  320. $this->modx->toPlaceholders(array(
  321. $this->getProperty('mathOp1Field','op1') => $op1,
  322. $this->getProperty('mathOp2Field','op2') => $op2,
  323. $this->getProperty('mathOperatorField','operator') => $operators[$operator],
  324. ), $placeholderPrefix);
  325. }
  326. public function loadReCaptcha() {
  327. $placeholderPrefix = rtrim($this->getProperty('placeholderPrefix', ''), '.');
  328. /** @var reCaptcha $recaptcha */
  329. $recaptcha = $this->modx->getService('recaptcha','reCaptcha',$this->login->config['modelPath'].'recaptcha/');
  330. if ($recaptcha instanceof reCaptcha) {
  331. $this->modx->lexicon->load('login:recaptcha');
  332. $recaptchaTheme = $this->getProperty('recaptchaTheme','clean');
  333. $recaptchaWidth = $this->getProperty('recaptchaWidth',500);
  334. $recaptchaHeight = $this->getProperty('recaptchaHeight',300);
  335. $html = $recaptcha->getHtml($recaptchaTheme,$recaptchaWidth,$recaptchaHeight);
  336. $this->modx->toPlaceholder('recaptcha_html', $html, $placeholderPrefix);
  337. } else {
  338. $this->modx->log(modX::LOG_LEVEL_ERROR,'[Register] '.$this->modx->lexicon('register.recaptcha_err_load'));
  339. }
  340. }
  341. /**
  342. * Algorithm to ensure and suggest password strength
  343. * @return bool
  344. */
  345. public function ensurePasswordStrength() {
  346. $ensured = false;
  347. $password = $this->getPassword();
  348. $passwordField = $this->getProperty('passwordField','password');
  349. $passwordWordSeparator = $this->getProperty('passwordWordSeparator',' ','isset');
  350. if (strlen($passwordWordSeparator) == 0) $passwordWordSeparator = ' ';
  351. $wordCount = $this->getWordsInString($password,$passwordWordSeparator);
  352. $minimumStrongPasswordWordCount = $this->getProperty('minimumStrongPasswordWordCount',4,'!empty');
  353. if ($wordCount < $minimumStrongPasswordWordCount || $minimumStrongPasswordWordCount == 0) {
  354. $passwordStrengthThreshold = $this->getProperty('maximumPossibleStrongerPasswords',25,'!empty');
  355. if ($passwordStrengthThreshold > 0) {
  356. $possible = $this->getPossibleStrongerPasswords($password);
  357. if (count($possible) > $passwordStrengthThreshold) {
  358. $ensurePasswordStrengthSuggestions = $this->getProperty('ensurePasswordStrengthSuggestions',5,'!empty');
  359. $suggestionIndexes = array_rand($possible,$ensurePasswordStrengthSuggestions);
  360. $suggestions = array();
  361. foreach ($suggestionIndexes as $idx) {
  362. $suggestions[] = $possible[$idx];
  363. }
  364. $this->validator->addError($passwordField,$this->modx->lexicon('register.use_stronger_password',array(
  365. 'suggestions' => implode(', ',$suggestions),
  366. )));
  367. }
  368. } else {
  369. $ensured = true;
  370. }
  371. } else {
  372. $ensured = true;
  373. }
  374. return $ensured;
  375. }
  376. public function getWordsInString($str,$separator) {
  377. return count(explode($separator,$str));
  378. }
  379. /** @var array $strongPasswordMap */
  380. public $strongPasswordMap = array(
  381. 'a' => array('@','A','4'),
  382. 'b' => array('8','B'),
  383. 'c' => array('('),
  384. 'e' => array('3','E'),
  385. 'f' => array('ph'),
  386. 'g' => array('6'),
  387. 'i' => array('1','!','|'),
  388. 'l' => array('1','L'),
  389. 'n' => array('en'),
  390. 'o' => array('0','O'),
  391. 's' => array('$','5'),
  392. 't' => array('7','+'),
  393. 'x' => array('X'),
  394. 'z' => array('2'),
  395. );
  396. /**
  397. * Given a password, find stronger ones
  398. *
  399. * @param string $password
  400. * @return array
  401. */
  402. public function getPossibleStrongerPasswords($password) {
  403. $passwordLength = strlen($password);
  404. if ($passwordLength == 1) return isset($this->strongPasswordMap[$password]) ? $this->strongPasswordMap[$password] : $password;
  405. $rest = $this->getPossibleStrongerPasswords(substr($password,1));
  406. $restLength = count($rest);
  407. $current = isset($this->strongPasswordMap[$password[0]]) ? $this->strongPasswordMap[$password[0]] : null;
  408. $currentLength = count($current);
  409. $result = array();
  410. if ($current) {
  411. for ($i=0;$i<$currentLength;$i++) {
  412. for ($j=0;$j<$restLength;$j++) {
  413. $result[] = $current[$i].$rest[$j];
  414. }
  415. }
  416. } else {
  417. for ($j=0;$j<$restLength;$j++) {
  418. $result[] = $password[0].$rest[$j];
  419. }
  420. }
  421. return $result;
  422. }
  423. }
  424. return 'LoginRegisterController';