We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Hashi no hash-y
### Observed behavior
Currently we build hashi without a build hash. This means that in the future if we upgrade our Hashi functionality, this may not get cache busted.

### Expected behavior
New versions of Hashi should cache bust old versions!

### User-facing consequences
Weird, unpredictable Hashi behaviour, depending on previous use.
Not possible to 'complete' an HTML5 App
### Observed behavior
In introducing Hashi, the 15s insta-points was removed from HTML5 apps. Now impossible to register any progress.

### Expected behavior
What should progress look like for an arbitrary HTML5 app?

### User-facing consequences
Confusion?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kolibri/core/content/views.py`
Content:
```
1 import mimetypes
2 import os
3 import zipfile
4 
5 from django.core.cache import cache
6 from django.http import Http404
7 from django.http import HttpResponse
8 from django.http.response import FileResponse
9 from django.http.response import HttpResponseNotModified
10 from django.urls import reverse
11 from django.template import loader
12 from django.views.decorators.clickjacking import xframe_options_exempt
13 from django.views.decorators.vary import vary_on_headers
14 from django.views.generic.base import View
15 from le_utils.constants import exercises
16 from six.moves.urllib.parse import urlparse
17 from six.moves.urllib.parse import urlunparse
18 
19 from .api import cache_forever
20 from .utils.paths import get_content_storage_file_path
21 from kolibri.utils.conf import OPTIONS
22 
23 # Do this to prevent import of broken Windows filetype registry that makes guesstype not work.
24 # https://www.thecodingforums.com/threads/mimetypes-guess_type-broken-in-windows-on-py2-7-and-python-3-x.952693/
25 mimetypes.init([os.path.join(os.path.dirname(__file__), 'constants', 'mime.types')])
26 
27 
28 def _add_access_control_headers(request, response):
29     response["Access-Control-Allow-Origin"] = "*"
30     response["Access-Control-Allow-Methods"] = "GET, OPTIONS"
31     requested_headers = request.META.get("HTTP_ACCESS_CONTROL_REQUEST_HEADERS", "")
32     if requested_headers:
33         response["Access-Control-Allow-Headers"] = requested_headers
34 
35 
36 def get_referrer_url(request):
37     if request.META.get('HTTP_REFERER'):
38         # If available use HTTP_REFERER to infer the host as that will give us more
39         # information if Kolibri is behind a proxy.
40         return urlparse(request.META.get('HTTP_REFERER'))
41 
42 
43 def generate_image_prefix_url(request, zipped_filename):
44     parsed_referrer_url = get_referrer_url(request)
45     # Remove trailing slash
46     zipcontent = reverse(
47         'kolibri:core:zipcontent',
48         kwargs={
49             "zipped_filename": zipped_filename,
50             "embedded_filepath": ''
51         })[:-1]
52     if parsed_referrer_url:
53         # Reconstruct the parsed URL using a blank scheme and host + port(1)
54         zipcontent = urlunparse(('', parsed_referrer_url[1], zipcontent, '', '', ''))
55     return zipcontent.encode()
56 
57 
58 def get_host(request):
59     parsed_referrer_url = get_referrer_url(request)
60     if parsed_referrer_url:
61         host = urlunparse((parsed_referrer_url[0], parsed_referrer_url[1], '', '', '', ''))
62     else:
63         host = request.build_absolute_uri(OPTIONS['Deployment']['URL_PATH_PREFIX'])
64     return host.strip("/")
65 
66 
67 def _add_content_security_policy_header(request, response):
68     # restrict CSP to only allow resources to be loaded from the Kolibri host, to prevent info leakage
69     # (e.g. via passing user info out as GET parameters to an attacker's server), or inadvertent data usage
70     host = get_host(request)
71     response["Content-Security-Policy"] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: " + host
72 
73 
74 class ZipContentView(View):
75 
76     @xframe_options_exempt
77     def options(self, request, *args, **kwargs):
78         """
79         Handles OPTIONS requests which may be sent as "preflight CORS" requests to check permissions.
80         """
81         response = HttpResponse()
82         _add_access_control_headers(request, response)
83         return response
84 
85     @vary_on_headers('X-Requested-With')
86     @cache_forever
87     @xframe_options_exempt
88     def get(self, request, zipped_filename, embedded_filepath):
89         """
90         Handles GET requests and serves a static file from within the zip file.
91         """
92 
93         # calculate the local file path to the zip file
94         zipped_path = get_content_storage_file_path(zipped_filename)
95 
96         # file size
97         file_size = 0
98 
99         # if the zipfile does not exist on disk, return a 404
100         if not os.path.exists(zipped_path):
101             raise Http404('"%(filename)s" does not exist locally' % {'filename': zipped_filename})
102 
103         # if client has a cached version, use that (we can safely assume nothing has changed, due to MD5)
104         if request.META.get('HTTP_IF_MODIFIED_SINCE'):
105             return HttpResponseNotModified()
106 
107         with zipfile.ZipFile(zipped_path) as zf:
108 
109             # if no path, or a directory, is being referenced, look for an index.html file
110             if not embedded_filepath or embedded_filepath.endswith("/"):
111                 embedded_filepath += "index.html"
112 
113             # get the details about the embedded file, and ensure it exists
114             try:
115                 info = zf.getinfo(embedded_filepath)
116             except KeyError:
117                 raise Http404('"{}" does not exist inside "{}"'.format(embedded_filepath, zipped_filename))
118 
119             if (not request.is_ajax()) and zipped_path.endswith('zip') and (embedded_filepath.endswith('htm') or embedded_filepath.endswith('html')):
120                 # Sets up our HTML5 zip file endpoint on Kolibri to serve up a
121                 # special template that loads Hashi and then initializes it.
122                 # Only do this when the request is not AJAX, as Hashi will fetch
123                 # the real HTML file using an AJAX request, and presumably other
124                 # dynamic loading of HTML content would also get confused if it
125                 # got the special Hashi template back instead!
126                 cache_key = 'hashi_bootstrap_html'
127                 bootstrap_content = cache.get(cache_key)
128                 if bootstrap_content is None:
129                     template = loader.get_template('content/hashi.html')
130                     bootstrap_content = template.render({}, None)
131                     cache.set(cache_key, bootstrap_content)
132                 response = HttpResponse(bootstrap_content)
133                 _add_access_control_headers(request, response)
134                 _add_content_security_policy_header(request, response)
135                 return response
136 
137             # try to guess the MIME type of the embedded file being referenced
138             content_type = mimetypes.guess_type(embedded_filepath)[0] or 'application/octet-stream'
139 
140             if not os.path.splitext(embedded_filepath)[1] == '.json':
141                 # generate a streaming response object, pulling data from within the zip  file
142                 response = FileResponse(zf.open(info), content_type=content_type)
143                 file_size = info.file_size
144             else:
145                 image_prefix_url = generate_image_prefix_url(request, zipped_filename)
146                 # load the stream from json file into memory, replace the path_place_holder.
147                 content = zf.open(info).read()
148                 str_to_be_replaced = ('$' + exercises.IMG_PLACEHOLDER).encode()
149                 content_with_path = content.replace(str_to_be_replaced, image_prefix_url)
150                 response = HttpResponse(content_with_path, content_type=content_type)
151                 file_size = len(content_with_path)
152 
153         # set the content-length header to the size of the embedded file
154         if info.file_size:
155             response["Content-Length"] = file_size
156 
157         # ensure the browser knows not to try byte-range requests, as we don't support them here
158         response["Accept-Ranges"] = "none"
159 
160         # add headers to ensure AJAX requests will be permitted for these files, even from a null origin
161         _add_access_control_headers(request, response)
162         _add_content_security_policy_header(request, response)
163 
164         return response
165 
166 
167 class DownloadContentView(View):
168 
169     def get(self, request, filename, new_filename):
170         """
171         Handles GET requests and serves a static file as an attachment.
172         """
173 
174         # calculate the local file path of the file
175         path = get_content_storage_file_path(filename)
176 
177         # if the file does not exist on disk, return a 404
178         if not os.path.exists(path):
179             raise Http404('"%(filename)s" does not exist locally' % {'filename': filename})
180 
181         # generate a file response
182         response = FileResponse(open(path, 'rb'))
183 
184         # set the content-type by guessing from the filename
185         response['Content-Type'] = mimetypes.guess_type(filename)[0]
186 
187         # set the content-disposition as attachment to force download
188         response['Content-Disposition'] = 'attachment;'
189 
190         # set the content-length to the file size
191         response['Content-Length'] = os.path.getsize(path)
192 
193         return response
194
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/kolibri/core/content/views.py b/kolibri/core/content/views.py
--- a/kolibri/core/content/views.py
+++ b/kolibri/core/content/views.py
@@ -1,15 +1,20 @@
+import hashlib
+import io
 import mimetypes
 import os
 import zipfile
 
