compatibility: Shift functions to new module.

Shift functions used for compatibility from
zerver.lib.home (is_outdated_server) and
zerver.view.compatibility (pop_numerals,
version_lt, find_mobile_os,
is_outdated_desktop_app, is_unsupported_browser)
to zerver.lib.compatibility module.
This commit is contained in:
Gaurav Pandey
2021-06-04 13:49:50 +05:30
committed by Tim Abbott
parent fcff3cc5da
commit f82aba5a3d
6 changed files with 153 additions and 148 deletions

141
zerver/lib/compatibility.py Normal file
View File

@@ -0,0 +1,141 @@
import datetime
import os
import re
from typing import List, Optional, Tuple
import pytz
from django.conf import settings
from django.utils.timezone import now as timezone_now
from version import DESKTOP_MINIMUM_VERSION, DESKTOP_WARNING_VERSION
from zerver.lib.user_agent import parse_user_agent
from zerver.models import UserProfile
from zerver.signals import get_device_browser
# LAST_SERVER_UPGRADE_TIME is the last time the server had a version deployed.
if settings.PRODUCTION: # nocoverage
timestamp = os.path.basename(os.path.abspath(settings.DEPLOY_ROOT))
LAST_SERVER_UPGRADE_TIME = datetime.datetime.strptime(timestamp, "%Y-%m-%d-%H-%M-%S").replace(
tzinfo=pytz.utc
)
else:
LAST_SERVER_UPGRADE_TIME = timezone_now()
def is_outdated_server(user_profile: Optional[UserProfile]) -> bool:
# Release tarballs are unpacked via `tar -xf`, which means the
# `mtime` on files in them is preserved from when the release
# tarball was built. Checking this allows us to catch cases where
# someone has upgraded in the last year but to a release more than
# a year old.
git_version_path = os.path.join(settings.DEPLOY_ROOT, "version.py")
release_build_time = datetime.datetime.utcfromtimestamp(
os.path.getmtime(git_version_path)
).replace(tzinfo=pytz.utc)
version_no_newer_than = min(LAST_SERVER_UPGRADE_TIME, release_build_time)
deadline = version_no_newer_than + datetime.timedelta(
days=settings.SERVER_UPGRADE_NAG_DEADLINE_DAYS
)
if user_profile is None or not user_profile.is_realm_admin:
# Administrators get warned at the deadline; all users 30 days later.
deadline = deadline + datetime.timedelta(days=30)
if timezone_now() > deadline:
return True
return False
def pop_numerals(ver: str) -> Tuple[List[int], str]:
match = re.search(r"^( \d+ (?: \. \d+ )* ) (.*)", ver, re.X)
if match is None:
return [], ver
numerals, rest = match.groups()
numbers = [int(n) for n in numerals.split(".")]
return numbers, rest
def version_lt(ver1: str, ver2: str) -> Optional[bool]:
"""
Compare two Zulip-style version strings.
Versions are dot-separated sequences of decimal integers,
followed by arbitrary trailing decoration. Comparison is
lexicographic on the integer sequences, and refuses to
guess how any trailing decoration compares to any other,
to further numerals, or to nothing.
Returns:
True if ver1 < ver2
False if ver1 >= ver2
None if can't tell.
"""
num1, rest1 = pop_numerals(ver1)
num2, rest2 = pop_numerals(ver2)
if not num1 or not num2:
return None
common_len = min(len(num1), len(num2))
common_num1, rest_num1 = num1[:common_len], num1[common_len:]
common_num2, rest_num2 = num2[:common_len], num2[common_len:]
# Leading numbers win.
if common_num1 != common_num2:
return common_num1 < common_num2
# More numbers beats end-of-string, but ??? vs trailing text.
# (NB at most one of rest_num1, rest_num2 is nonempty.)
if not rest1 and rest_num2:
return True
if rest_num1 and not rest2:
return False
if rest_num1 or rest_num2:
return None
# Trailing text we can only compare for equality.
if rest1 == rest2:
return False
return None
def find_mobile_os(user_agent: str) -> Optional[str]:
if re.search(r"\b Android \b", user_agent, re.I | re.X):
return "android"
if re.search(r"\b(?: iOS | iPhone\ OS )\b", user_agent, re.I | re.X):
return "ios"
return None
def is_outdated_desktop_app(user_agent_str: str) -> Tuple[bool, bool, bool]:
# Returns (insecure, banned, auto_update_broken)
user_agent = parse_user_agent(user_agent_str)
if user_agent["name"] == "ZulipDesktop":
# The deprecated QT/webkit based desktop app, last updated in ~2016.
return (True, True, True)
if user_agent["name"] != "ZulipElectron":
return (False, False, False)
if version_lt(user_agent["version"], "4.0.0"):
# Version 2.3.82 and older (aka <4.0.0) of the modern
# Electron-based Zulip desktop app with known security issues.
# won't auto-update; we may want a special notice to
# distinguish those from modern releases.
return (True, True, True)
if version_lt(user_agent["version"], DESKTOP_MINIMUM_VERSION):
# Below DESKTOP_MINIMUM_VERSION, we reject access as well.
return (True, True, False)
if version_lt(user_agent["version"], DESKTOP_WARNING_VERSION):
# Other insecure versions should just warn.
return (True, False, False)
return (False, False, False)
def is_unsupported_browser(user_agent: str) -> Tuple[bool, Optional[str]]:
browser_name = get_device_browser(user_agent)
if browser_name == "Internet Explorer":
return (True, browser_name)
return (False, browser_name)

