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.
 
 
 

1210 lines
44 KiB

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