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.
 
 
 

1206 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_cslog"].FERNET.decrypt(pass_hash)
  528. salt = user_login_info.get("password_salt", None)
  529. hashed_pass = compute_password_hash(
  530. context, provided, salt, iterations, encrypt=False
  531. )
  532. return hashed_pass == pass_hash
  533. return False
  534. def get_new_password_salt(length=128):
  535. """
  536. Generate a new salt of length length. Tries to use os.urandom, and
  537. falls back on random if that doesn't work for some reason.
  538. """
  539. try:
  540. out = os.urandom(length)
  541. except:
  542. out = "".join(chr(random.randint(1, 127)) for i in range(length)).encode()
  543. return out
  544. def _ensure_bytes(x):
  545. try:
  546. return x.encode()
  547. except:
  548. return x
  549. def compute_password_hash(
  550. context, password, salt=None, iterations=500000, encrypt=True
  551. ):
  552. """
  553. Given a password, and (optionally) an associated salt, return a hash value.
  554. """
  555. hash_ = hashlib.pbkdf2_hmac(
  556. "sha512", _ensure_bytes(password), _ensure_bytes(salt), iterations
  557. )
  558. if encrypt and (context["csm_cslog"].ENCRYPT_KEY is not None):
  559. hash_ = context["csm_cslog"].FERNET.encrypt(hash_)
  560. return hash_
  561. def generate_confirmation_token(n=20):
  562. chars = string.ascii_uppercase + string.digits
  563. return "".join(random.choice(chars) for i in range(n))
  564. def _get_base_url(context):
  565. return "/".join([context["cs_url_root"]] + context["cs_path_info"])
  566. def generate_forgot_password_form(context):
  567. """
  568. Generate a "forgot password" form.
  569. """
  570. base = _get_base_url(context)
  571. req_url = base + "?loginaction=forgot_password"
  572. out = INCLUDE_HASHLIB
  573. out += '<form method="POST" action="%s">' % req_url
  574. msg = context["cs_session_data"].get("login_message", None)
  575. if msg is not None:
  576. out += "\n%s<p>" % msg
  577. last = context["cs_session_data"].get("last_form", {})
  578. last_uname = last.get("uname", "").replace('"', "&quot;")
  579. last_email = last.get("email", "").replace('"', "&quot;")
  580. out += (
  581. "\n<table>"
  582. "\n<tr>"
  583. '\n<td style="text-align:right;">Username:</td>'
  584. '\n<td style="text-align:right;">'
  585. '\n<input type="text" '
  586. 'name="uname" '
  587. 'id="uname" '
  588. 'value="%s"/>'
  589. "\n</td>"
  590. "\n</tr>"
  591. ) % last_uname
  592. out += (
  593. "\n<tr>"
  594. '\n<td style="text-align:right;">Email Address:</td>'
  595. '\n<td style="text-align:right;">'
  596. '\n<input type="text" '
  597. 'name="email" '
  598. 'id="email" '
  599. 'value="%s" />'
  600. "\n</td>"
  601. "\n</tr>"
  602. ) % last_email
  603. out += (
  604. "\n<tr>"
  605. '\n<td style="text-align:right;"></td>'
  606. '\n<td style="text-align:right;">'
  607. '\n<input type="submit" value="Reset Password" class="btn btn-catsoop"></td>'
  608. "\n</tr>"
  609. )
  610. out += "\n</table>\n</form>"
  611. return out
  612. def generate_password_reset_form(context):
  613. """
  614. Generate a "reset password" form.
  615. """
  616. base = _get_base_url(context)
  617. req_url = base + "?loginaction=reset_password"
  618. req_url += "&username=%s" % context["cs_form"]["username"]
  619. req_url += "&token=%s" % context["cs_form"]["token"]
  620. out = INCLUDE_HASHLIB
  621. out += '<form method="POST" action="%s" id="pwdform">' % req_url
  622. msg = context["cs_session_data"].get("login_message", None)
  623. if msg is not None:
  624. out += "\n%s<p>" % msg
  625. safe_uname = context["cs_form"]["username"].replace('"', "&quot;")
  626. out += '\n<input type="hidden" name="uname" id="uname" value="%s" />' % safe_uname
  627. out += "\n<table>"
  628. out += (
  629. "\n<tr>"
  630. '\n<td style="text-align:right;">New Password:</td>'
  631. '\n<td style="text-align:right;">'
  632. '\n<input type="password" name="passwd" id="passwd" />'
  633. "\n</td>"
  634. "\n</tr>"
  635. )
  636. out += (
  637. "\n<tr>"
  638. '\n<td style="text-align:right;">Confirm New Password:</td>'
  639. '\n<td style="text-align:right;">'
  640. '\n<input type="password" name="passwd2" id="passwd2" />'
  641. "\n</td>"
  642. '\n<td><span id="pwd_check"></span></td>'
  643. "\n</tr>"
  644. )
  645. out += (
  646. "\n<tr>"
  647. '\n<td style="text-align:right;"></td>'
  648. '\n<td style="text-align:right;">'
  649. )
  650. out += _submit_button(
  651. ["passwd", "passwd2"], "uname", [], "pwdform", "Change Password"
  652. )
  653. out += "</td>\n</tr>"
  654. out += "\n</table>\n</form>"
  655. out += CHANGE_PASSWORD_FORM_CHECKER
  656. return out
  657. def generate_password_change_form(context):
  658. """
  659. Generate a "change password" form.
  660. """
  661. base = _get_base_url(context)
  662. req_url = base + "?loginaction=change_password"
  663. out = INCLUDE_HASHLIB
  664. out += '<form method="POST" action="%s" id="pwdform">' % req_url
  665. msg = context["cs_session_data"].get("login_message", None)
  666. if msg is not None:
  667. out += "\n%s<p>" % msg
  668. safe_uname = context["cs_session_data"]["username"].replace('"', "&quot;")
  669. out += '\n<input type="hidden" name="uname" id="uname" value="%s" />' % safe_uname
  670. out += "\n<table>"
  671. out += (
  672. "\n<tr>"
  673. '\n<td style="text-align:right;">Current Password:</td>'
  674. '\n<td style="text-align:right;">'
  675. '\n<input type="password" name="oldpasswd" id="oldpasswd" />'
  676. "\n</td>"
  677. "\n</tr>"
  678. )
  679. out += (
  680. "\n<tr>"
  681. '\n<td style="text-align:right;">New Password:</td>'
  682. '\n<td style="text-align:right;">'
  683. '\n<input type="password" name="passwd" id="passwd" />'
  684. "\n</td>"
  685. "\n</tr>"
  686. )
  687. out += (
  688. "\n<tr>"
  689. '\n<td style="text-align:right;">Confirm New Password:</td>'
  690. '\n<td style="text-align:right;">'
  691. '\n<input type="password" name="passwd2" id="passwd2" />'
  692. "\n</td>"
  693. '\n<td><span id="pwd_check"></span></td>'
  694. "\n</tr>"
  695. )
  696. out += (
  697. "\n<tr>"
  698. '\n<td style="text-align:right;"></td>'
  699. '\n<td style="text-align:right;">'
  700. )
  701. out += _submit_button(
  702. ["passwd", "passwd2", "oldpasswd"], "uname", [], "pwdform", "Change Password"
  703. )
  704. out += "</td>\n</tr>"
  705. out += "\n</table>\n</form>"
  706. out += CHANGE_PASSWORD_FORM_CHECKER
  707. return out
  708. def generate_login_form(context):
  709. """
  710. Generate a login form.
  711. """
  712. base = _get_base_url(context)
  713. out = INCLUDE_HASHLIB
  714. out += '<form method="POST" id="loginform" action="%s">' % (
  715. base + "?loginaction=login"
  716. )
  717. msg = context["cs_session_data"].get("login_message", None)
  718. last_uname = context["cs_session_data"].get("last_form", {}).get("login_uname", "")
  719. if msg is not None:
  720. out += "\n%s<p>" % msg
  721. last_uname = last_uname.replace('"', "&quot;")
  722. out += (
  723. "\n<table>"
  724. "\n<tr>"
  725. '\n<td style="text-align:right;">Username:</td>'
  726. '\n<td style="text-align:right;">'
  727. '\n<input type="text" '
  728. 'name="login_uname" '
  729. 'id="login_uname" '
  730. 'value="%s"/>'
  731. "\n</td>"
  732. "\n</tr>"
  733. "\n<tr>"
  734. '\n<td style="text-align:right;">Password:</td>'
  735. '\n<td style="text-align:right;">'
  736. '\n<input type="password" '
  737. 'name="login_passwd" '
  738. 'id="login_passwd" />'
  739. "\n</td>"
  740. "\n</tr>"
  741. "\n<tr>"
  742. '\n<td style="text-align:right;"></td>'
  743. '\n<td style="text-align:right;">'
  744. ) % last_uname
  745. out += _submit_button(
  746. ["login_passwd"], "login_uname", ["login_uname"], "loginform", "Log In"
  747. )
  748. out += "<td>\n</tr>" "\n</table>"
  749. out += "<p>"
  750. if context["csm_mail"].can_send_email(context):
  751. base = _get_base_url(context)
  752. loc = base + "?loginaction=forgot_password"
  753. out += ("\nForgot your password? " 'Click <a href="%s">here</a>.<br/>') % loc
  754. if context.get("cs_allow_registration", True):
  755. loc = _get_base_url(context)
  756. loc += "?loginaction=register"
  757. link = '<a href="%s">create one</a>' % loc
  758. out += "\nIf you do not already have an account, please %s." % link
  759. out += "</p>"
  760. return out + "</form>"
  761. def generate_registration_form(context):
  762. """
  763. Generate a registration form.
  764. """
  765. base = _get_base_url(context)
  766. qstring = "?loginaction=register"
  767. out = INCLUDE_HASHLIB
  768. out += '<form method="POST" action="%s" id="regform">' % (base + qstring)
  769. last = context["cs_session_data"].get("last_form", {})
  770. msg = context["cs_session_data"].get("login_message", None)
  771. if msg is not None:
  772. out += "\n%s<p>" % msg
  773. last_name = last.get("name", "").replace('"', "&quot;")
  774. last_uname = last.get("uname", "").replace('"', "&quot;")
  775. last_email = last.get("email", "").replace('"', "&quot;")
  776. last_email2 = last.get("email2", "").replace('"', "&quot;")
  777. out += (
  778. "\n<table>"
  779. "\n<tr>"
  780. '\n<td style="text-align:right;">Username:</td>'
  781. '\n<td style="text-align:right;">'
  782. '\n<input type="text" '
  783. 'name="uname" '
  784. 'id="uname" '
  785. 'value="%s"/>'
  786. "\n</td>"
  787. '\n<td><span id="uname_check"></span></td>'
  788. "\n</tr>"
  789. ) % last_uname
  790. out += (
  791. "\n<tr>"
  792. '\n<td style="text-align:right;">Email Address:</td>'
  793. '\n<td style="text-align:right;">'
  794. '\n<input type="text" '
  795. 'name="email" '
  796. 'id="email" '
  797. 'value="%s" />'
  798. "\n</td>"
  799. "\n</tr>"
  800. ) % last_email
  801. out += (
  802. "\n<tr>"
  803. '\n<td style="text-align:right;">Confirm Email Address:</td>'
  804. '\n<td style="text-align:right;">'
  805. '\n<input type="text" '
  806. 'name="email2" '
  807. 'id="email2" '
  808. 'value="%s"/>'
  809. "\n</td>"
  810. '\n<td><span id="email_check"></span></td>'
  811. "\n</tr>"
  812. ) % last_email2
  813. out += (
  814. "\n<tr>"
  815. '\n<td style="text-align:right;">Password:</td>'
  816. '\n<td style="text-align:right;">'
  817. '\n<input type="password" name="passwd" id="passwd" />'
  818. "\n</td>"
  819. "\n</tr>"
  820. )
  821. out += (
  822. "\n<tr>"
  823. '\n<td style="text-align:right;">Confirm Password:</td>'
  824. '\n<td style="text-align:right;">'
  825. '\n<input type="password" name="passwd2" id="passwd2" />'
  826. "\n</td>"
  827. '\n<td><span id="pwd_check"></span></td>'
  828. "\n</tr>"
  829. )
  830. out += (
  831. "\n<tr>"
  832. '\n<td style="text-align:right;">Name (Optional):</td>'
  833. '\n<td style="text-align:right;">'
  834. '\n<input type="text" name="name" id="name" value="%s"/>'
  835. "\n</td>"
  836. "\n</tr>"
  837. ) % last_name
  838. out += (
  839. "\n<tr>"
  840. '\n<td style="text-align:right;"></td>'
  841. '\n<td style="text-align:right;">'
  842. )
  843. out += _submit_button(
  844. ["passwd", "passwd2"],
  845. "uname",
  846. ["uname", "email", "email2", "name"],
  847. "regform",
  848. "Register",
  849. )
  850. out += "\n</td>" "\n</tr>"
  851. out += REGISTRATION_FORM_CHECKER
  852. return out + "</table></form>"
  853. def _run_validators(validators, x):
  854. for extra_validator_regexp, error_msg in validators:
  855. if not re.match(extra_validator_regexp, x):
  856. return error_msg
  857. return None
  858. # PASSWORD VALIDATION
  859. _pwd_too_short_msg = "Passwords must be at least 8 characters long."
  860. _validate_password_javascript = r"""
  861. function _validate_password(p){
  862. if (p.length < 8){
  863. return %r;
  864. }
  865. return null;
  866. }
  867. """ % (
  868. _pwd_too_short_msg
  869. )
  870. # EMAIL VALIDATION
  871. # email validation regex from http://www.regular-expressions.info/email.html
  872. _re_valid_email_string = (
  873. r"^[A-Za-z0-9._%+-]{1,64}@(?:[A-Za-z0-9-]{1,63}\.){1,125}[A-Za-z]{2,63}$"
  874. )
  875. RE_VALID_EMAIL = re.compile(_re_valid_email_string)
  876. _eml_too_long_msg = "E-mail addresses must be less than 255 characters long."
  877. _eml_invalid_msg = "Please make sure you have entered a valid e-mail address."
  878. _validate_email_javascript = r"""
  879. var _re_valid_email = /%s/;
  880. function _validate_email(e){
  881. if (e.length > 254) {
  882. return %r;
  883. } else if (!_re_valid_email.test(e)){
  884. return %r;
  885. }
  886. return null;
  887. }
  888. """ % (
  889. _re_valid_email_string,
  890. _eml_too_long_msg,
  891. _eml_invalid_msg,
  892. )
  893. def _validate_email(context, e):
  894. if len(e) > 254:
  895. return _eml_too_long_msg
  896. elif not RE_VALID_EMAIL.match(e):
  897. return _eml_invalid_msg
  898. return _run_validators(context.get("cs_extra_email_validators", []), e)
  899. # USERNAME VALIDATION
  900. _re_valid_username_string = r"^[A-Za-z0-9][A-Za-z0-9_.-]*$"
  901. RE_VALID_USERNAME = re.compile(_re_valid_username_string)
  902. _uname_too_short_msg = "Usernames must contain at least one character."
  903. _uname_wrong_start_msg = "Usernames must begin with an ASCII letter or number."
  904. _uname_invalid_msg = (
  905. "Usernames must contain only letters and numbers, "
  906. "dashes (-), underscores (_), and periods (.)."
  907. )
  908. _validate_username_javascript = r"""
  909. var _re_valid_uname = /%s/;
  910. function _validate_username(u){
  911. if (u.length < 1){
  912. return %r;
  913. } else if (!_re_valid_uname.test(u)){
  914. if (!_re_valid_uname.test(u.charAt(0))){
  915. return %r;
  916. }else{
  917. return %r;
  918. }
  919. }
  920. return null;
  921. }
  922. """ % (
  923. _re_valid_username_string,
  924. _uname_too_short_msg,
  925. _uname_wrong_start_msg,
  926. _uname_invalid_msg,
  927. )
  928. def _validate_username(context, u):
  929. if len(u) < 1:
  930. return _uname_too_short_msg
  931. elif not RE_VALID_USERNAME.match(u):
  932. if not RE_VALID_USERNAME.match(u[0]):
  933. return _uname_wrong_start_msg
  934. else:
  935. return _uname_invalid_msg
  936. return _run_validators(context.get("cs_extra_username_validators", []), u)
  937. REGISTRATION_FORM_CHECKER = """<script type="text/javascript">
  938. // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3
  939. %s
  940. %s
  941. %s
  942. function check_form(){
  943. var e_msg = "";
  944. var u_msg = "";
  945. var p_msg = "";
  946. // validate email
  947. var e_val = document.getElementById("email").value;
  948. if(e_val.length == 0){
  949. e_msg = "Please enter an email address.";
  950. }else if(e_val != document.getElementById("email2").value){
  951. e_msg = "E-mail addresses do not match.";
  952. }else{
  953. var e_check_result = _validate_email(e_val);
  954. if (e_check_result !== null){
  955. e_msg = e_check_result;
  956. }
  957. }
  958. document.getElementById("email_check").innerHTML = '<font color="red">' + e_msg + '</font>';
  959. // validate username
  960. var u_val = document.getElementById("uname").value;
  961. if(u_val.length == 0){
  962. u_msg = "Please enter a username.";
  963. }else{
  964. var u_check_result = _validate_username(u_val);
  965. if (u_check_result !== null){
  966. u_msg = u_check_result;
  967. }
  968. }
  969. document.getElementById("uname_check").innerHTML = '<font color="red">' + u_msg + '</font>';
  970. // validate password
  971. var p_val = document.getElementById("passwd").value;
  972. if(p_val != document.getElementById("passwd2").value){
  973. p_msg = "Passwords do not match.";
  974. }else{
  975. var p_check_result = _validate_password(p_val);
  976. if (p_check_result !== null){
  977. p_msg = p_check_result;
  978. }
  979. }
  980. document.getElementById("pwd_check").innerHTML = '<font color="red">' + p_msg + '</font>';
  981. document.getElementById('regform_submitter').disabled = !!(e_msg || u_msg || p_msg);
  982. }
  983. document.addEventListener('DOMContentLoaded', check_form);
  984. document.getElementById("regform").addEventListener('keyup', check_form);
  985. // @license-end
  986. </script>""" % (
  987. _validate_email_javascript,
  988. _validate_username_javascript,
  989. _validate_password_javascript,
  990. )
  991. "Javascript code for checking inputs to the registration form"
  992. CHANGE_PASSWORD_FORM_CHECKER = (
  993. """<script type="text/javascript">
  994. // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3
  995. %s
  996. function check_form(){
  997. var p_msg = "";
  998. // validate password
  999. var p_val = document.getElementById("passwd").value;
  1000. if(p_val != document.getElementById("passwd2").value){
  1001. p_msg = "Passwords do not match.";
  1002. }else{
  1003. var p_check_result = _validate_password(p_val);
  1004. if (p_check_result !== null){
  1005. p_msg = p_check_result;
  1006. }
  1007. }
  1008. document.getElementById("pwd_check").innerHTML = '<font color="red">' + p_msg + '</font>';
  1009. document.getElementById('pwdform_submitter').disabled = !!p_msg;
  1010. }
  1011. document.addEventListener('DOMContentLoaded', check_form);
  1012. document.getElementById("pwdform").addEventListener('keyup', check_form);
  1013. // @license-end
  1014. </script>"""
  1015. % _validate_password_javascript
  1016. )
  1017. "Javascript code for checking inputs to the password change form"
  1018. def reg_confirm_emails(context, username, confirmation_code):
  1019. """
  1020. @param context: The context associated with this request
  1021. @param username: The username of the user who needs to confirm
  1022. @param confirmation_code: The user's confirmation token (from
  1023. L{generate_confirmation_token})
  1024. @return: A 2-tuple representing the message to be sent. The first element
  1025. is the plain-text version of the e-mail, and the second is the HTML
  1026. version.
  1027. """
  1028. base = _get_base_url(context)
  1029. u = "%s?loginaction=confirm_reg&username=%s&token=%s" % (
  1030. base,
  1031. username,
  1032. confirmation_code,
  1033. )
  1034. url_root = context["cs_url_root"]
  1035. return (
  1036. _reg_confirm_msg_base_plain % (username, url_root, u),
  1037. _reg_confirm_msg_base_html % (username, url_root, u, u),
  1038. )
  1039. _reg_confirm_msg_base_plain = r"""You recently signed up for an account with username %s at the CAT-SOOP instance at %s.
  1040. In order to confirm your account, please visit the following URL:
  1041. %s
  1042. If you did not sign up for this account, or if you otherwise feel that you
  1043. are receiving this message in error, please ignore or delete it."""
  1044. _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>
  1045. <p>In order to confirm your account, please click on the following link:<br/>
  1046. <a href="%s">%s</a></p>
  1047. <p>If you did not sign up for this account, or if you otherwise feel that you
  1048. are receiving this message in error, please ignore or delete it.</p>"""
  1049. def passwd_confirm_emails(context, username, code):
  1050. """
  1051. @param context: The context associated with this request
  1052. @param username: The username of the user who needs to confirm
  1053. @param confirmation: The user's confirmation token (from
  1054. L{generate_confirmation_token})
  1055. @return: A 2-tuple representing the message to be sent. The first element
  1056. is the plain-text version of the e-mail, and the second is the HTML
  1057. version.
  1058. """
  1059. base = _get_base_url(context)
  1060. u = "%s?loginaction=reset_password&username=%s&token=%s" % (base, username, code)
  1061. url_root = context["cs_url_root"]
  1062. return (
  1063. _passwd_confirm_msg_base_plain % (username, url_root, u),
  1064. _passwd_confirm_msg_base_html % (username, url_root, u, u),
  1065. )
  1066. _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.
  1067. In order to reset your password, please visit the following URL:
  1068. %s
  1069. If you did not submit this request, or if you otherwise feel that you
  1070. are receiving this message in error, please ignore or delete it."""
  1071. _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>
  1072. <p>In order to reset your password, please click on the following link:<br/>
  1073. <a href="%s">%s</a></p>
  1074. <p>If you did not submit this request, or if you otherwise feel that you
  1075. are receiving this message in error, please ignore or delete it.</p>"""
  1076. def _submit_button(fields, username, preserve, form, value="Submit"):
  1077. base = (
  1078. '<input type="button"'
  1079. ' class="btn btn-catsoop"'
  1080. ' id="%s_submitter"'
  1081. ' value="%s"'
  1082. ' onclick="catsoop.hashlib.hash_passwords(%r, %r, %r, %r)" />'
  1083. )
  1084. base += (
  1085. '<script type="text/javascript">'
  1086. "\n// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3"
  1087. '\ndocument.querySelectorAll("#%s input").forEach(function(a){'
  1088. '\n a.addEventListener("keypress",function(e){'
  1089. '\n if(e.which == 13) document.getElementById("%s_submitter").click();'
  1090. "\n });"
  1091. "\n});"
  1092. "\n// @license-end"
  1093. "\n</script>"
  1094. )
  1095. return base % (form, value, fields, username, preserve, form, form, form)
  1096. LOGIN_BOX = """
  1097. <div class="response" id="catsoop_login_box">
  1098. <b><center>You are not logged in.</center></b><br/>
  1099. If you are a current student, please <a href="%s?loginaction=login">Log In</a> for full access to this page.
  1100. </div>
  1101. """
  1102. INCLUDE_HASHLIB = """
  1103. <script type="text/javascript" src="_auth/login/cs_hash.js"></script>
  1104. """