"""Provides Flask integration for the external user interface."""
from typing import Any, Callable
from datetime import datetime, timedelta
from functools import wraps
from pytz import timezone
from flask import Blueprint, render_template, url_for, abort, request, \
    make_response, redirect, current_app, send_file, Response
from arxiv import status
from arxiv.users.auth.decorators import scoped
from arxiv.users.auth import scopes
from arxiv.users import domain
from arxiv.base import logging
from accounts.controllers import captcha_image, registration, authentication
from werkzeug.exceptions import BadRequest
EASTERN = timezone('US/Eastern')
logger = logging.getLogger(__name__)
blueprint = Blueprint('ui', __name__, url_prefix='')
[docs]def user_is_owner(session: domain.Session, user_id: str, **kw: Any) -> bool:
    """Determine whether the authenticated user matches the requested user."""
    return bool(session.user.user_id == user_id) 
[docs]def anonymous_only(func: Callable) -> Callable:
    """Redirect logged-in users to their profile."""
    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        if request.session:
            user = request.session.user
            target = url_for('account', user_id=user.user_id)
            content = redirect(target, code=status.HTTP_303_SEE_OTHER)
            response = make_response(content)
            return response
        return func(*args, **kwargs)
    return wrapper 
[docs]def set_cookies(response: Response, data: dict) -> None:
    """
    Update a :class:`.Response` with cookies in controller data.
    Contollers seeking to update cookies must include a 'cookies' key
    in their response data.
    """
    # Set the session cookie.
    cookies = data.pop('cookies')
    if cookies is None:
        return None
    for cookie_key, (cookie_value, expires) in cookies.items():
        cookie_name = current_app.config[f'{cookie_key.upper()}_NAME']
        max_age = timedelta(seconds=expires)
        # expires_date = expires_date.replace(tzinfo=EASTERN)
        logger.debug('Set cookie %s with %s, max_age %s',
                     cookie_name, cookie_value, max_age)
        domain = current_app.config['AUTH_SESSION_COOKIE_DOMAIN']
        params = dict(httponly=True, domain=domain)
        if current_app.config['AUTH_SESSION_COOKIE_SECURE']:
            # Setting samesite to lax, to allow reasonable links to
            # authenticated views using GET requests.
            params.update({'secure': True, 'samesite': 'lax'})
        response.set_cookie(cookie_name, cookie_value, max_age=max_age,
                            **params) 
# This is unlikely to be useful once the classic submission UI is disabled.
[docs]def unset_submission_cookie(response: Response) -> None:
    """
    Unset the legacy Catalyst submission cookie.
    In addition to the authenticated session (which was originally from the
    Tapir auth system), Catalyst also tracks a session used specifically for
    the submission process. The legacy Catalyst controller sets this
    automatically, so we don't need to do anything on login. But on logout,
    if this cookie is not cleared, Catalyst may attempt to use the same
    submission session upon subsequent logins. This can lead to weird
    inconsistencies.
    """
    response.set_cookie('submit_session', '', max_age=0, httponly=True) 
[docs]@blueprint.route('/register', methods=['GET', 'POST'])
@anonymous_only
def register() -> Response:
    """Interface for creating new accounts."""
    captcha_secret = current_app.config['CAPTCHA_SECRET']
    ip_address = request.remote_addr
    next_page = request.args.get('next_page', url_for('account'))
    data, code, headers = registration.register(request.method, request.form,
                                                captcha_secret, ip_address,
                                                next_page)
    # Flask puts cookie-setting methods on the response, so we do that here
    # instead of in the controller.
    if code is status.HTTP_303_SEE_OTHER:
        response = make_response(redirect(headers['Location'], code=code))
        set_cookies(response, data)
        return response
    content = render_template("accounts/register.html", **data)
    response = make_response(content, code, headers)
    return response 
# @blueprint.route('/<string:user_id>/profile', methods=['GET'])
# @scoped(scopes.VIEW_PROFILE, authorizer=user_is_owner)
# def view_profile(user_id: str) -> Response:
#     """User can view their account information."""
#     data, code, headers = registration.view_profile(user_id, request.session)
#     return render_template("accounts/profile.html", **data)
# @blueprint.route('/<string:user_id>/profile/edit', methods=['GET', 'POST'])
# @scoped(scopes.EDIT_PROFILE, authorizer=user_is_owner)
# def edit_profile(user_id: str) -> Response:
#     """User can update their account information."""
#     data, code, headers = registration.edit_profile(request.method, user_id,
#                                                     request.session,
#                                                     request.form,
#                                                     request.remote_addr)
#     if code is status.HTTP_303_SEE_OTHER:
#         target = url_for('ui.view_profile', user_id=user_id)
#         response = make_response(redirect(target, code=code))
#         set_cookies(response, data)
#         return response
#     content = render_template("accounts/edit_profile.html", **data)
#     response = make_response(content, code, headers)
#     return response
[docs]@blueprint.route('/login', methods=['GET', 'POST'])
@anonymous_only
def login() -> Response:
    """User can log in with username and password, or permanent token."""
    ip_address = request.remote_addr
    form_data = request.form
    next_page = request.args.get('next_page', url_for('account'))
    logger.debug('Request to log in, then redirect to %s', next_page)
    data, code, headers = authentication.login(request.method,
                                               form_data, ip_address,
                                               next_page)
    data.update({'pagetitle': 'Log in to arXiv'})
    # Flask puts cookie-setting methods on the response, so we do that here
    # instead of in the controller.
    if code is status.HTTP_303_SEE_OTHER:
        # Set the session cookie.
        response = make_response(redirect(headers.get('Location'), code=code))
        set_cookies(response, data)
        unset_submission_cookie(response)    # Fix for ARXIVNG-1149.
        return response
    # Form is invalid, or login failed.
    return render_template("accounts/login.html", **data), code 
[docs]@blueprint.route('/logout', methods=['GET'])
def logout() -> Response:
    """Log out of arXiv."""
    session_cookie_key = current_app.config['AUTH_SESSION_COOKIE_NAME']
    classic_cookie_key = current_app.config['CLASSIC_COOKIE_NAME']
    session_cookie = request.cookies.get(session_cookie_key, None)
    classic_cookie = request.cookies.get(classic_cookie_key, None)
    next_page = request.args.get('next_page', url_for('ui.login'))
    logger.debug('Request to log out, then redirect to %s', next_page)
    data, code, headers = authentication.logout(session_cookie, classic_cookie,
                                                next_page)
    # Flask puts cookie-setting methods on the response, so we do that here
    # instead of in the controller.
    if code is status.HTTP_303_SEE_OTHER:
        logger.debug('Redirecting to %s: %i', headers.get('Location'), code)
        response = make_response(redirect(headers.get('Location'), code=code))
        set_cookies(response, data)
        unset_submission_cookie(response)    # Fix for ARXIVNG-1149.
        return response
    return redirect(url_for('get_login'), code=status.HTTP_302_FOUND) 
[docs]@blueprint.route('/captcha', methods=['GET'])
@anonymous_only
def captcha() -> Response:
    """Provide the image for stateless stateless_captcha."""
    secret = current_app.config['CAPTCHA_SECRET']
    font = current_app.config.get('CAPTCHA_FONT')
    token = request.args.get('token')
    data, code, headers = captcha_image.get(token, secret, request.remote_addr, font)
    return send_file(data['image'], mimetype=data['mimetype']), code, headers