CAT-SOOP is a flexible, programmable learning management system based on the Python programming language. https://catsoop.mit.edu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1062 lines
44 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2017 Adam Hartz <hartz@mit.edu>
  3. #
  4. # This program is free software: you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License as published by the Free
  6. # Software Foundation, either version 3 of the License, or (at your option) any
  7. # later version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import os
  17. import re
  18. import random
  19. import string
  20. import hashlib
  21. from operator import xor
  22. from struct import Struct
  23. from itertools import starmap
  24. def get_logged_in_user(context):
  25. # form-based login
  26. base_context = context['csm_base_context']
  27. logging = context['csm_logging'].get_logger(context)
  28. loader = context['csm_loader']
  29. form = context.get('cs_form', {})
  30. mail = context['csm_mail']
  31. session = context['cs_session_data']
  32. action = form.get('loginaction', '')
  33. message = form.get('message', '')
  34. hash_iterations = context.get('cs_password_hash_iterations', 250000)
  35. url = _get_base_url(context)
  36. # if the user is trying to log out, do that.
  37. if action == 'logout':
  38. context['cs_session_data'] = {}
  39. return {'cs_reload': True}
  40. # if a user is changing their password
  41. elif action == 'change_password':
  42. uname = session.get('username', None)
  43. if uname is None:
  44. # cannot change password without first being logged in
  45. base = _get_base_url(context)
  46. context['cs_content_header'] = "Please Log In"
  47. context['cs_content'] = ("You cannot change your password "
  48. "until you have logged in.<br/>"
  49. '<a href="%s">Go Back</a>') % base
  50. context['cs_handler'] = 'passthrough'
  51. return {'cs_render_now': True}
  52. login_info = logging.most_recent(None, 'logininfo', uname, {})
  53. if not login_info.get('confirmed', False):
  54. # show the confirmation message again
  55. context['cs_content_header'] = "Your E-mail Has Not Been Confirmed"
  56. context['cs_content'] = ("Your registration is not yet "
  57. "complete. Please check your"
  58. " e-mail for instructions on "
  59. "how to complete the process. "
  60. "If you did not receive a "
  61. "confirmation e-mail, please "
  62. "<a href='%s?loginaction=reconfirm_reg"
  63. "&username=%s'>click here</a> to "
  64. "re-send the email.") % (url, uname)
  65. context['cs_handler'] = 'passthrough'
  66. return {'cs_render_now': True}
  67. if 'cs_hashed_2' in form:
  68. # the user has submitted the form. check it.
  69. errors = []
  70. if not check_password(context, form['oldpasswd'], uname, hash_iterations):
  71. errors.append('Incorrect password entered.')
  72. passwd = form['cs_hashed_0']
  73. passwd2 = form['cs_hashed_1']
  74. if passwd != passwd2:
  75. errors.append('New passwords do not match.')
  76. if len(errors) > 0:
  77. # at least one error happened; display error message and show
  78. # the form again
  79. errs = '\n'.join('<li>%s</li>' % i for i in errors)
  80. lmsg = ('<font color="red">Your password was not changed:\n'
  81. '<ul>%s</ul></font>') % errs
  82. session['login_message'] = lmsg
  83. else:
  84. # clear login info from session.
  85. clear_session_vars(context, 'login_message')
  86. # store new password.
  87. salt = get_new_password_salt()
  88. phash = compute_password_hash(passwd, salt, hash_iterations)
  89. login_info['password_salt'] = salt
  90. login_info['password_hash'] = phash
  91. logging.update_log(None, 'logininfo', uname, login_info)
  92. context['cs_content_header'] = "Password Changed!"
  93. base = _get_base_url(context)
  94. context['cs_content'] = ("Your password has been successfully"
  95. " changed.<br/>"
  96. '<a href="%s">Continue</a>') % base
  97. context['cs_handler'] = 'passthrough'
  98. return {'cs_render_now': True}
  99. # show the form.
  100. context['cs_content_header'] = 'Change Password'
  101. context['cs_content'] = generate_password_change_form(context)
  102. context['cs_handler'] = 'passthrough'
  103. return {'cs_render_now': True}
  104. # if a user is confirming their account
  105. elif action == 'confirm_reg':
  106. u = form.get('username', None)
  107. t = form.get('token', None)
  108. stored_token = logging.most_recent(None, 'confirmation_token', u, '')
  109. login_info = logging.most_recent(None, 'logininfo', u, {})
  110. context['cs_handler'] = 'passthrough'
  111. retval = {'cs_render_now': True}
  112. url = _get_base_url(context)
  113. if login_info.get('confirmed', False):
  114. context['cs_content_header'] = "Already Confirmed"
  115. context['cs_content'] = ("This account has already been confirmed."
  116. " Please <a href='%s'>click here</a> to "
  117. "log in.") % url
  118. elif t == stored_token and 'confirmed' in login_info:
  119. login_info['confirmed'] = True
  120. logging.update_log(None, 'logininfo', u, login_info)
  121. context['cs_content_header'] = "Account Confirmation Succeeded"
  122. context['cs_content'] = ('Please <a href="%s">click here</a>'
  123. ' to log in.') % url
  124. clear_session_vars(context, 'login_message', 'last_form')
  125. retval.update(login_info)
  126. session.update(login_info)
  127. session['username'] = u
  128. session['course'] = context.get('cs_course', None)
  129. else:
  130. cs_debug(t, stored_token, login_info)
  131. context['cs_content_header'] = "Account Confirmation Failed"
  132. context['cs_content'] = ("Please double-check the details "
  133. "from the confirmation e-mail you"
  134. " received.")
  135. return retval
  136. # if the session tells us someone is logged in, return their
  137. # information
  138. elif 'username' in session and session.get('course', None) == context['cs_course']:
  139. uname = session['username']
  140. clear_session_vars(context, 'login_message', 'last_form')
  141. return {'username': uname,
  142. 'name': session.get('name', uname),
  143. 'email': session.get('email', uname)}
  144. # if a user has forgotten their password
  145. elif action == 'forgot_password':
  146. if not mail.can_send_email(context):
  147. # can't send e-mail; show error message
  148. context['cs_content_header'] = "Password Reset: Error"
  149. context['cs_content'] = ("This feature is not available "
  150. "on this CAT-SOOP instance.")
  151. context['cs_handler'] = 'passthrough'
  152. return {'cs_render_now': True}
  153. if 'uname' in form:
  154. # user has submitted the form; check and send request
  155. uname = form['uname']
  156. email = form.get('email', None)
  157. login_info = logging.most_recent(None, 'logininfo', uname, {})
  158. if email != login_info.get('email', ''):
  159. lmsg = ('<font color="red">The information you provided '
  160. 'does not match any known accounts.</font>')
  161. session['login_message'] = lmsg
  162. session['last_form'] = form
  163. else:
  164. # clear login info from session
  165. clear_session_vars(context, 'login_message', 'last_form')
  166. # generate and store token
  167. token = generate_confirmation_token()
  168. logging.update_log(None, uname, 'password_reset_token', token)
  169. # generate and send e-mail
  170. mail.send_email(context, email,
  171. "CAT-SOOP: Confirm Password Reset",
  172. *passwd_confirm_emails(context, uname, token))
  173. # show confirmation message
  174. context['cs_content_header'] = "Password Reset: Confirm"
  175. context['cs_content'] = ("Please check your e-mail for "
  176. "instructions on how to complete "
  177. "the process.")
  178. context['cs_handler'] = 'passthrough'
  179. return {'cs_render_now': True}
  180. # show the form.
  181. context['cs_content_header'] = 'Forgot Password'
  182. context['cs_content'] = ("Please enter your information below to "
  183. "reset your password.")
  184. context['cs_content'] += generate_forgot_password_form(context)
  185. context['cs_handler'] = 'passthrough'
  186. return {'cs_render_now': True}
  187. # if a user is requesting a password reset
  188. elif action == 'reset_password':
  189. if not mail.can_send_email(context):
  190. # can't send e-mail; show error message
  191. context['cs_content_header'] = "Password Reset: Error"
  192. context['cs_content'] = ("This feature is not available "
  193. "on this CAT-SOOP instance.")
  194. context['cs_handler'] = 'passthrough'
  195. return {'cs_render_now': True}
  196. if 'cs_hashed_0' in form:
  197. # user has submitted the form; check and update password
  198. errors = []
  199. u = form.get('username', None)
  200. t = form.get('token', None)
  201. stored_token = logging.most_recent(None, u, 'password_reset_token',
  202. '')
  203. if stored_token != t:
  204. errors.append('Unknown user, or incorrect confirmation token.')
  205. passwd = form['cs_hashed_0']
  206. passwd2 = form['cs_hashed_1']
  207. if passwd != passwd2:
  208. errors.append('New passwords do not match.')
  209. if len(errors) > 0:
  210. # at least one error happened; display error message and show
  211. # the form again
  212. errs = '\n'.join('<li>%s</li>' % i for i in errors)
  213. lmsg = ('<font color="red">Your password was not reset:\n'
  214. '<ul>%s</ul></font>') % errs
  215. session['login_message'] = lmsg
  216. else:
  217. # success!
  218. # clear login info from session.
  219. clear_session_vars(context, 'login_message')
  220. # store new password.
  221. login_info = logging.most_recent(None, 'logininfo', u, {})
  222. salt = get_new_password_salt()
  223. phash = compute_password_hash(passwd, salt, hash_iterations)
  224. login_info['password_salt'] = salt
  225. login_info['password_hash'] = phash
  226. logging.update_log(None, 'logininfo', u, login_info)
  227. context['cs_content_header'] = "Password Changed!"
  228. base = _get_base_url(context)
  229. context['cs_content'] = ("Your password has been successfully"
  230. " changed.<br/>"
  231. '<a href="%s">Continue</a>') % base
  232. context['cs_handler'] = 'passthrough'
  233. email = login_info.get('email', u)
  234. name = login_info.get('name', u)
  235. info = {'username': u, 'name': name, 'email': email}
  236. session.update(info)
  237. session['course'] = context.get('cs_course', None)
  238. return {'cs_render_now': True}
  239. # show the form.
  240. context['cs_content_header'] = 'Reset Password'
  241. context['cs_content'] = generate_password_reset_form(context)
  242. context['cs_handler'] = 'passthrough'
  243. return {'cs_render_now': True}
  244. # if the form has login information, we should try to authenticate
  245. elif action == 'login':
  246. uname = form.get('login_uname', '')
  247. if uname == '':
  248. clear_session_vars(context, 'login_message')
  249. entered_password = form.get('cs_hashed_0', '')
  250. valid_uname = True
  251. if _validate_email(context, uname) is None:
  252. # this looks like an e-mail address, not a username.
  253. # find the associated username, if any
  254. # TODO: implement caching of some kind so this isn't so slow/involved
  255. data_root = context.get('cs_data_root', base_context.cs_data_root)
  256. global_log_dir = os.path.join(data_root, '__LOGS__')
  257. for d in os.listdir(global_log_dir):
  258. if not d.endswith('.db'):
  259. continue
  260. u = d[:-3]
  261. e = logging.most_recent(None, 'logininfo', u, {})
  262. e = e.get('email', None)
  263. if e == uname:
  264. uname = u
  265. break
  266. vmsg = _validate_username(context, uname)
  267. if vmsg is not None:
  268. valid_uname = False
  269. lmsg = ('<font color="red">' + vmsg + '</font>')
  270. session.update({'login_message': lmsg, 'last_form': form})
  271. valid_pwd = check_password(context, entered_password, uname, hash_iterations)
  272. if valid_uname and valid_pwd:
  273. # successful login
  274. login_info = logging.most_recent(None, 'logininfo', uname, {})
  275. if not login_info.get('confirmed', False):
  276. # show the confirmation message again
  277. context[
  278. 'cs_content_header'] = "Your E-mail Has Not Been Confirmed"
  279. context['cs_content'] = ("Your registration is not yet "
  280. "complete. Please check your"
  281. " e-mail for instructions on "
  282. "how to complete the process. "
  283. "If you did not receive a "
  284. "confirmation e-mail, please "
  285. "<a href='%s?loginaction=reconfirm_reg"
  286. "&username=%s'>click here</a> to "
  287. "re-send the email.") % (url, uname)
  288. context['cs_handler'] = 'passthrough'
  289. return {'cs_render_now': True}
  290. email = login_info.get('email', uname)
  291. name = login_info.get('name', uname)
  292. info = {'username': uname, 'name': name, 'email': email}
  293. session.update(info)
  294. session['course'] = context.get('cs_course', None)
  295. clear_session_vars(context, 'login_message')
  296. info['cs_reload'] = True
  297. return info
  298. elif valid_uname:
  299. lmsg = ('<font color="red">'
  300. 'Incorrect username or password.'
  301. '</font>')
  302. session.update({'login_message': lmsg, 'last_form': form})
  303. # a user is asking to re-send the confirmation message
  304. elif action == 'reconfirm_reg':
  305. uname = form.get('username', None)
  306. token = logging.most_recent(None, 'confirmation_token', uname, None)
  307. login_info = logging.most_recent(None, 'logininfo', uname)
  308. if login_info.get('confirmed', False):
  309. context['cs_content_header'] = "Already Confirmed"
  310. context['cs_content'] = ("This account has already been confirmed."
  311. " Please <a href='%s'>click here</a> to "
  312. "log in.") % url
  313. elif token is None:
  314. context['cs_content_header'] = "Error"
  315. context['cs_content'] = ("The provided information is "
  316. "complete. Please check your"
  317. " e-mail for instructions on "
  318. "how to complete the process.")
  319. else:
  320. # generate and send e-mail
  321. mail.send_email(context, login_info['email'],
  322. "CAT-SOOP: Confirm E-mail Address",
  323. *reg_confirm_emails(context, uname, token))
  324. context['cs_content_header'] = "Confirmation E-mail Sent"
  325. context['cs_content'] = ("Your registration is almost "
  326. "complete. Please check your"
  327. " e-mail for instructions on "
  328. "how to complete the process. "
  329. "If you do not receive a "
  330. "confirmation e-mail within 5 minutes, please "
  331. "<a href='%s?loginaction=reconfirm_reg"
  332. "&username=%s'>click here</a> to "
  333. "re-send the email.") % (url, uname)
  334. context['cs_handler'] = 'passthrough'
  335. return {'cs_render_now': True}
  336. # if we are looking at a registration action
  337. elif action == 'register':
  338. if not context.get('cs_allow_registration', True):
  339. return {'cs_render_now': True}
  340. uname = form.get('uname', '').strip()
  341. if uname != '':
  342. # form has been filled out. validate (mirrors javascript checks).
  343. email = form.get('email', '').strip()
  344. email2 = form.get('email2', '').strip()
  345. passwd = form.get('cs_hashed_0', '')
  346. passwd2 = form.get('cs_hashed_1', '')
  347. name = form.get('name', '').strip()
  348. if name == '':
  349. name = uname
  350. errors = []
  351. # validate e-mail
  352. if len(email) == 0:
  353. errors.append("No e-mail address entered.")
  354. elif email != email2:
  355. errors.append("E-mail addresses do not match.")
  356. else:
  357. e_check_result = _validate_email(context, email)
  358. if e_check_result is not None:
  359. errors.append(e_check_result)
  360. # validate username
  361. uname_okay = True
  362. if len(uname) == 0:
  363. errors.append("No username entered.")
  364. uname_okay = False
  365. else:
  366. u_check_result = _validate_username(context, uname)
  367. if u_check_result is not None:
  368. errors.append(u_check_result)
  369. uname_okay = False
  370. if uname_okay:
  371. login_info = logging.most_recent(None,
  372. 'logininfo',
  373. uname,
  374. default=None)
  375. if uname.lower() == 'none' or login_info is not None:
  376. errors.append('Username %s is not available.' % uname)
  377. # validate password
  378. if passwd != passwd2:
  379. errors.append('Passwords do not match.')
  380. if len(errors) > 0:
  381. # at least one error happened; display error message and show
  382. # registration form again
  383. errs = '\n'.join('<li>%s</li>' % i for i in errors)
  384. lmsg = ('<font color="red">Your account was not created:\n'
  385. '<ul>%s</ul></font>') % errs
  386. session['login_message'] = lmsg
  387. session['last_form'] = form
  388. else:
  389. # clear login info from session
  390. clear_session_vars(context, 'login_message', 'last_form')
  391. # generate new salt and password hash
  392. salt = get_new_password_salt()
  393. phash = compute_password_hash(passwd, salt, hash_iterations)
  394. # if necessary, send confirmation e-mail
  395. # otherwise, treat like already confirmed
  396. if (mail.can_send_email(context) and
  397. context.get('cs_require_confirm_email', True)):
  398. # generate and store token
  399. token = generate_confirmation_token()
  400. logging.overwrite_log(None,
  401. 'confirmation_token',
  402. uname,
  403. token)
  404. # generate and send e-mail
  405. mail.send_email(context, email,
  406. "CAT-SOOP: Confirm E-mail Address",
  407. *reg_confirm_emails(context, uname, token))
  408. confirmed = False
  409. else:
  410. confirmed = True
  411. # store login information
  412. uinfo = {'password_salt': salt,
  413. 'password_hash': phash,
  414. 'email': email,
  415. 'name': name,
  416. 'confirmed': confirmed}
  417. logging.overwrite_log(None, 'logininfo', uname, uinfo)
  418. if confirmed:
  419. # load user info into session
  420. info = {'username': uname, 'name': name, 'email': email}
  421. session.update(info)
  422. session['course'] = context.get('cs_course', None)
  423. # redirect to current location, with no "logininfo" in URL
  424. info['cs_reload'] = True
  425. return info
  426. else:
  427. # show a "please confirm" message
  428. context['cs_content_header'] = "Thank You!"
  429. context['cs_content'] = ("Your registration is almost "
  430. "complete. Please check your"
  431. " e-mail for instructions on "
  432. "how to complete the process. "
  433. "If you do not receive a "
  434. "confirmation e-mail within 5 minutes, please "
  435. "<a href='%s?loginaction=reconfirm_reg"
  436. "&username=%s'>click here</a> to "
  437. "re-send the email.") % (url, uname)
  438. context['cs_handler'] = 'passthrough'
  439. return {'cs_render_now': True}
  440. # if we haven't returned something else by now, show the
  441. # registration form
  442. context['cs_content_header'] = 'Register'
  443. context['cs_content'] = generate_registration_form(context)
  444. context['cs_handler'] = 'passthrough'
  445. return {'cs_render_now': True}
  446. if action != 'login':
  447. clear_session_vars(context, 'login_message')
  448. # no one is logged in; show the login form.
  449. if context.get('cs_view_without_auth', True) and action != 'login':
  450. old_postload = context.get('cs_post_load', None)
  451. def new_postload(context):
  452. if old_postload is not None:
  453. old_postload(context)
  454. context['cs_content'] = ((LOGIN_BOX % _get_base_url(context)) +
  455. context['cs_content'])
  456. context['cs_post_load'] = new_postload
  457. return {}
  458. else:
  459. uname = form.get('login_uname', '')
  460. if uname == '':
  461. clear_session_vars(context, 'login_message')
  462. context['cs_content_header'] = 'Please Log In To Continue'
  463. context['cs_content'] = generate_login_form(context)
  464. context['cs_handler'] = 'passthrough'
  465. return {'cs_render_now': True}
  466. def clear_session_vars(context, *args):
  467. """
  468. Helper function to clear session variables
  469. """
  470. session = context['cs_session_data']
  471. for i in args:
  472. try:
  473. del session[i]
  474. except:
  475. pass
  476. def check_password(context, provided, uname, iterations=250000):
  477. """
  478. Compare the provided password against a stored hash.
  479. """
  480. logging = context['csm_logging'].get_logger(context)
  481. user_login_info = logging.most_recent(None, 'logininfo', uname, {})
  482. pass_hash = user_login_info.get('password_hash', None)
  483. if pass_hash is not None:
  484. salt = user_login_info.get('password_salt', None)
  485. hashed_pass = compute_password_hash(provided, salt, iterations)
  486. if hashed_pass == pass_hash:
  487. return True
  488. return False
  489. def get_new_password_salt(length=128):
  490. """
  491. Generate a new salt of length length. Tries to use os.urandom, and
  492. falls back on random if that doesn't work for some reason.
  493. """
  494. try:
  495. return os.urandom(length)
  496. except:
  497. return ''.join(chr(random.randint(0, 255)) for i in range(length))
  498. def _ensure_bytes(x):
  499. try:
  500. return x.encode()
  501. except:
  502. return x
  503. def compute_password_hash(password, salt=None, iterations=250000):
  504. """
  505. Given a password, and (optionally) an associated salt, return a hash value.
  506. """
  507. return hashlib.pbkdf2_hmac('sha512', _ensure_bytes(password),
  508. _ensure_bytes(salt), iterations)
  509. def generate_confirmation_token(n=20):
  510. chars = string.ascii_uppercase + string.digits
  511. return ''.join(random.choice(chars) for i in range(n))
  512. def _get_base_url(context):
  513. return '/'.join([context['cs_url_root']] + context['cs_path_info'])
  514. def generate_forgot_password_form(context):
  515. """
  516. Generate a "forgot password" form.
  517. """
  518. base = _get_base_url(context)
  519. req_url = base + '?loginaction=forgot_password'
  520. out = '<form method="POST" action="%s">' % req_url
  521. msg = context['cs_session_data'].get('login_message', None)
  522. if msg is not None:
  523. out += '\n%s<p>' % msg
  524. last = context['cs_session_data'].get('last_form', {})
  525. last_uname = last.get('uname', '').replace('"', '&quot;')
  526. last_email = last.get('email', '').replace('"', '&quot;')
  527. out += ('\n<table>'
  528. '\n<tr>'
  529. '\n<td style="text-align:right;">Username:</td>'
  530. '\n<td style="text-align:right;">'
  531. '\n<input type="text" '
  532. 'name="uname" '
  533. 'id="uname" '
  534. 'value="%s"/>'
  535. '\n</td>'
  536. '\n</tr>') % last_uname
  537. out += ('\n<tr>'
  538. '\n<td style="text-align:right;">Email Address:</td>'
  539. '\n<td style="text-align:right;">'
  540. '\n<input type="text" '
  541. 'name="email" '
  542. 'id="email" '
  543. 'value="%s" />'
  544. '\n</td>'
  545. '\n</tr>') % last_email
  546. out += ('\n<tr>'
  547. '\n<td style="text-align:right;"></td>'
  548. '\n<td style="text-align:right;">'
  549. '\n<input type="submit" value="Reset Password"></td>'
  550. '\n</tr>')
  551. out += '\n</table>\n</form>'
  552. return out
  553. def generate_password_reset_form(context):
  554. """
  555. Generate a "reset password" form.
  556. """
  557. base = _get_base_url(context)
  558. req_url = base + '?loginaction=reset_password'
  559. req_url += '&username=%s' % context['cs_form']['username']
  560. req_url += '&token=%s' % context['cs_form']['token']
  561. out = '<form method="POST" action="%s" id="pwdform">' % req_url
  562. msg = context['cs_session_data'].get('login_message', None)
  563. if msg is not None:
  564. out += '\n%s<p>' % msg
  565. safe_uname = context['cs_form']['username'].replace('"', '&quot;')
  566. out += '\n<input type="hidden" name="uname" id="uname" value="%s" />' % safe_uname
  567. out += '\n<table>'
  568. out += ('\n<tr>'
  569. '\n<td style="text-align:right;">New Password:</td>'
  570. '\n<td style="text-align:right;">'
  571. '\n<input type="password" name="passwd" id="passwd" />'
  572. '\n</td>'
  573. '\n</tr>')
  574. out += ('\n<tr>'
  575. '\n<td style="text-align:right;">Confirm New Password:</td>'
  576. '\n<td style="text-align:right;">'
  577. '\n<input type="password" name="passwd2" id="passwd2" />'
  578. '\n</td>'
  579. '\n<td><span id="pwd_check"></span></td>'
  580. '\n</tr>')
  581. out += ('\n<tr>'
  582. '\n<td style="text-align:right;"></td>'
  583. '\n<td style="text-align:right;">')
  584. out += _submit_button(['passwd', 'passwd2'],
  585. 'uname',
  586. [],
  587. 'pwdform', 'Change Password')
  588. out += '</td>\n</tr>'
  589. out += '\n</table>\n</form>'
  590. out += CHANGE_PASSWORD_FORM_CHECKER
  591. return out
  592. def generate_password_change_form(context):
  593. """
  594. Generate a "change password" form.
  595. """
  596. base = _get_base_url(context)
  597. req_url = base + '?loginaction=change_password'
  598. out = '<form method="POST" action="%s" id="pwdform">' % req_url
  599. msg = context['cs_session_data'].get('login_message', None)
  600. if msg is not None:
  601. out += '\n%s<p>' % msg
  602. safe_uname = context['cs_session_data']['username'].replace('"', '&quot;')
  603. out += '\n<input type="hidden" name="uname" id="uname" value="%s" />' % safe_uname
  604. out += '\n<table>'
  605. out += ('\n<tr>'
  606. '\n<td style="text-align:right;">Current Password:</td>'
  607. '\n<td style="text-align:right;">'
  608. '\n<input type="password" name="oldpasswd" id="oldpasswd" />'
  609. '\n</td>'
  610. '\n</tr>')
  611. out += ('\n<tr>'
  612. '\n<td style="text-align:right;">New Password:</td>'
  613. '\n<td style="text-align:right;">'
  614. '\n<input type="password" name="passwd" id="passwd" />'
  615. '\n</td>'
  616. '\n</tr>')
  617. out += ('\n<tr>'
  618. '\n<td style="text-align:right;">Confirm New Password:</td>'
  619. '\n<td style="text-align:right;">'
  620. '\n<input type="password" name="passwd2" id="passwd2" />'
  621. '\n</td>'
  622. '\n<td><span id="pwd_check"></span></td>'
  623. '\n</tr>')
  624. out += ('\n<tr>'
  625. '\n<td style="text-align:right;"></td>'
  626. '\n<td style="text-align:right;">')
  627. out += _submit_button(['passwd', 'passwd2', 'oldpasswd'],
  628. 'uname',
  629. [],
  630. 'pwdform', 'Change Password')
  631. out += '</td>\n</tr>'
  632. out += '\n</table>\n</form>'
  633. out += CHANGE_PASSWORD_FORM_CHECKER
  634. return out
  635. def generate_login_form(context):
  636. """
  637. Generate a login form.
  638. """
  639. base = _get_base_url(context)
  640. out = '<form method="POST" id="loginform" action="%s">' % (base + '?loginaction=login')
  641. msg = context['cs_session_data'].get('login_message', None)
  642. last_uname = context['cs_session_data'].get('last_form', {}).get(
  643. 'login_uname', '')
  644. if msg is not None:
  645. out += '\n%s<p>' % msg
  646. last_uname = last_uname.replace('"', '&quot;')
  647. out += ('\n<table>'
  648. '\n<tr>'
  649. '\n<td style="text-align:right;">Username:</td>'
  650. '\n<td style="text-align:right;">'
  651. '\n<input type="text" '
  652. 'name="login_uname" '
  653. 'id="login_uname" '
  654. 'value="%s"/>'
  655. '\n</td>'
  656. '\n</tr>'
  657. '\n<tr>'
  658. '\n<td style="text-align:right;">Password:</td>'
  659. '\n<td style="text-align:right;">'
  660. '\n<input type="password" '
  661. 'name="login_passwd" '
  662. 'id="login_passwd" />'
  663. '\n</td>'
  664. '\n</tr>'
  665. '\n<tr>'
  666. '\n<td style="text-align:right;"></td>'
  667. '\n<td style="text-align:right;">') % last_uname
  668. out += _submit_button(['login_passwd'],
  669. 'login_uname',
  670. ['login_uname'],
  671. 'loginform', 'Log In')
  672. out += ('<td>\n</tr>'
  673. '\n</table>')
  674. out += '<p>'
  675. if mail.can_send_email(context):
  676. base = _get_base_url(context)
  677. loc = base + '?loginaction=forgot_password'
  678. out += ('\nForgot your password? '
  679. 'Click <a href="%s">here</a>.<br/>') % loc
  680. if context.get('cs_allow_registration', True):
  681. loc = _get_base_url(context)
  682. loc += '?loginaction=register'
  683. link = '<a href="%s">create one</a>' % loc
  684. out += '\nIf you do not already have an account, please %s.' % link
  685. out += '</p>'
  686. return out + '</form>'
  687. def generate_registration_form(context):
  688. """
  689. Generate a registration form.
  690. """
  691. base = _get_base_url(context)
  692. qstring = '?loginaction=register'
  693. out = '<form method="POST" action="%s" id="regform">' % (base + qstring)
  694. last = context['cs_session_data'].get('last_form', {})
  695. msg = context['cs_session_data'].get('login_message', None)
  696. if msg is not None:
  697. out += '\n%s<p>' % msg
  698. last_name = last.get('name', '').replace('"', '&quot;')
  699. last_uname = last.get('uname', '').replace('"', '&quot;')
  700. last_email = last.get('email', '').replace('"', '&quot;')
  701. last_email2 = last.get('email2', '').replace('"', '&quot;')
  702. out += ('\n<table>'
  703. '\n<tr>'
  704. '\n<td style="text-align:right;">Username:</td>'
  705. '\n<td style="text-align:right;">'
  706. '\n<input type="text" '
  707. 'name="uname" '
  708. 'id="uname" '
  709. 'value="%s"/>'
  710. '\n</td>'
  711. '\n<td><span id="uname_check"></span></td>'
  712. '\n</tr>') % last_uname
  713. out += ('\n<tr>'
  714. '\n<td style="text-align:right;">Email Address:</td>'
  715. '\n<td style="text-align:right;">'
  716. '\n<input type="text" '
  717. 'name="email" '
  718. 'id="email" '
  719. 'value="%s" />'
  720. '\n</td>'
  721. '\n</tr>') % last_email
  722. out += ('\n<tr>'
  723. '\n<td style="text-align:right;">Confirm Email Address:</td>'
  724. '\n<td style="text-align:right;">'
  725. '\n<input type="text" '
  726. 'name="email2" '
  727. 'id="email2" '
  728. 'value="%s"/>'
  729. '\n</td>'
  730. '\n<td><span id="email_check"></span></td>'
  731. '\n</tr>') % last_email2
  732. out += ('\n<tr>'
  733. '\n<td style="text-align:right;">Password:</td>'
  734. '\n<td style="text-align:right;">'
  735. '\n<input type="password" name="passwd" id="passwd" />'
  736. '\n</td>'
  737. '\n</tr>')
  738. out += ('\n<tr>'
  739. '\n<td style="text-align:right;">Confirm Password:</td>'
  740. '\n<td style="text-align:right;">'
  741. '\n<input type="password" name="passwd2" id="passwd2" />'
  742. '\n</td>'
  743. '\n<td><span id="pwd_check"></span></td>'
  744. '\n</tr>')
  745. out += ('\n<tr>'
  746. '\n<td style="text-align:right;">Name (Optional):</td>'
  747. '\n<td style="text-align:right;">'
  748. '\n<input type="text" name="name" id="name" value="%s"/>'
  749. '\n</td>'
  750. '\n</tr>') % last_name
  751. out += ('\n<tr>'
  752. '\n<td style="text-align:right;"></td>'
  753. '\n<td style="text-align:right;">')
  754. out += _submit_button(['passwd', 'passwd2'],
  755. 'uname',
  756. ['uname', 'email', 'email2', 'name'],
  757. 'regform', 'Register')
  758. out += ('\n</td>'
  759. '\n</tr>')
  760. out += REGISTRATION_FORM_CHECKER
  761. return out + '</table></form>'
  762. def _run_validators(validators, x):
  763. for extra_validator_regexp, error_msg in validators:
  764. if not re.match(extra_validator_regexp, x):
  765. return error_msg
  766. return None
  767. # PASSWORD VALIDATION
  768. _pwd_too_short_msg = "Passwords must be at least 8 characters long."
  769. _validate_password_javascript = r"""
  770. function _validate_password(p){
  771. if (p.length < 8){
  772. return %r;
  773. }
  774. return null;
  775. }
  776. """ % (_pwd_too_short_msg)
  777. # EMAIL VALIDATION
  778. # email validation regex from http://www.regular-expressions.info/email.html
  779. _re_valid_email_string = r"^[A-Za-z0-9._%+-]{1,64}@(?:[A-Za-z0-9-]{1,63}\.){1,125}[A-Za-z]{2,63}$"
  780. RE_VALID_EMAIL = re.compile(_re_valid_email_string)
  781. _eml_too_long_msg = "E-mail addresses must be less than 255 characters long."
  782. _eml_invalid_msg = "Please make sure you have entered a valid e-mail address."
  783. _validate_email_javascript = r"""
  784. var _re_valid_email = /%s/;
  785. function _validate_email(e){
  786. if (e.length > 254) {
  787. return %r;
  788. } else if (!_re_valid_email.test(e)){
  789. return %r;
  790. }
  791. return null;
  792. }
  793. """ % (_re_valid_email_string, _eml_too_long_msg, _eml_invalid_msg)
  794. def _validate_email(context, e):
  795. if len(e) > 254:
  796. return _eml_too_long_msg
  797. elif not RE_VALID_EMAIL.match(e):
  798. return _eml_invalid_msg
  799. return _run_validators(context.get('cs_extra_email_validators', []), e)
  800. # USERNAME VALIDATION
  801. _re_valid_username_string = r"^[A-Za-z0-9][A-Za-z0-9_.-]*$"
  802. RE_VALID_USERNAME = re.compile(_re_valid_username_string)
  803. _uname_too_short_msg = "Usernames must contain at least one character."
  804. _uname_wrong_start_msg = "Usernames must begin with an ASCII letter or number."
  805. _uname_invalid_msg = ("Usernames must contain only letters and numbers, "
  806. "dashes (-), underscores (_), and periods (.).")
  807. _validate_username_javascript = r"""
  808. var _re_valid_uname = /%s/;
  809. function _validate_username(u){
  810. if (u.length < 1){
  811. return %r;
  812. } else if (!_re_valid_uname.test(u)){
  813. if (!_re_valid_uname.test(u.charAt(0))){
  814. return %r;
  815. }else{
  816. return %r;
  817. }
  818. }
  819. return null;
  820. }
  821. """ % (_re_valid_username_string, _uname_too_short_msg, _uname_wrong_start_msg,
  822. _uname_invalid_msg)
  823. def _validate_username(context, u):
  824. if len(u) < 1:
  825. return _uname_too_short_msg
  826. elif not RE_VALID_USERNAME.match(u):
  827. if not RE_VALID_USERNAME.match(u[0]):
  828. return _uname_wrong_start_msg
  829. else:
  830. return _uname_invalid_msg
  831. return _run_validators(context.get('cs_extra_username_validators', []), u)
  832. REGISTRATION_FORM_CHECKER = """<script type="text/javascript">
  833. %s
  834. %s
  835. %s
  836. function check_form(){
  837. var e_msg = "";
  838. var u_msg = "";
  839. var p_msg = "";
  840. // validate email
  841. var e_val = $("#email").val();
  842. if(e_val.length == 0){
  843. e_msg = "Please enter an email address.";
  844. }else if(e_val != $("#email2").val()){
  845. e_msg = "E-mail addresses do not match.";
  846. }else{
  847. var e_check_result = _validate_email(e_val);
  848. if (e_check_result !== null){
  849. e_msg = e_check_result;
  850. }
  851. }
  852. $("#email_check").html('<font color="red">' + e_msg + '</font>');
  853. // validate username
  854. var u_val = $("#uname").val();
  855. if(u_val.length == 0){
  856. u_msg = "Please enter a username.";
  857. }else{
  858. var u_check_result = _validate_username(u_val);
  859. if (u_check_result !== null){
  860. u_msg = u_check_result;
  861. }
  862. }
  863. $("#uname_check").html('<font color="red">' + u_msg + '</font>');
  864. // validate password
  865. var p_val = $("#passwd").val();
  866. if(p_val != $("#passwd2").val()){
  867. p_msg = "Passwords do not match.";
  868. }else{
  869. var p_check_result = _validate_password(p_val);
  870. if (p_check_result !== null){
  871. p_msg = p_check_result;
  872. }
  873. }
  874. $("#pwd_check").html('<font color="red">' + p_msg + '</font>');
  875. }
  876. $(document).ready(check_form);
  877. $("#regform").keyup(check_form);
  878. </script>""" % (_validate_email_javascript, _validate_username_javascript,
  879. _validate_password_javascript)
  880. "Javascript code for checking inputs to the registration form"
  881. CHANGE_PASSWORD_FORM_CHECKER = """<script type="text/javascript">
  882. %s
  883. function check_form(){
  884. var p_msg = "";
  885. // validate password
  886. var p_val = $("#passwd").val();
  887. if(p_val != $("#passwd2").val()){
  888. p_msg = "Passwords do not match.";
  889. }else{
  890. var p_check_result = _validate_password(p_val);
  891. if (p_check_result !== null){
  892. p_msg = p_check_result;
  893. }
  894. }
  895. $("#pwd_check").html('<font color="red">' + p_msg + '</font>');
  896. }
  897. $(document).ready(check_form);
  898. $("#pwdform").keyup(check_form);
  899. </script>""" % _validate_password_javascript
  900. "Javascript code for checking inputs to the password change form"
  901. def reg_confirm_emails(context, username, confirmation_code):
  902. """
  903. @param context: The context associated with this request
  904. @param username: The username of the user who needs to confirm
  905. @param confirmation_code: The user's confirmation token (from
  906. L{generate_confirmation_token})
  907. @return: A 2-tuple representing the message to be sent. The first element
  908. is the plain-text version of the e-mail, and the second is the HTML
  909. version.
  910. """
  911. base = _get_base_url(context)
  912. u = "%s?loginaction=confirm_reg&username=%s&token=%s" % (base, username,
  913. confirmation_code)
  914. url_root = context['cs_url_root']
  915. return (_reg_confirm_msg_base_plain % (username, url_root, u),
  916. _reg_confirm_msg_base_html % (username, url_root, u, u))
  917. _reg_confirm_msg_base_plain = r"""You recently signed up for an account with username %s at the CAT-SOOP instance at %s.
  918. In order to confirm your account, please visit the following URL:
  919. %s
  920. If you did not sign up for this account, or if you otherwise feel that you
  921. are receiving this message in error, please ignore or delete it."""
  922. _reg_confirm_msg_base_html = r"""<p>You recently signed up for an account with username <tt>%s</tt> at the CAT-SOOP instance at <tt>%s</tt>.</p>
  923. <p>In order to confirm your account, please click on the following link:<br/>
  924. <a href="%s">%s</a></p>
  925. <p>If you did not sign up for this account, or if you otherwise feel that you
  926. are receiving this message in error, please ignore or delete it.</p>"""
  927. def passwd_confirm_emails(context, username, code):
  928. """
  929. @param context: The context associated with this request
  930. @param username: The username of the user who needs to confirm
  931. @param confirmation: The user's confirmation token (from
  932. L{generate_confirmation_token})
  933. @return: A 2-tuple representing the message to be sent. The first element
  934. is the plain-text version of the e-mail, and the second is the HTML
  935. version.
  936. """
  937. base = _get_base_url(context)
  938. u = "%s?loginaction=reset_password&username=%s&token=%s" % (base, username,
  939. code)
  940. url_root = context['cs_url_root']
  941. return (_passwd_confirm_msg_base_plain % (username, url_root, u),
  942. _passwd_confirm_msg_base_html % (username, url_root, u, u))
  943. _passwd_confirm_msg_base_plain = r"""You recently submitted a request to reset the password for an account with username %s at the CAT-SOOP instance at %s.
  944. In order to reset your password, please visit the following URL:
  945. %s
  946. If you did not submit this request, or if you otherwise feel that you
  947. are receiving this message in error, please ignore or delete it."""
  948. _passwd_confirm_msg_base_html = r"""<p>You recently submitted a request to reset the password for an account with username <tt>%s</tt> at the CAT-SOOP instance at <tt>%s</tt>.</p>
  949. <p>In order to reset your password, please click on the following link:<br/>
  950. <a href="%s">%s</a></p>
  951. <p>If you did not submit this request, or if you otherwise feel that you
  952. are receiving this message in error, please ignore or delete it.</p>"""
  953. def _submit_button(fields, username, preserve, form, value='Submit'):
  954. base = ('<input type="button"'
  955. ' id="%s_submitter"'
  956. ' value="%s"'
  957. ' onclick="catsoop.hashlib.hash_passwords(%r, %r, %r, %r)" />')
  958. base += '<script type="text/javascript">$("#%s input").keypress'
  959. base += '(function(e){if(e.which == 13) $("#%s_submitter").click();});'
  960. base += '</script>'
  961. return base % (form, value, fields, username, preserve, form, form, form)
  962. LOGIN_BOX = """
  963. <div class="response">
  964. <b><center>You are not logged in.</center></b><br/>
  965. If you are a current student, please <a href="%s?loginaction=login">Log In</a> for full access to this page.
  966. </div>
  967. """