+from django.conf import settings
 from django.core.cache import cache
 from django.http import Http404
 from django.http import HttpResponse
 from django.http.response import FileResponse
 from django.http.response import HttpResponseNotModified
-from django.urls import reverse
 from django.template import loader
+from django.urls import reverse
+from django.utils.decorators import method_decorator
 from django.views.decorators.clickjacking import xframe_options_exempt
+from django.views.decorators.http import etag
 from django.views.decorators.vary import vary_on_headers
 from django.views.generic.base import View
 from le_utils.constants import exercises
@@ -24,6 +29,16 @@
 # https://www.thecodingforums.com/threads/mimetypes-guess_type-broken-in-windows-on-py2-7-and-python-3-x.952693/
 mimetypes.init([os.path.join(os.path.dirname(__file__), 'constants', 'mime.types')])
 
+HASHI_FILENAME = None
+
+
+def get_hashi_filename():
+    global HASHI_FILENAME
+    if HASHI_FILENAME is None or getattr(settings, 'DEVELOPER_MODE', None):
+        with io.open(os.path.join(os.path.dirname(__file__), './build/hashi_filename'), mode='r', encoding='utf-8') as f:
+            HASHI_FILENAME = f.read().strip()
+    return HASHI_FILENAME
+
 
 def _add_access_control_headers(request, response):
     response["Access-Control-Allow-Origin"] = "*"
