diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 965d445eb..1221e3fd3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,6 +44,8 @@ repos: 'PYSEC-2023-73', # https://github.com/RedisLabs/redisraft/issues/608 '--ignore-vuln', 'PYSEC-2023-101', # https://github.com/pytest-dev/pytest-selenium/issues/310 + '--ignore-vuln', + 'PYSEC-2023-206', # pytest-selenium again ] files: ^requirements/.*\.txt$ - repo: https://github.com/asottile/pyupgrade diff --git a/funnel/views/account.py b/funnel/views/account.py index 4a5776e24..a28a7f8bc 100644 --- a/funnel/views/account.py +++ b/funnel/views/account.py @@ -348,7 +348,7 @@ def edit(self) -> ReturnView: form = AccountForm(obj=current_auth.user) if form.validate_on_submit(): form.populate_obj(current_auth.user) - autoset_timezone_and_locale(current_auth.user) + autoset_timezone_and_locale() db.session.commit() user_data_changed.send(current_auth.user, changes=['profile']) diff --git a/funnel/views/helpers.py b/funnel/views/helpers.py index 76fee10c3..c87755592 100644 --- a/funnel/views/helpers.py +++ b/funnel/views/helpers.py @@ -4,11 +4,13 @@ import gzip import zlib +import zoneinfo from base64 import urlsafe_b64encode from collections.abc import Callable from contextlib import nullcontext from datetime import datetime, timedelta from hashlib import blake2b +from importlib import resources from os import urandom from typing import Any from urllib.parse import quote, unquote, urljoin, urlsplit @@ -28,11 +30,12 @@ url_for, ) from furl import furl -from pytz import common_timezones, timezone as pytz_timezone, utc +from pytz import timezone as pytz_timezone, utc from werkzeug.exceptions import MethodNotAllowed, NotFound from werkzeug.routing import BuildError, RequestRedirect from baseframe import cache, statsd +from coaster.auth import current_auth from coaster.sqlalchemy import RoleMixin from coaster.utils import utcnow @@ -42,13 +45,22 @@ from ..proxies import request_wants from ..typing import ResponseType, ReturnResponse, ReturnView -valid_timezones = set(common_timezones) - nocache_expires = utc.localize(datetime(1990, 1, 1)) # Six avatar colours defined in _variable.scss avatar_color_count = 6 +# --- Timezone data -------------------------------------------------------------------- + +# Get all known timezones from zoneinfo and make a lowercased lookup table +valid_timezones = {tz.lower(): tz for tz in zoneinfo.available_timezones()} +# Get timezone aliases from tzinfo.zi and place them in the lookup table +with resources.open_text('tzdata.zoneinfo', 'tzdata.zi') as _tzdata: + for _tzline in _tzdata.readlines(): + if _tzline.startswith('L'): + _tzlink, _tznew, _tzold = _tzline.strip().split() + valid_timezones[_tzold.lower()] = _tznew + # --- Classes -------------------------------------------------------------------------- @@ -239,24 +251,25 @@ def get_scheme_netloc(uri: str) -> tuple[str, str]: return (parsed_uri.scheme, parsed_uri.netloc) -def autoset_timezone_and_locale(user: Account) -> None: - # Set the user's timezone and locale automatically if required +def autoset_timezone_and_locale() -> None: + """Set the current user's timezone and locale automatically if required.""" + user = current_auth.user if ( user.auto_timezone - or user.timezone is None - or str(user.timezone) not in valid_timezones + or not user.timezone + or str(user.timezone).lower() not in valid_timezones ): if request.cookies.get('timezone'): - timezone = unquote(request.cookies['timezone']) - if timezone in valid_timezones: - user.timezone = timezone - if ( - user.auto_locale - or user.locale is None - or str(user.locale) not in supported_locales - ): + cookie_timezone = unquote(request.cookies['timezone']).lower() + remapped_timezone = valid_timezones.get(cookie_timezone) + if remapped_timezone is not None: + user.timezone = remapped_timezone # type: ignore[assignment] + if user.auto_locale or not user.locale or str(user.locale) not in supported_locales: user.locale = ( - request.accept_languages.best_match(supported_locales.keys()) or 'en' + request.accept_languages.best_match( # type: ignore[assignment] + supported_locales.keys() + ) + or 'en' ) diff --git a/funnel/views/login_session.py b/funnel/views/login_session.py index 075712dd3..7ab5f3086 100644 --- a/funnel/views/login_session.py +++ b/funnel/views/login_session.py @@ -842,7 +842,7 @@ def login_internal( current_auth.cookie['sessionid'] = login_session.buid current_auth.cookie['userid'] = user.buid session.permanent = True - autoset_timezone_and_locale(user) + autoset_timezone_and_locale() user_login.send(user) diff --git a/requirements/base.in b/requirements/base.in index 174f4290c..3919f51d1 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -63,6 +63,7 @@ toml tweepy twilio typing-extensions +tzdata urllib3[socks] # Not required here, but the [socks] extra shows up in test.txt user-agents werkzeug diff --git a/requirements/base.txt b/requirements/base.txt index 90214bd88..6047c603d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:a6a774ecc26fb3f7ed24d8b2ef66027d9c25ceb2 +# SHA1:8d570f8f7fb4bd607d33ddb31c5b62c51b09ec48 # # This file is autogenerated by pip-compile-multi # To update, run: @@ -21,7 +21,7 @@ aiohttp-retry==2.8.3 # via twilio aiosignal==1.3.1 # via aiohttp -alembic==1.12.0 +alembic==1.12.1 # via # -r requirements/base.in # flask-migrate @@ -39,7 +39,7 @@ async-timeout==4.0.3 # via aiohttp attrs==23.1.0 # via aiohttp -babel==2.13.0 +babel==2.13.1 # via # -r requirements/base.in # flask-babel @@ -61,9 +61,9 @@ blinker==1.6.3 # baseframe # coaster # flask -boto3==1.28.65 +boto3==1.28.72 # via -r requirements/base.in -botocore==1.31.65 +botocore==1.31.72 # via # boto3 # s3transfer @@ -71,7 +71,7 @@ brotli==1.1.0 # via -r requirements/base.in cachelib==0.9.0 # via flask-caching -cachetools==5.3.1 +cachetools==5.3.2 # via premailer certifi==2023.7.22 # via @@ -83,7 +83,7 @@ cffi==1.16.0 # via # argon2-cffi-bindings # cryptography -charset-normalizer==3.3.0 +charset-normalizer==3.3.1 # via # aiohttp # requests @@ -97,7 +97,7 @@ click==8.1.7 # rq crontab==1.0.1 # via rq-scheduler -cryptography==41.0.4 +cryptography==41.0.5 # via -r requirements/base.in cssmin==0.2.0 # via baseframe @@ -334,7 +334,7 @@ pyisemail==2.0.1 # mxsniff pyjwt==2.8.0 # via twilio -pymdown-extensions==10.3 +pymdown-extensions==10.3.1 # via coaster pyparsing==3.1.1 # via httplib2 @@ -472,7 +472,7 @@ tuspy==1.0.1 # via pyvimeo tweepy==4.14.0 # via -r requirements/base.in -twilio==8.9.1 +twilio==8.10.0 # via -r requirements/base.in types-python-dateutil==2.8.19.14 # via arrow @@ -489,6 +489,8 @@ typing-extensions==4.8.0 # typing-inspect typing-inspect==0.9.0 # via dataclasses-json +tzdata==2023.3 + # via -r requirements/base.in ua-parser==0.18.0 # via user-agents uc-micro-py==1.0.2 @@ -509,7 +511,7 @@ webencodings==0.5.1 # via # bleach # html5lib -werkzeug==3.0.0 +werkzeug==3.0.1 # via # -r requirements/base.in # baseframe diff --git a/requirements/dev.txt b/requirements/dev.txt index 25f76152e..9c3a6a5fd 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -16,7 +16,7 @@ astroid==3.0.1 # via pylint bandit==1.7.5 # via -r requirements/dev.in -black==23.10.0 +black==23.10.1 # via -r requirements/dev.in build==1.0.3 # via pip-tools @@ -79,9 +79,9 @@ flask-debugtoolbar==0.13.1 # via -r requirements/dev.in gherkin-official==24.0.0 # via reformat-gherkin -gitdb==4.0.10 +gitdb==4.0.11 # via gitpython -gitpython==3.1.38 +gitpython==3.1.40 # via bandit html-tag-names==0.1.2 # via djlint @@ -141,7 +141,7 @@ pydocstyle==6.3.0 # via flake8-docstrings pyflakes==3.1.0 # via flake8 -pylint==3.0.1 +pylint==3.0.2 # via -r requirements/dev.in pyproject-hooks==1.0.0 # via build @@ -149,7 +149,7 @@ pyupgrade==3.15.0 # via -r requirements/dev.in reformat-gherkin==3.0.1 # via -r requirements/dev.in -ruff==0.1.0 +ruff==0.1.3 # via -r requirements/dev.in smmap==5.0.1 # via gitdb @@ -177,11 +177,11 @@ types-pyopenssl==23.2.0.2 # via types-redis types-pytz==2023.3.1.1 # via -r requirements/dev.in -types-redis==4.6.0.7 +types-redis==4.6.0.8 # via -r requirements/dev.in types-requests==2.31.0.10 # via -r requirements/dev.in -virtualenv==20.24.5 +virtualenv==20.24.6 # via pre-commit wcwidth==0.2.8 # via reformat-gherkin diff --git a/requirements/test.txt b/requirements/test.txt index c9de37b30..facf4d46e 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -27,7 +27,7 @@ docopt==0.6.2 # via coveralls iniconfig==2.0.0 # via pytest -outcome==1.3.0 +outcome==1.3.0.post0 # via trio parse==1.19.1 # via @@ -39,7 +39,7 @@ pluggy==1.3.0 # via pytest py==1.11.0 # via -r requirements/test.in -pytest==7.4.2 +pytest==7.4.3 # via # -r requirements/test.in # pytest-asyncio @@ -64,7 +64,7 @@ pytest-cov==4.1.0 # via -r requirements/test.in pytest-dotenv==0.5.2 # via -r requirements/test.in -pytest-env==1.0.1 +pytest-env==1.1.0 # via -r requirements/test.in pytest-html==4.0.2 # via pytest-selenium diff --git a/tests/unit/views/helpers_test.py b/tests/unit/views/helpers_test.py index c06d70624..296b581df 100644 --- a/tests/unit/views/helpers_test.py +++ b/tests/unit/views/helpers_test.py @@ -41,6 +41,16 @@ def __call__(self, length: int) -> Any: return value +def test_valid_timezones_remap() -> None: + """Confirm valid_timezones has correct mappings for canary timezones.""" + assert '' not in vhelpers.valid_timezones + assert None not in vhelpers.valid_timezones + assert 'asia/kolkata' in vhelpers.valid_timezones + assert 'asia/calcutta' in vhelpers.valid_timezones + assert vhelpers.valid_timezones['asia/kolkata'] == 'Asia/Kolkata' + assert vhelpers.valid_timezones['asia/calcutta'] == 'Asia/Kolkata' + + def test_app_url_for(app, testapp) -> None: """Test that app_url_for works cross-app and in-app.""" # App context is not necessary to use app_url_for