View File

@@ -1,17 +1,14 @@
import calendar import calendar
import datetime
import os
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import pytz
from django.conf import settings from django.conf import settings
from django.http import HttpRequest from django.http import HttpRequest
from django.utils import translation from django.utils import translation
from django.utils.timezone import now as timezone_now
from two_factor.utils import default_device from two_factor.utils import default_device
from zerver.lib.compatibility import is_outdated_server
from zerver.lib.events import do_events_register from zerver.lib.events import do_events_register
from zerver.lib.i18n import ( from zerver.lib.i18n import (
get_and_set_request_language, get_and_set_request_language,
@@ -39,41 +36,6 @@ class UserPermissionInfo:
show_webathena: bool show_webathena: bool
# LAST_SERVER_UPGRADE_TIME is the last time the server had a version deployed.
if settings.PRODUCTION: # nocoverage
timestamp = os.path.basename(os.path.abspath(settings.DEPLOY_ROOT))
LAST_SERVER_UPGRADE_TIME = datetime.datetime.strptime(timestamp, "%Y-%m-%d-%H-%M-%S").replace(
tzinfo=pytz.utc
)
else:
LAST_SERVER_UPGRADE_TIME = timezone_now()
def is_outdated_server(user_profile: Optional[UserProfile]) -> bool:
# Release tarballs are unpacked via `tar -xf`, which means the
# `mtime` on files in them is preserved from when the release
# tarball was built. Checking this allows us to catch cases where
# someone has upgraded in the last year but to a release more than
# a year old.
git_version_path = os.path.join(settings.DEPLOY_ROOT, "version.py")
release_build_time = datetime.datetime.utcfromtimestamp(
os.path.getmtime(git_version_path)
).replace(tzinfo=pytz.utc)
version_no_newer_than = min(LAST_SERVER_UPGRADE_TIME, release_build_time)
deadline = version_no_newer_than + datetime.timedelta(
days=settings.SERVER_UPGRADE_NAG_DEADLINE_DAYS
)
if user_profile is None or not user_profile.is_realm_admin:
# Administrators get warned at the deadline; all users 30 days later.
deadline = deadline + datetime.timedelta(days=30)
if timezone_now() > deadline:
return True
return False
def get_furthest_read_time(user_profile: Optional[UserProfile]) -> Optional[float]: def get_furthest_read_time(user_profile: Optional[UserProfile]) -> Optional[float]:
if user_profile is None: if user_profile is None:
return time.time() return time.time()

View File

@@ -1,7 +1,7 @@
from unittest import mock from unittest import mock
from zerver.lib.compatibility import find_mobile_os, is_outdated_desktop_app, version_lt
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.views.compatibility import find_mobile_os, is_outdated_desktop_app, version_lt
class VersionTest(ZulipTestCase): class VersionTest(ZulipTestCase):
@@ -109,8 +109,8 @@ class CompatibilityTest(ZulipTestCase):
else: else:
assert False # nocoverage assert False # nocoverage
@mock.patch("zerver.views.compatibility.DESKTOP_MINIMUM_VERSION", "5.0.0") @mock.patch("zerver.lib.compatibility.DESKTOP_MINIMUM_VERSION", "5.0.0")
@mock.patch("zerver.views.compatibility.DESKTOP_WARNING_VERSION", "5.2.0") @mock.patch("zerver.lib.compatibility.DESKTOP_WARNING_VERSION", "5.2.0")
def test_insecure_desktop_app(self) -> None: def test_insecure_desktop_app(self) -> None:
self.assertEqual(is_outdated_desktop_app("ZulipDesktop/0.5.2 (Mac)"), (True, True, True)) self.assertEqual(is_outdated_desktop_app("ZulipDesktop/0.5.2 (Mac)"), (True, True, True))
self.assertEqual( self.assertEqual(
@@ -134,7 +134,7 @@ class CompatibilityTest(ZulipTestCase):
) )
# Verify what happens if DESKTOP_MINIMUM_VERSION < v < DESKTOP_WARNING_VERSION # Verify what happens if DESKTOP_MINIMUM_VERSION < v < DESKTOP_WARNING_VERSION
with mock.patch("zerver.views.compatibility.DESKTOP_MINIMUM_VERSION", "4.0.3"): with mock.patch("zerver.lib.compatibility.DESKTOP_MINIMUM_VERSION", "4.0.3"):
self.assertEqual( self.assertEqual(
is_outdated_desktop_app( is_outdated_desktop_app(
"ZulipElectron/4.0.3 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36" "ZulipElectron/4.0.3 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36"

View File

@@ -17,8 +17,9 @@ from zerver.lib.actions import (
do_change_plan_type, do_change_plan_type,
do_create_user, do_create_user,
) )
from zerver.lib.compatibility import LAST_SERVER_UPGRADE_TIME, is_outdated_server
from zerver.lib.events import add_realm_logo_fields from zerver.lib.events import add_realm_logo_fields
from zerver.lib.home import LAST_SERVER_UPGRADE_TIME, get_furthest_read_time, is_outdated_server from zerver.lib.home import get_furthest_read_time
from zerver.lib.soft_deactivation import do_soft_deactivate_users from zerver.lib.soft_deactivation import do_soft_deactivate_users
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import get_user_messages, override_settings, queries_captured from zerver.lib.test_helpers import get_user_messages, override_settings, queries_captured
@@ -896,17 +897,17 @@ class HomeTest(ZulipTestCase):
hamlet = self.example_user("hamlet") hamlet = self.example_user("hamlet")
iago = self.example_user("iago") iago = self.example_user("iago")
now = LAST_SERVER_UPGRADE_TIME.replace(tzinfo=pytz.utc) now = LAST_SERVER_UPGRADE_TIME.replace(tzinfo=pytz.utc)
with patch("zerver.lib.home.timezone_now", return_value=now + timedelta(days=10)): with patch("zerver.lib.compatibility.timezone_now", return_value=now + timedelta(days=10)):
self.assertEqual(is_outdated_server(iago), False) self.assertEqual(is_outdated_server(iago), False)
self.assertEqual(is_outdated_server(hamlet), False) self.assertEqual(is_outdated_server(hamlet), False)
self.assertEqual(is_outdated_server(None), False) self.assertEqual(is_outdated_server(None), False)
with patch("zerver.lib.home.timezone_now", return_value=now + timedelta(days=397)): with patch("zerver.lib.compatibility.timezone_now", return_value=now + timedelta(days=397)):
self.assertEqual(is_outdated_server(iago), True) self.assertEqual(is_outdated_server(iago), True)
self.assertEqual(is_outdated_server(hamlet), True) self.assertEqual(is_outdated_server(hamlet), True)
self.assertEqual(is_outdated_server(None), True) self.assertEqual(is_outdated_server(None), True)
with patch("zerver.lib.home.timezone_now", return_value=now + timedelta(days=380)): with patch("zerver.lib.compatibility.timezone_now", return_value=now + timedelta(days=380)):
self.assertEqual(is_outdated_server(iago), True) self.assertEqual(is_outdated_server(iago), True)
self.assertEqual(is_outdated_server(hamlet), False) self.assertEqual(is_outdated_server(hamlet), False)
self.assertEqual(is_outdated_server(None), False) self.assertEqual(is_outdated_server(None), False)

View File

@@ -1,73 +1,9 @@
import re
from typing import List, Optional, Tuple
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from version import DESKTOP_MINIMUM_VERSION, DESKTOP_WARNING_VERSION from zerver.lib.compatibility import find_mobile_os, version_lt
from zerver.lib.response import json_error, json_success from zerver.lib.response import json_error, json_success
from zerver.lib.user_agent import parse_user_agent from zerver.lib.user_agent import parse_user_agent
from zerver.signals import get_device_browser
def pop_numerals(ver: str) -> Tuple[List[int], str]:
match = re.search(r"^( \d+ (?: \. \d+ )* ) (.*)", ver, re.X)
if match is None:
return [], ver
numerals, rest = match.groups()
numbers = [int(n) for n in numerals.split(".")]
return numbers, rest
def version_lt(ver1: str, ver2: str) -> Optional[bool]:
"""
Compare two Zulip-style version strings.
Versions are dot-separated sequences of decimal integers,
followed by arbitrary trailing decoration. Comparison is
lexicographic on the integer sequences, and refuses to
guess how any trailing decoration compares to any other,
to further numerals, or to nothing.
Returns:
True if ver1 < ver2
False if ver1 >= ver2
None if can't tell.
"""
num1, rest1 = pop_numerals(ver1)
num2, rest2 = pop_numerals(ver2)
if not num1 or not num2:
return None
common_len = min(len(num1), len(num2))
common_num1, rest_num1 = num1[:common_len], num1[common_len:]
common_num2, rest_num2 = num2[:common_len], num2[common_len:]
# Leading numbers win.
if common_num1 != common_num2:
return common_num1 < common_num2
# More numbers beats end-of-string, but ??? vs trailing text.
# (NB at most one of rest_num1, rest_num2 is nonempty.)
if not rest1 and rest_num2:
return True
if rest_num1 and not rest2:
return False
if rest_num1 or rest_num2:
return None
# Trailing text we can only compare for equality.
if rest1 == rest2:
return False
return None
def find_mobile_os(user_agent: str) -> Optional[str]:
if re.search(r"\b Android \b", user_agent, re.I | re.X):
return "android"
if re.search(r"\b(?: iOS | iPhone\ OS )\b", user_agent, re.I | re.X):
return "ios"
return None
# Zulip Mobile release 16.2.96 was made 2018-08-22. It fixed a # Zulip Mobile release 16.2.96 was made 2018-08-22. It fixed a
# bug in our Android code that causes spammy, obviously-broken # bug in our Android code that causes spammy, obviously-broken
@@ -91,38 +27,3 @@ def check_global_compatibility(request: HttpRequest) -> HttpResponse:
if user_os == "android" and version_lt(user_agent["version"], android_min_app_version): if user_os == "android" and version_lt(user_agent["version"], android_min_app_version):
return json_error(legacy_compatibility_error_message) return json_error(legacy_compatibility_error_message)
return json_success() return json_success()
def is_outdated_desktop_app(user_agent_str: str) -> Tuple[bool, bool, bool]:
# Returns (insecure, banned, auto_update_broken)
user_agent = parse_user_agent(user_agent_str)
if user_agent["name"] == "ZulipDesktop":
# The deprecated QT/webkit based desktop app, last updated in ~2016.
return (True, True, True)
if user_agent["name"] != "ZulipElectron":
return (False, False, False)
if version_lt(user_agent["version"], "4.0.0"):
# Version 2.3.82 and older (aka <4.0.0) of the modern
# Electron-based Zulip desktop app with known security issues.
# won't auto-update; we may want a special notice to
# distinguish those from modern releases.
return (True, True, True)
if version_lt(user_agent["version"], DESKTOP_MINIMUM_VERSION):
# Below DESKTOP_MINIMUM_VERSION, we reject access as well.
return (True, True, False)
if version_lt(user_agent["version"], DESKTOP_WARNING_VERSION):
# Other insecure versions should just warn.
return (True, False, False)
return (False, False, False)
def is_unsupported_browser(user_agent: str) -> Tuple[bool, Optional[str]]:
browser_name = get_device_browser(user_agent)
if browser_name == "Internet Explorer":
return (True, browser_name)
return (False, browser_name)

View File

@@ -13,6 +13,7 @@ from zerver.context_processors import get_valid_realm_from_request
from zerver.decorator import web_public_view, zulip_login_required from zerver.decorator import web_public_view, zulip_login_required
from zerver.forms import ToSForm from zerver.forms import ToSForm
from zerver.lib.actions import do_change_tos_version, realm_user_count from zerver.lib.actions import do_change_tos_version, realm_user_count
from zerver.lib.compatibility import is_outdated_desktop_app, is_unsupported_browser
from zerver.lib.home import ( from zerver.lib.home import (
build_page_params_for_home_page_load, build_page_params_for_home_page_load,
get_billing_info, get_billing_info,
@@ -24,7 +25,6 @@ from zerver.lib.subdomains import get_subdomain
from zerver.lib.users import compute_show_invites_and_add_streams from zerver.lib.users import compute_show_invites_and_add_streams
from zerver.lib.utils import statsd from zerver.lib.utils import statsd
from zerver.models import PreregistrationUser, Realm, Stream, UserProfile from zerver.models import PreregistrationUser, Realm, Stream, UserProfile
from zerver.views.compatibility import is_outdated_desktop_app, is_unsupported_browser
from zerver.views.portico import hello_view from zerver.views.portico import hello_view