@@ -71,6 +86,21 @@
     response["Content-Security-Policy"] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: " + host
 
 
+def calculate_zip_content_etag(request, zipped_filename, embedded_filepath):
+    zipped_path = get_content_storage_file_path(zipped_filename)
+
+    # if no path, or a directory, is being referenced, look for an index.html file
+    if not embedded_filepath or embedded_filepath.endswith("/"):
+        embedded_filepath += "index.html"
+
+    # Are we returning the Hashi bootstrap html? In which case the etag should change
+    # along with the built file asset of the Hashi client library.
+    if (not request.is_ajax()) and zipped_path.endswith('zip') and (embedded_filepath.endswith('htm') or embedded_filepath.endswith('html')):
+        return hashlib.md5(get_hashi_filename().encode('utf-8')).hexdigest()
+
+    return hashlib.md5((zipped_filename + embedded_filepath).encode('utf-8')).hexdigest()
+
+
 class ZipContentView(View):
 
     @xframe_options_exempt
@@ -85,6 +115,7 @@
     @vary_on_headers('X-Requested-With')
     @cache_forever
     @xframe_options_exempt
+    @method_decorator(etag(calculate_zip_content_etag))
     def get(self, request, zipped_filename, embedded_filepath):
         """
         Handles GET requests and serves a static file from within the zip file.
@@ -127,7 +158,8 @@
                 bootstrap_content = cache.get(cache_key)
                 if bootstrap_content is None:
                     template = loader.get_template('content/hashi.html')
-                    bootstrap_content = template.render({}, None)
+                    hashi_path = "content/{filename}".format(filename=get_hashi_filename())
+                    bootstrap_content = template.render({"hashi_path": hashi_path}, None)
                     cache.set(cache_key, bootstrap_content)
                 response = HttpResponse(bootstrap_content)
                 _add_access_control_headers(request, response)

{"golden_diff": "diff --git a/kolibri/core/content/views.py b/kolibri/core/content/views.py\n--- a/kolibri/core/content/views.py\n+++ b/kolibri/core/content/views.py\n@@ -1,15 +1,20 @@\n+import hashlib\n+import io\n import mimetypes\n import os\n import zipfile\n \n+from django.conf import settings\n from django.core.cache import cache\n from django.http import Http404\n from django.http import HttpResponse\n from django.http.response import FileResponse\n from django.http.response import HttpResponseNotModified\n-from django.urls import reverse\n from django.template import loader\n+from django.urls import reverse\n+from django.utils.decorators import method_decorator\n from django.views.decorators.clickjacking import xframe_options_exempt\n+from django.views.decorators.http import etag\n from django.views.decorators.vary import vary_on_headers\n from django.views.generic.base import View\n from le_utils.constants import exercises\n@@ -24,6 +29,16 @@\n # https://www.thecodingforums.com/threads/mimetypes-guess_type-broken-in-windows-on-py2-7-and-python-3-x.952693/\n mimetypes.init([os.path.join(os.path.dirname(__file__), 'constants', 'mime.types')])\n \n+HASHI_FILENAME = None\n+\n+\n+def get_hashi_filename():\n+    global HASHI_FILENAME\n+    if HASHI_FILENAME is None or getattr(settings, 'DEVELOPER_MODE', None):\n+        with io.open(os.path.join(os.path.dirname(__file__), './build/hashi_filename'), mode='r', encoding='utf-8') as f:\n+            HASHI_FILENAME = f.read().strip()\n+    return HASHI_FILENAME\n+\n \n def _add_access_control_headers(request, response):\n     response[\"Access-Control-Allow-Origin\"] = \"*\"\n@@ -71,6 +86,21 @@\n     response[\"Content-Security-Policy\"] = \"default-src 'self' 'unsafe-inline' 'unsafe-eval' data: \" + host\n \n \n+def calculate_zip_content_etag(request, zipped_filename, embedded_filepath):\n+    zipped_path = get_content_storage_file_path(zipped_filename)\n+\n+    # if no path, or a directory, is being referenced, look for an index.html file\n+    if not embedded_filepath or embedded_filepath.endswith(\"/\"):\n+        embedded_filepath += \"index.html\"\n+\n+    # Are we returning the Hashi bootstrap html? In which case the etag should change\n+    # along with the built file asset of the Hashi client library.\n+    if (not request.is_ajax()) and zipped_path.endswith('zip') and (embedded_filepath.endswith('htm') or embedded_filepath.endswith('html')):\n+        return hashlib.md5(get_hashi_filename().encode('utf-8')).hexdigest()\n+\n+    return hashlib.md5((zipped_filename + embedded_filepath).encode('utf-8')).hexdigest()\n+\n+\n class ZipContentView(View):\n \n     @xframe_options_exempt\n@@ -85,6 +115,7 @@\n     @vary_on_headers('X-Requested-With')\n     @cache_forever\n     @xframe_options_exempt\n+    @method_decorator(etag(calculate_zip_content_etag))\n     def get(self, request, zipped_filename, embedded_filepath):\n         \"\"\"\n         Handles GET requests and serves a static file from within the zip file.\n@@ -127,7 +158,8 @@\n                 bootstrap_content = cache.get(cache_key)\n                 if bootstrap_content is None:\n                     template = loader.get_template('content/hashi.html')\n-                    bootstrap_content = template.render({}, None)\n+                    hashi_path = \"content/{filename}\".format(filename=get_hashi_filename())\n+                    bootstrap_content = template.render({\"hashi_path\": hashi_path}, None)\n                     cache.set(cache_key, bootstrap_content)\n                 response = HttpResponse(bootstrap_content)\n                 _add_access_control_headers(request, response)\n", "issue": "Hashi no hash-y\n### Observed behavior\r\nCurrently we build hashi without a build hash. This means that in the future if we upgrade our Hashi functionality, this may not get cache busted.\r\n\r\n### Expected behavior\r\nNew versions of Hashi should cache bust old versions!\r\n\r\n### User-facing consequences\r\nWeird, unpredictable Hashi behaviour, depending on previous use.\nNot possible to 'complete' an HTML5 App\n### Observed behavior\r\nIn introducing Hashi, the 15s insta-points was removed from HTML5 apps. Now impossible to register any progress.\r\n\r\n### Expected behavior\r\nWhat should progress look like for an arbitrary HTML5 app?\r\n\r\n### User-facing consequences\r\nConfusion?\n", "before_files": [{"content": "import mimetypes\nimport os\nimport zipfile\n\nfrom django.core.cache import cache\nfrom django.http import Http404\nfrom django.http import HttpResponse\nfrom django.http.response import FileResponse\nfrom django.http.response import HttpResponseNotModified\nfrom django.urls import reverse\nfrom django.template import loader\nfrom django.views.decorators.clickjacking import xframe_options_exempt\nfrom django.views.decorators.vary import vary_on_headers\nfrom django.views.generic.base import View\nfrom le_utils.constants import exercises\nfrom six.moves.urllib.parse import urlparse\nfrom six.moves.urllib.parse import urlunparse\n\nfrom .api import cache_forever\nfrom .utils.paths import get_content_storage_file_path\nfrom kolibri.utils.conf import OPTIONS\n\n# Do this to prevent import of broken Windows filetype registry that makes guesstype not work.\n# https://www.thecodingforums.com/threads/mimetypes-guess_type-broken-in-windows-on-py2-7-and-python-3-x.952693/\nmimetypes.init([os.path.join(os.path.dirname(__file__), 'constants', 'mime.types')])\n\n\ndef _add_access_control_headers(request, response):\n    response[\"Access-Control-Allow-Origin\"] = \"*\"\n    response[\"Access-Control-Allow-Methods\"] = \"GET, OPTIONS\"\n    requested_headers = request.META.get(\"HTTP_ACCESS_CONTROL_REQUEST_HEADERS\", \"\")\n    if requested_headers:\n        response[\"Access-Control-Allow-Headers\"] = requested_headers\n\n\ndef get_referrer_url(request):\n    if request.META.get('HTTP_REFERER'):\n        # If available use HTTP_REFERER to infer the host as that will give us more\n        # information if Kolibri is behind a proxy.\n        return urlparse(request.META.get('HTTP_REFERER'))\n\n\ndef generate_image_prefix_url(request, zipped_filename):\n    parsed_referrer_url = get_referrer_url(request)\n    # Remove trailing slash\n    zipcontent = reverse(\n        'kolibri:core:zipcontent',\n        kwargs={\n            \"zipped_filename\": zipped_filename,\n            \"embedded_filepath\": ''\n        })[:-1]\n    if parsed_referrer_url:\n        # Reconstruct the parsed URL using a blank scheme and host + port(1)\n        zipcontent = urlunparse(('', parsed_referrer_url[1], zipcontent, '', '', ''))\n    return zipcontent.encode()\n\n\ndef get_host(request):\n    parsed_referrer_url = get_referrer_url(request)\n    if parsed_referrer_url:\n        host = urlunparse((parsed_referrer_url[0], parsed_referrer_url[1], '', '', '', ''))\n    else:\n        host = request.build_absolute_uri(OPTIONS['Deployment']['URL_PATH_PREFIX'])\n    return host.strip(\"/\")\n\n\ndef _add_content_security_policy_header(request, response):\n    # restrict CSP to only allow resources to be loaded from the Kolibri host, to prevent info leakage\n    # (e.g. via passing user info out as GET parameters to an attacker's server), or inadvertent data usage\n    host = get_host(request)\n    response[\"Content-Security-Policy\"] = \"default-src 'self' 'unsafe-inline' 'unsafe-eval' data: \" + host\n\n\nclass ZipContentView(View):\n\n    @xframe_options_exempt\n    def options(self, request, *args, **kwargs):\n        \"\"\"\n        Handles OPTIONS requests which may be sent as \"preflight CORS\" requests to check permissions.\n        \"\"\"\n        response = HttpResponse()\n        _add_access_control_headers(request, response)\n        return response\n\n    @vary_on_headers('X-Requested-With')\n    @cache_forever\n    @xframe_options_exempt\n    def get(self, request, zipped_filename, embedded_filepath):\n        \"\"\"\n        Handles GET requests and serves a static file from within the zip file.\n        \"\"\"\n\n        # calculate the local file path to the zip file\n        zipped_path = get_content_storage_file_path(zipped_filename)\n\n        # file size\n        file_size = 0\n\n        # if the zipfile does not exist on disk, return a 404\n        if not os.path.exists(zipped_path):\n            raise Http404('\"%(filename)s\" does not exist locally' % {'filename': zipped_filename})\n\n        # if client has a cached version, use that (we can safely assume nothing has changed, due to MD5)\n        if request.META.get('HTTP_IF_MODIFIED_SINCE'):\n            return HttpResponseNotModified()\n\n        with zipfile.ZipFile(zipped_path) as zf:\n\n            # if no path, or a directory, is being referenced, look for an index.html file\n            if not embedded_filepath or embedded_filepath.endswith(\"/\"):\n                embedded_filepath += \"index.html\"\n\n            # get the details about the embedded file, and ensure it exists\n            try:\n                info = zf.getinfo(embedded_filepath)\n            except KeyError:\n                raise Http404('\"{}\" does not exist inside \"{}\"'.format(embedded_filepath, zipped_filename))\n\n            if (not request.is_ajax()) and zipped_path.endswith('zip') and (embedded_filepath.endswith('htm') or embedded_filepath.endswith('html')):\n                # Sets up our HTML5 zip file endpoint on Kolibri to serve up a\n                # special template that loads Hashi and then initializes it.\n                # Only do this when the request is not AJAX, as Hashi will fetch\n                # the real HTML file using an AJAX request, and presumably other\n                # dynamic loading of HTML content would also get confused if it\n                # got the special Hashi template back instead!\n                cache_key = 'hashi_bootstrap_html'\n                bootstrap_content = cache.get(cache_key)\n                if bootstrap_content is None:\n                    template = loader.get_template('content/hashi.html')\n                    bootstrap_content = template.render({}, None)\n                    cache.set(cache_key, bootstrap_content)\n                response = HttpResponse(bootstrap_content)\n                _add_access_control_headers(request, response)\n                _add_content_security_policy_header(request, response)\n                return response\n\n            # try to guess the MIME type of the embedded file being referenced\n            content_type = mimetypes.guess_type(embedded_filepath)[0] or 'application/octet-stream'\n\n            if not os.path.splitext(embedded_filepath)[1] == '.json':\n                # generate a streaming response object, pulling data from within the zip  file\n                response = FileResponse(zf.open(info), content_type=content_type)\n                file_size = info.file_size\n            else:\n                image_prefix_url = generate_image_prefix_url(request, zipped_filename)\n                # load the stream from json file into memory, replace the path_place_holder.\n                content = zf.open(info).read()\n                str_to_be_replaced = ('$' + exercises.IMG_PLACEHOLDER).encode()\n                content_with_path = content.replace(str_to_be_replaced, image_prefix_url)\n                response = HttpResponse(content_with_path, content_type=content_type)\n                file_size = len(content_with_path)\n\n        # set the content-length header to the size of the embedded file\n        if info.file_size:\n            response[\"Content-Length\"] = file_size\n\n        # ensure the browser knows not to try byte-range requests, as we don't support them here\n        response[\"Accept-Ranges\"] = \"none\"\n\n        # add headers to ensure AJAX requests will be permitted for these files, even from a null origin\n        _add_access_control_headers(request, response)\n        _add_content_security_policy_header(request, response)\n\n        return response\n\n\nclass DownloadContentView(View):\n\n    def get(self, request, filename, new_filename):\n        \"\"\"\n        Handles GET requests and serves a static file as an attachment.\n        \"\"\"\n\n        # calculate the local file path of the file\n        path = get_content_storage_file_path(filename)\n\n        # if the file does not exist on disk, return a 404\n        if not os.path.exists(path):\n            raise Http404('\"%(filename)s\" does not exist locally' % {'filename': filename})\n\n        # generate a file response\n        response = FileResponse(open(path, 'rb'))\n\n        # set the content-type by guessing from the filename\n        response['Content-Type'] = mimetypes.guess_type(filename)[0]\n\n        # set the content-disposition as attachment to force download\n        response['Content-Disposition'] = 'attachment;'\n\n        # set the content-length to the file size\n        response['Content-Length'] = os.path.getsize(path)\n\n        return response\n", "path": "kolibri/core/content/views.py"}], "after_files": [{"content": "import hashlib\nimport io\nimport mimetypes\nimport os\nimport zipfile\n\nfrom django.conf import settings\nfrom django.core.cache import cache\nfrom django.http import Http404\nfrom django.http import HttpResponse\nfrom django.http.response import FileResponse\nfrom django.http.response import HttpResponseNotModified\nfrom django.template import loader\nfrom django.urls import reverse\nfrom django.utils.decorators import method_decorator\nfrom django.views.decorators.clickjacking import xframe_options_exempt\nfrom django.views.decorators.http import etag\nfrom django.views.decorators.vary import vary_on_headers\nfrom django.views.generic.base import View\nfrom le_utils.constants import exercises\nfrom six.moves.urllib.parse import urlparse\nfrom six.moves.urllib.parse import urlunparse\n\nfrom .api import cache_forever\nfrom .utils.paths import get_content_storage_file_path\nfrom kolibri.utils.conf import OPTIONS\n\n# Do this to prevent import of broken Windows filetype registry that makes guesstype not work.\n# https://www.thecodingforums.com/threads/mimetypes-guess_type-broken-in-windows-on-py2-7-and-python-3-x.952693/\nmimetypes.init([os.path.join(os.path.dirname(__file__), 'constants', 'mime.types')])\n\nHASHI_FILENAME = None\n\n\ndef get_hashi_filename():\n    global HASHI_FILENAME\n    if HASHI_FILENAME is None or getattr(settings, 'DEVELOPER_MODE', None):\n        with io.open(os.path.join(os.path.dirname(__file__), './build/hashi_filename'), mode='r', encoding='utf-8') as f:\n            HASHI_FILENAME = f.read().strip()\n    return HASHI_FILENAME\n\n\ndef _add_access_control_headers(request, response):\n    response[\"Access-Control-Allow-Origin\"] = \"*\"\n    response[\"Access-Control-Allow-Methods\"] = \"GET, OPTIONS\"\n    requested_headers = request.META.get(\"HTTP_ACCESS_CONTROL_REQUEST_HEADERS\", \"\")\n    if requested_headers:\n        response[\"Access-Control-Allow-Headers\"] = requested_headers\n\n\ndef get_referrer_url(request):\n    if request.META.get('HTTP_REFERER'):\n        # If available use HTTP_REFERER to infer the host as that will give us more\n        # information if Kolibri is behind a proxy.\n        return urlparse(request.META.get('HTTP_REFERER'))\n\n\ndef generate_image_prefix_url(request, zipped_filename):\n    parsed_referrer_url = get_referrer_url(request)\n    # Remove trailing slash\n    zipcontent = reverse(\n        'kolibri:core:zipcontent',\n        kwargs={\n            \"zipped_filename\": zipped_filename,\n            \"embedded_filepath\": ''\n        })[:-1]\n    if parsed_referrer_url:\n        # Reconstruct the parsed URL using a blank scheme and host + port(1)\n        zipcontent = urlunparse(('', parsed_referrer_url[1], zipcontent, '', '', ''))\n    return zipcontent.encode()\n\n\ndef get_host(request):\n    parsed_referrer_url = get_referrer_url(request)\n    if parsed_referrer_url:\n        host = urlunparse((parsed_referrer_url[0], parsed_referrer_url[1], '', '', '', ''))\n    else:\n        host = request.build_absolute_uri(OPTIONS['Deployment']['URL_PATH_PREFIX'])\n    return host.strip(\"/\")\n\n\ndef _add_content_security_policy_header(request, response):\n    # restrict CSP to only allow resources to be loaded from the Kolibri host, to prevent info leakage\n    # (e.g. via passing user info out as GET parameters to an attacker's server), or inadvertent data usage\n    host = get_host(request)\n    response[\"Content-Security-Policy\"] = \"default-src 'self' 'unsafe-inline' 'unsafe-eval' data: \" + host\n\n\ndef calculate_zip_content_etag(request, zipped_filename, embedded_filepath):\n    zipped_path = get_content_storage_file_path(zipped_filename)\n\n    # if no path, or a directory, is being referenced, look for an index.html file\n    if not embedded_filepath or embedded_filepath.endswith(\"/\"):\n        embedded_filepath += \"index.html\"\n\n    # Are we returning the Hashi bootstrap html? In which case the etag should change\n    # along with the built file asset of the Hashi client library.\n    if (not request.is_ajax()) and zipped_path.endswith('zip') and (embedded_filepath.endswith('htm') or embedded_filepath.endswith('html')):\n        return hashlib.md5(get_hashi_filename().encode('utf-8')).hexdigest()\n\n    return hashlib.md5((zipped_filename + embedded_filepath).encode('utf-8')).hexdigest()\n\n\nclass ZipContentView(View):\n\n    @xframe_options_exempt\n    def options(self, request, *args, **kwargs):\n        \"\"\"\n        Handles OPTIONS requests which may be sent as \"preflight CORS\" requests to check permissions.\n        \"\"\"\n        response = HttpResponse()\n        _add_access_control_headers(request, response)\n        return response\n\n    @vary_on_headers('X-Requested-With')\n    @cache_forever\n    @xframe_options_exempt\n    @method_decorator(etag(calculate_zip_content_etag))\n    def get(self, request, zipped_filename, embedded_filepath):\n        \"\"\"\n        Handles GET requests and serves a static file from within the zip file.\n        \"\"\"\n\n        # calculate the local file path to the zip file\n        zipped_path = get_content_storage_file_path(zipped_filename)\n\n        # file size\n        file_size = 0\n\n        # if the zipfile does not exist on disk, return a 404\n        if not os.path.exists(zipped_path):\n            raise Http404('\"%(filename)s\" does not exist locally' % {'filename': zipped_filename})\n\n        # if client has a cached version, use that (we can safely assume nothing has changed, due to MD5)\n        if request.META.get('HTTP_IF_MODIFIED_SINCE'):\n            return HttpResponseNotModified()\n\n        with zipfile.ZipFile(zipped_path) as zf:\n\n            # if no path, or a directory, is being referenced, look for an index.html file\n            if not embedded_filepath or embedded_filepath.endswith(\"/\"):\n                embedded_filepath += \"index.html\"\n\n            # get the details about the embedded file, and ensure it exists\n            try:\n                info = zf.getinfo(embedded_filepath)\n            except KeyError:\n                raise Http404('\"{}\" does not exist inside \"{}\"'.format(embedded_filepath, zipped_filename))\n\n            if (not request.is_ajax()) and zipped_path.endswith('zip') and (embedded_filepath.endswith('htm') or embedded_filepath.endswith('html')):\n                # Sets up our HTML5 zip file endpoint on Kolibri to serve up a\n                # special template that loads Hashi and then initializes it.\n                # Only do this when the request is not AJAX, as Hashi will fetch\n                # the real HTML file using an AJAX request, and presumably other\n                # dynamic loading of HTML content would also get confused if it\n                # got the special Hashi template back instead!\n                cache_key = 'hashi_bootstrap_html'\n                bootstrap_content = cache.get(cache_key)\n                if bootstrap_content is None:\n                    template = loader.get_template('content/hashi.html')\n                    hashi_path = \"content/{filename}\".format(filename=get_hashi_filename())\n                    bootstrap_content = template.render({\"hashi_path\": hashi_path}, None)\n                    cache.set(cache_key, bootstrap_content)\n                response = HttpResponse(bootstrap_content)\n                _add_access_control_headers(request, response)\n                _add_content_security_policy_header(request, response)\n                return response\n\n            # try to guess the MIME type of the embedded file being referenced\n            content_type = mimetypes.guess_type(embedded_filepath)[0] or 'application/octet-stream'\n\n            if not os.path.splitext(embedded_filepath)[1] == '.json':\n                # generate a streaming response object, pulling data from within the zip  file\n                response = FileResponse(zf.open(info), content_type=content_type)\n                file_size = info.file_size\n            else:\n                image_prefix_url = generate_image_prefix_url(request, zipped_filename)\n                # load the stream from json file into memory, replace the path_place_holder.\n                content = zf.open(info).read()\n                str_to_be_replaced = ('$' + exercises.IMG_PLACEHOLDER).encode()\n                content_with_path = content.replace(str_to_be_replaced, image_prefix_url)\n                response = HttpResponse(content_with_path, content_type=content_type)\n                file_size = len(content_with_path)\n\n        # set the content-length header to the size of the embedded file\n        if info.file_size:\n            response[\"Content-Length\"] = file_size\n\n        # ensure the browser knows not to try byte-range requests, as we don't support them here\n        response[\"Accept-Ranges\"] = \"none\"\n\n        # add headers to ensure AJAX requests will be permitted for these files, even from a null origin\n        _add_access_control_headers(request, response)\n        _add_content_security_policy_header(request, response)\n\n        return response\n\n\nclass DownloadContentView(View):\n\n    def get(self, request, filename, new_filename):\n        \"\"\"\n        Handles GET requests and serves a static file as an attachment.\n        \"\"\"\n\n        # calculate the local file path of the file\n        path = get_content_storage_file_path(filename)\n\n        # if the file does not exist on disk, return a 404\n        if not os.path.exists(path):\n            raise Http404('\"%(filename)s\" does not exist locally' % {'filename': filename})\n\n        # generate a file response\n        response = FileResponse(open(path, 'rb'))\n\n        # set the content-type by guessing from the filename\n        response['Content-Type'] = mimetypes.guess_type(filename)[0]\n\n        # set the content-disposition as attachment to force download\n        response['Content-Disposition'] = 'attachment;'\n\n        # set the content-length to the file size\n        response['Content-Length'] = os.path.getsize(path)\n\n        return response\n", "path": "kolibri/core/content/views.py"}]}