middleware.py 6.21 KB
Newer Older
1 2 3 4
"""
Middleware for Mobile APIs
"""
from datetime import datetime
5

6 7 8 9
from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponse
from pytz import UTC
10 11

import request_cache
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
from mobile_api.mobile_platform import MobilePlatform
from mobile_api.models import AppVersionConfig
from mobile_api.utils import parsed_version
from openedx.core.lib.mobile_utils import is_request_from_mobile_app


class AppVersionUpgrade(object):
    """
    Middleware class to keep track of mobile application version being used.
    """
    LATEST_VERSION_HEADER = 'EDX-APP-LATEST-VERSION'
    LAST_SUPPORTED_DATE_HEADER = 'EDX-APP-VERSION-LAST-SUPPORTED-DATE'
    NO_LAST_SUPPORTED_DATE = 'NO_LAST_SUPPORTED_DATE'
    NO_LATEST_VERSION = 'NO_LATEST_VERSION'
    USER_APP_VERSION = 'USER_APP_VERSION'
    REQUEST_CACHE_NAME = 'app-version-info'
    CACHE_TIMEOUT = settings.APP_UPGRADE_CACHE_TIMEOUT

    def process_request(self, request):
        """
        Processes request to validate app version that is making request.

        Returns:
            Http response with status code 426 (i.e. Update Required) if request is from
            mobile native app and app version is no longer supported else returns None
        """
        version_data = self._get_version_info(request)
        if version_data:
            last_supported_date = version_data[self.LAST_SUPPORTED_DATE_HEADER]
            if last_supported_date != self.NO_LAST_SUPPORTED_DATE:
                if datetime.now().replace(tzinfo=UTC) > last_supported_date:
                    return HttpResponse(status=426)  # Http status 426; Update Required

    def process_response(self, __, response):
        """
        If request is from mobile native app, then add version related info to response headers.

        Returns:
            Http response: with additional headers;
                1. EDX-APP-LATEST-VERSION; if user app version < latest available version
                2. EDX-APP-VERSION-LAST-SUPPORTED-DATE; if user app version < min supported version and
                   timestamp < expiry of that version
        """
        request_cache_dict = request_cache.get_cache(self.REQUEST_CACHE_NAME)
        if request_cache_dict:
            last_supported_date = request_cache_dict[self.LAST_SUPPORTED_DATE_HEADER]
            if last_supported_date != self.NO_LAST_SUPPORTED_DATE:
                response[self.LAST_SUPPORTED_DATE_HEADER] = last_supported_date.isoformat()
            latest_version = request_cache_dict[self.LATEST_VERSION_HEADER]
            user_app_version = request_cache_dict[self.USER_APP_VERSION]
            if (latest_version != self.NO_LATEST_VERSION and
                    parsed_version(user_app_version) < parsed_version(latest_version)):
                response[self.LATEST_VERSION_HEADER] = latest_version
        return response

    def _get_cache_key_name(self, field, key):
        """
        Get key name to use to cache any property against field name and identification key.

        Arguments:
            field (str): The property name that needs to get cached.
            key (str): Unique identification for cache key (e.g. platform_name).

        Returns:
            string: Cache key to be used.
        """
        return "mobile_api.app_version_upgrade.{}.{}".format(field, key)

    def _get_version_info(self, request):
        """
        Gets and Sets version related info in mem cache and request cache; and returns a dict of it.

        It sets request cache data for last_supported_date and latest_version with memcached values if exists against
        user app properties else computes the values for specific platform and sets it in both memcache (for next
        server interaction from same app version/platform) and request cache

        Returns:
            dict: Containing app version info
        """
        user_agent = request.META.get('HTTP_USER_AGENT')
        if user_agent:
            platform = self._get_platform(request, user_agent)
            if platform:
                request_cache_dict = request_cache.get_cache(self.REQUEST_CACHE_NAME)
                request_cache_dict[self.USER_APP_VERSION] = platform.version
                last_supported_date_cache_key = self._get_cache_key_name(
                    self.LAST_SUPPORTED_DATE_HEADER,
                    platform.version
                )
                latest_version_cache_key = self._get_cache_key_name(self.LATEST_VERSION_HEADER, platform.NAME)
                cached_data = cache.get_many([last_supported_date_cache_key, latest_version_cache_key])

                last_supported_date = cached_data.get(last_supported_date_cache_key)
                if not last_supported_date:
                    last_supported_date = self._get_last_supported_date(platform.NAME, platform.version)
                    cache.set(last_supported_date_cache_key, last_supported_date, self.CACHE_TIMEOUT)
                request_cache_dict[self.LAST_SUPPORTED_DATE_HEADER] = last_supported_date

                latest_version = cached_data.get(latest_version_cache_key)
                if not latest_version:
                    latest_version = self._get_latest_version(platform.NAME)
                    cache.set(latest_version_cache_key, latest_version, self.CACHE_TIMEOUT)
                request_cache_dict[self.LATEST_VERSION_HEADER] = latest_version

                return request_cache_dict

    def _get_platform(self, request, user_agent):
        """
        Determines the platform type for mobile app making the request against user_agent.

        Returns:
            None if request app does not belong to one of the supported mobile platforms
            else returns an instance of corresponding mobile platform.
        """
        if is_request_from_mobile_app(request):
            return MobilePlatform.get_instance(user_agent)

    def _get_last_supported_date(self, platform_name, platform_version):
        """ Get expiry date of app version for a platform. """
        return AppVersionConfig.last_supported_date(platform_name, platform_version) or self.NO_LAST_SUPPORTED_DATE

    def _get_latest_version(self, platform_name):
        """ Get latest app version available for platform. """
        return AppVersionConfig.latest_version(platform_name) or self.NO_LATEST_VERSION