def edit_content(course, content_path, TOC: TableOfContent): try: content = TOC.get_page_from_path("%s.rst" % content_path) except ContentNotFoundError: content = TOC.get_content_from_path(content_path) if request.method == "POST": inpt = request.form if "new_content" not in inpt: return seeother(request.path) else: if type(content) is Chapter: with open( safe_join(syllabus.get_pages_path(course), content.path, "chapter_introduction.rst"), "w") as f: f.write(inpt["new_content"]) else: with open( safe_join(syllabus.get_pages_path(course), content.path), "w") as f: f.write(inpt["new_content"]) return seeother(request.path) elif request.method == "GET": return render_template("edit_page.html", content_data=get_content_data(course, content), content=content, TOC=TOC)
def log_in(): if request.method == "GET": return render_template("login.html", auth_methods=syllabus.get_config()['authentication_methods'], feedback=pop_feeback(session, feedback_type="login")) if request.method == "POST": if "local" not in syllabus.get_config()['authentication_methods']: abort(404) inpt = request.form email = inpt["email"] password = inpt["password"] try: password_hash = hash_password_func(email=email, password=password, global_salt=syllabus.get_config().get('password_salt', None), n_iterations=syllabus.get_config().get('password_hash_iterations', 100000)) except UnicodeEncodeError: # TODO: log return seeother("/login") user = User.query.filter(User.email == email).first() if user is None or user.hash_password != password_hash: set_feedback(session, ErrorFeedback("Invalid email/password."), feedback_type="login") return seeother("/login") if not user.activated: set_feedback(session, ErrorFeedback("Your account is not yet activated."), feedback_type="login") return seeother("/login") session['user'] = user.to_dict() return seeother(session.get("last_visited", "/"))
def config_edition(): if request.method == "POST": inpt = request.form if "new_config" in inpt: try: # check YAML validity config = yaml.load(inpt["new_config"]) # TODO: check that the configuration has the appropriate fields # update the config old_config = syllabus.get_config() syllabus.set_config(inpt["new_config"]) # sync the git repo if it has changed try: if ( "git" not in old_config["pages"] and "git" in config["pages"] ) or old_config["pages"]["git"] != config["pages"]["git"]: syllabus.utils.pages.init_and_sync_repo( force_sync=True) except KeyError as e: pass except AttributeError: return seeother( request.path, Feedback(feedback_type="error", message="The git repository has " "failed to synchronize. " "Please check your " "configuration.")) return seeother( request.path, Feedback(feedback_type="success", message="The table of contents has been" " modified successfully !")) except yaml.YAMLError: return seeother( request.path, feedback=Feedback( feedback_type="error", message="The submitted configuration is not " "written in valid YAML.")) else: params = Params.query.one() config = syllabus.get_config() hook_path = ("/update_pages/%s" % params.git_hook_url) if "git" in config[ "pages"] and params.git_hook_url is not None else None try: with open(syllabus.get_config_path(), 'r') as f: return render_template( 'edit_configuration.html', active_element=sidebar['active_element'], sidebar_elements=sidebar['elements'], config=f.read(), hook_path=hook_path, feedback=pop_feeback(session)) except TemplateNotFound: abort(404)
def toc_edition(course): if not course in syllabus.get_config()["courses"].keys(): abort(404) toc = syllabus.get_toc(course) if toc.ignored and not has_feedback(session): set_feedback( session, Feedback(feedback_type="warning", message="The following contents have not been found :\n" + "<pre>" + "\n".join(toc.ignored) + "</pre>")) if request.method == "POST": inpt = request.form if "new_content" in inpt: try: # check YAML validity toc_dict = yaml.load(inpt["new_content"], OrderedDictYAMLLoader) if not TableOfContent.is_toc_dict_valid( syllabus.get_pages_path(course), toc_dict): set_feedback( session, Feedback(feedback_type="error", message="The submitted table of contents " "is not consistent with the files " "located in the pages directory.")) return seeother(request.path) except yaml.YAMLError: set_feedback( session, Feedback(feedback_type="error", message="The submitted table of contents is not " "written in valid YAML.")) return seeother(request.path) # the YAML is valid, write it in the ToC with open( os.path.join(syllabus.get_pages_path(course), "toc.yaml"), "w") as f: f.write(inpt["new_content"]) syllabus.get_toc(course, force=True) set_feedback( session, Feedback(feedback_type="success", message="The table of contents has been modified " "successfully !")) return seeother(request.path) else: with open(os.path.join(syllabus.get_pages_path(course), "toc.yaml"), "r") as f: try: return render_template( 'edit_table_of_content.html', active_element=sidebar['active_element'], sidebar_elements=sidebar['elements'], content=f.read(), feedback=pop_feeback(session)) except TemplateNotFound: abort(404)
def delete_content(course, inpt, TOC): pages_path = syllabus.get_pages_path(course) content_path = inpt["content-path"] path = os.path.join(pages_path, content_path) content_to_delete = TOC.get_content_from_path(content_path) TOC.remove_content_from_toc(content_to_delete) # dump the TOC and reload it syllabus.save_toc(course, TOC) syllabus.get_toc(course, force=True) # remove the files if asked if inpt.get("delete-files", None) == "on": if type(content_to_delete) is Chapter: # delete a chapter shutil.rmtree(os.path.join(path)) else: # delete a page os.remove(path) set_feedback( session, Feedback(feedback_type="success", message="The content has been successfully deleted")) return seeother(request.path)
def update_pages(secret, course): params = Params.query.one() if secret != params.git_hook_url or 'git' not in syllabus.get_config( )['courses'][course]['pages']: return seeother("/") syllabus.utils.pages.init_and_sync_repo(course, force_sync=True) return "done"
def users(): if request.method == 'POST': inpt = request.form if inpt["action"] == "change_right": user = User.query.filter(User.username == inpt["username"]).first() if user.username == session["user"]["username"]: return seeother(request.path) user.right = "admin" if "admin" in inpt and inpt["admin"] == "on" else None db_session.commit() return seeother(request.path, SuccessFeedback("The rights of %s have been successfully edited" % user.username)) return seeother(request.path) try: return render_template('users.html', active_element=sidebar['active_element'], sidebar_elements=sidebar['elements'], users=User.query.all(), feedback=pop_feeback(session)) except TemplateNotFound: abort(404)
def log_out(): if "user" in session: saml = session["user"].get("login_method", None) == "saml" session.pop("user", None) if saml and "singleLogoutService" in saml_config["sp"]: req = prepare_request(request) auth = init_saml_auth(req, saml_config) return redirect(auth.logout()) return seeother('/')
def log_in(): if request.method == "GET": return render_template("login.html", auth_methods=syllabus.get_config()['authentication_methods']) if request.method == "POST": inpt = request.form username = inpt["username"] password = inpt["password"] try: password_hash = hash_password(password.encode("utf-8")) except UnicodeEncodeError: # TODO: log return seeother("/login") user = User.query.filter(User.username == username).first() if user is None or user.hash_password != password_hash: abort(403) session['user'] = user.to_dict() return seeother('/')
def reset_password(secret): user = db_session.query(User).filter(User.change_password_url == secret).first() if user is None: # TODO: log return seeother("/") if request.method == "GET": return render_template("reset_password.html", alert_hidden=True) if request.method == "POST": inpt = request.form password = inpt["password"] password_confirm = inpt["password_confirm"] if password != password_confirm: return render_template("reset_password.html", alert_hidden=False) password_hash = hash_password(password.encode("utf-8")) user.hash_password = password_hash user.change_password_url = None db_session.commit() return seeother("/login")
def saml(): if "saml" not in syllabus.get_config()['authentication_methods']: abort(404) req = prepare_request(request) req['request_uri'] = request.path # hack to ensure to have the correct path and to avoid RelayState loops auth = init_saml_auth(req, saml_config) # if 'sso' in request.args: # return if request.method == "GET": return redirect(auth.login()) else: auth.process_response() errors = auth.get_errors() # Try and check if IdP is using several signature certificates # This is a limitation of python3-saml for cert in saml_config["idp"].get("additionalX509certs", []): if auth.get_last_error_reason( ) == "Signature validation failed. SAML Response rejected": import copy # Change used IdP certificate new_settings = copy.deepcopy(saml_config) new_settings["idp"]["x509cert"] = cert # Retry processing response auth = init_saml_auth(req, new_settings) auth.process_response() errors = auth.get_errors() if len(errors) == 0: attrs = auth.get_attributes() # session['samlNameId'] = auth.get_nameid() # session['samlSessionIndex'] = auth.get_session_index() username = attrs[saml_config['sp']['attrs']['username']][0] realname = attrs[saml_config['sp']['attrs']['realname']][0] email = attrs[saml_config['sp']['attrs']['email']][0] user = User.query.filter(User.email == email).first() if user is None: # The user does not exist in our DB user = User(name=username, full_name=realname, email=email, hash_password=None, change_password_url=None) db_session.add(user) db_session.commit() session["user"] = user.to_dict() session["user"].update({"login_method": "saml"}) self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form[ 'RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) return seeother("/")
def reset_password(secret): user = db_session.query(User).filter(User.change_password_url == secret).first() if user is None: # TODO: log return seeother("/") if request.method == "GET": return render_template("reset_password.html", alert_hidden=True) if request.method == "POST": inpt = request.form password = inpt["password"] password_confirm = inpt["password_confirm"] if password != password_confirm: return render_template("reset_password.html", alert_hidden=False) password_hash = hash_password_func(email=user.email, password=password, global_salt=syllabus.get_config().get('password_salt', None), n_iterations=syllabus.get_config().get('password_hash_iterations', 100000)) user.hash_password = password_hash user.change_password_url = None db_session.commit() return seeother("/login")
def log_out(): if "user" in session: saml = session["user"].get("login_method", None) == "saml" session.pop("user", None) if saml and "singleLogoutService" in saml_config["sp"]: try: req = prepare_request(request) auth = init_saml_auth(req, saml_config) return redirect(auth.logout()) except OneLogin_Saml2_Error: pass return seeother(session.get("last_visited", "/"))
def sphinx_rebuild(course, course_config): if course_config.get("sphinx"): syllabus.get_sphinx_build(course, True) set_feedback( session, Feedback(feedback_type="success", message="The syllabus has been successfully rebuilt")) else: set_feedback( session, Feedback(feedback_type="error", message="The syllabus is not a sphinx syllabus")) return seeother(request.path)
def course_index(course, print_mode=False): if not course in syllabus.get_config()["courses"].keys(): abort(404) session["course"] = course course_config = syllabus.get_config()["courses"][course] if course_config.get("sphinx"): return seeother("/syllabus/{}/{}".format(course, course_config["sphinx"].get("index_page", "index.html"))) try: TOC = syllabus.get_toc(course) if request.args.get("edit") is not None: return edit_content(course, TOC.index.path, TOC) print_mode = print_mode or request.args.get("print") is not None # only display the button to print the whole syllabus in the index return render_web_page(course, TOC.index, print_mode=print_mode, display_print_all=True) except ContentNotFoundError: abort(404)
def saml(): if "saml" not in syllabus.get_config()['authentication_methods']: abort(404) req = prepare_request(request) req['request_uri'] = request.path # hack to ensure to have the correct path and to avoid RelayState loops auth = init_saml_auth(req, saml_config) # if 'sso' in request.args: # return if request.method == "GET": return redirect(auth.login()) elif 'acs' in request.args: auth.process_response() errors = auth.get_errors() if len(errors) == 0: attrs = auth.get_attributes() # session['samlNameId'] = auth.get_nameid() # session['samlSessionIndex'] = auth.get_session_index() username = attrs[saml_config['sp']['attrs']['username']][0] realname = attrs[saml_config['sp']['attrs']['realname']][0] email = attrs[saml_config['sp']['attrs']['email']][0] user = User.query.filter(User.email == email).first() if user is None: # The user does not exist in our DB user = User(name=username, full_name=realname, email=email, hash_password=None, change_password_url=None) db_session.add(user) db_session.commit() session["user"] = user.to_dict() session["user"].update({"login_method": "saml"}) self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form[ 'RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) return seeother("/")
def activate_account(): email = request.args.get('email') mac = request.args.get('token') try: ts = int(request.args.get('ts')) except TypeError: abort(404) return None email_activation_config = syllabus.get_config()["authentication_methods"]["local"].get("email_activation", {}) if not verify_activation_mac(email=email, secret=email_activation_config["secret"], mac_to_verify=mac, timestamp=ts): # the mac does not match the provided email abort(404) return None if User.query.filter(User.email == email).count() != 0: set_feedback(session, ErrorFeedback("This user is already activated."), feedback_type="login") return seeother("/login") if request.method == "GET": return render_template("local_register_confirmed_account.html", feedback=pop_feeback(session, feedback_type="login")) if request.method == "POST": inpt = request.form try: u = handle_user_registration_infos(inpt, email, False) except UnicodeEncodeError: # TODO: log return seeother("/login") if u is None: return seeother("/activate?{}".format(urllib.parse.urlencode({"email": email, "token": mac, "ts": ts}))) try: locally_register_new_user(u, activated=True) feedback_message = "You have been successfully registered." set_feedback(session, SuccessFeedback(feedback_message), feedback_type="login") return seeother("/login") except UserAlreadyExists: db_session.rollback() set_feedback(session, ErrorFeedback("Could not register: this user already exists."), feedback_type="login") return seeother("/register/{}/{}".format(email, mac)) set_feedback(session, SuccessFeedback("Your account has been successfully activated"), feedback_type="login") return seeother("/login")
def content_edition(course): if not course in syllabus.get_config()["courses"].keys(): abort(404) TOC = syllabus.get_toc(course) if TOC.ignored and not has_feedback(session): set_feedback( session, Feedback(feedback_type="warning", message="The following contents have not been found :\n" + "<pre>" + "\n".join(TOC.ignored) + "</pre>")) if request.method == "POST": inpt = request.form if inpt["action"] == "delete_content": return delete_content(course, inpt, TOC) containing_chapter = None try: if inpt["containing-chapter"] != "": containing_chapter = TOC.get_chapter_from_path( inpt["containing-chapter"]) except ContentNotFoundError: set_feedback( session, Feedback(feedback_type="error", message="The containing chapter for this page " "does not exist")) return seeother(request.path) # sanity checks on the filename: refuse empty filenames of filenames with spaces if len(inpt["name"]) == 0 or len(re.sub( r'\s+', '', inpt["name"])) != len(inpt["name"]): set_feedback( session, Feedback( feedback_type="error", message='The file name cannot be empty or contain spaces.') ) return seeother(request.path) if os.sep in inpt["name"]: set_feedback( session, Feedback( feedback_type="error", message= 'The file name cannot contain the "%s" forbidden character.' % os.sep)) return seeother(request.path) pages_path = syllabus.get_pages_path() content_path = os.path.join( containing_chapter.path, inpt["name"]) if containing_chapter is not None else inpt["name"] path = os.path.join(pages_path, content_path) # ensure that we do not create a page at the top level if containing_chapter is None and inpt["action"] == "create_page": set_feedback( session, Feedback(feedback_type="error", message="Pages cannot be at the top level of the " "syllabus.")) return seeother(request.path) # check that there is no chapter with the same title in the containing chapter if containing_chapter is None: for content in TOC.get_top_level_content(): if content.title == inpt["title"]: set_feedback( session, Feedback(feedback_type="error", message="There is already a top level " "chapter with this title.")) return seeother(request.path) else: for content in TOC.get_direct_content_of(containing_chapter): if content.title == inpt["title"]: set_feedback( session, Feedback(feedback_type="error", message="There is already a chapter/page " "with this title in this chapter.")) return seeother(request.path) if inpt["action"] == "create_page": # add the extension to the page filename content_path += ".rst" path += ".rst" # when file/directory already exists if os.path.isdir(path) or os.path.isfile(path): set_feedback( session, Feedback(feedback_type="error", message="A file or directory with this name " "already exists.")) return seeother(request.path) # create a new page open(path, "w").close() page = Page(path=content_path, title=inpt["title"], pages_path=syllabus.get_pages_path(course)) TOC.add_content_in_toc(page) elif inpt["action"] == "create_chapter": # creating a new chapter try: os.mkdir(path) except FileExistsError: set_feedback( session, Feedback(feedback_type="error", message="A file or directory with this name " "already exists.")) return seeother(request.path) chapter = Chapter(path=content_path, title=inpt["title"], pages_path=syllabus.get_pages_path(course)) TOC.add_content_in_toc(chapter) # dump the TOC and reload it syllabus.save_toc(course, TOC) syllabus.get_toc(course, force=True) return seeother(request.path) try: return render_template('content_edition.html', active_element=sidebar['active_element'], course_str=course, sidebar_elements=sidebar['elements'], TOC=TOC, feedback=pop_feeback(session)) except TemplateNotFound: abort(404)
def register(): if "local" not in syllabus.get_config()['authentication_methods']: abort(404) timestamp = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) email_activation_config = syllabus.get_config()["authentication_methods"]["local"].get("email_activation", {}) activation_required = email_activation_config.get("required", True) if request.method == "GET": return render_template("register.html", auth_methods=syllabus.get_config()['authentication_methods'], feedback=pop_feeback(session, feedback_type="login"), activation_required=activation_required) if request.method == "POST": inpt = request.form if activation_required: # send the email confirmation email = inpt["email"] auth_config = email_activation_config.get("authentication", {}) parsed_url = urllib.parse.urlparse(urllib.parse.urljoin(request.host_url, "activate")) parsed_url = parsed_url._replace(query=urllib.parse.urlencode({ "email": email, "token": get_activation_mac(email=email, secret=email_activation_config["secret"], timestamp=timestamp), "ts": timestamp })) url = urllib.parse.urlunparse(parsed_url) if auth_config.get("required", False): send_authenticated_confirmation_mail(email_activation_config["sender_email_address"], email, url, email_activation_config["smtp_server"], username=auth_config["username"], password=auth_config["password"], smtp_port=email_activation_config["smtp_server_port"]) else: send_confirmation_mail(email_activation_config["sender_email_address"], email, url, email_activation_config["smtp_server"], use_ssl=email_activation_config["use_ssl"], smtp_port=email_activation_config["smtp_server_port"]) feedback_message = "Registration successful. Please activate your account using the activation link you received by e-mail." \ "Click <a href=\"/login\">here</a> to log in." set_feedback(session, SuccessFeedback(feedback_message), feedback_type="login") return seeother("/activation_needed") # here, the activation is not required try: u = handle_user_registration_infos(inpt, inpt["email"], False) except UnicodeEncodeError: # TODO: log return seeother("/login") if u is None: return seeother("/register") try: locally_register_new_user(u, not activation_required) feedback_message = "You have been successfully registered." set_feedback(session, SuccessFeedback(feedback_message), feedback_type="login") return seeother("/login") except UserAlreadyExists as e: set_feedback(session, ErrorFeedback("Could not register: this user already exists{}.".format( (": %s" % e.reason) if e.reason is not None else "")), feedback_type="login") db_session.rollback() return seeother("/register")