Repository: learningequality/kolibri

Issue:
KAthJax expressions in exercises do no load on ios/iphone/safari
## Summary

Starting to get really annoyed with iPhone... Oh wait, I never liked it in the first place! :unamused: 

## System information

 - Version: 0.2.0alpha5.dev35
 - Operating system:  iPhone 5S
 - Browser: all (Safari, Chrome, Firefox)

## How to reproduce

Try any of these (I could post more, but these will do for start):

https://instantschools.learningequality.org/learn/#/topics/11d9de56da744f98877cc9fe710bb78d/content/44d60f677fa04938afdba9807e46b8ea

https://instantschools.learningequality.org/learn/#/topics/11d9de56da744f98877cc9fe710bb78d/content/678f22a8bfec4f5bb4969d0817a23499

https://instantschools.learningequality.org/learn/#/topics/11d9de56da744f98877cc9fe710bb78d/content/dddefe5a72c84a37b74f22e9e2c54784

## Screenshots

![exercise2](https://cloud.githubusercontent.com/assets/1457929/23770456/f3abb398-04c7-11e7-8ca7-2cc1a195b3d9.JPG)

![exercise4](https://cloud.githubusercontent.com/assets/1457929/23770476/0a375f04-04c8-11e7-9348-c925099042ed.JPG)




Discussion:
I can reproduce this in iOS simulator (mobile Safari)

In iOS simulator, it seems the URLs for exercise content (svgs, json, etc.)  is not fully resolved, e.g. when trying to load this exercise

https://instantschools.learningequality.org/learn/#/topics/11d9de56da744f98877cc9fe710bb78d/content/fa854ffba7a741b9ab8cedeaa4432baf

the network tab shows me it's fetching from the wrong domain "zipcontent"

https://zipcontent/d7df09963277febd2e38462256782b78.perseus/images/bc014d8dc26d781dee175be6967b9e6c44e9798e.svg

instead of the proper URL (with the correct domain)

https://instantschools.learningequality.org/zipcontent/d7df09963277febd2e38462256782b78.perseus/images/bc014d8dc26d781dee175be6967b9e6c44e9798e.svg

<img width="668" alt="screen shot 2017-03-28 at 3 54 27 pm" src="https://cloud.githubusercontent.com/assets/10248067/24424654/57851ae2-13cf-11e7-86c4-ccb2e5c265d2.png">

Could someone point me to the code that handles this? It may be that there's some incompatible usage of `window.location`.
It looks like the generated URLs are actually wrong in all cases, and other browsers simply handle the malformed URL better. For example (in Chrome) I see:

```html
<img src="https:/zipcontent/d7df09963277febd2e38462256782b78.perseus/images/bc014d8dc26d781dee175be6967b9e6c44e9798e.svg" alt="" data-reactid=".1.$0.$0.$0.1:0">
```

Here are some pointers, but @jayoshih (Jordan) should be able to help more on Slack :)

* KA exercise 'sushi chef':
  * https://github.com/fle-internal/ka-sushi-chef/blob/master/ka_sushi_chef.py
* code that generates links to files inside zip files (zipcontent) is in the content curation server's `exportchannel` method:
  * https://github.com/fle-internal/content-curation/blob/unicef_demo/contentcuration/contentcuration/management/commands/exportchannel.py#L254

In any case, we'll probably need to migrate this issue to a different repo. It's not really a front-end issue, but If you'd like to take a swing at it regardless you're welcome to!

Is this fixed now? I don't see the `https://` prefix anymore. Using Khan Academy, assuming that was the source of the original error...

![image](https://user-images.githubusercontent.com/374612/39960521-9df234b8-5624-11e8-8e91-db519117e3fd.png)

we currently block access to Kolibri via iOS safari
@indirectlylit in that case, if the issue is gone, should we unblock?

I posted the above screenshot because it kind of looks like it's gone...?
unblocking would require some QA first. there are a few [known issues](https://github.com/learningequality/kolibri/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+ios+), and probably unknown ones also

Resolving patch:
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
@@ -6,9 +6,12 @@
 from django.http import HttpResponse
 from django.http.response import FileResponse
 from django.http.response import HttpResponseNotModified
+from django.urls import reverse
 from django.views.decorators.clickjacking import xframe_options_exempt
 from django.views.generic.base import View
 from le_utils.constants import exercises
+from six.moves.urllib.parse import urlparse
+from six.moves.urllib.parse import urlunparse
 
 from .api import cache_forever
 from .utils.paths import get_content_storage_file_path
@@ -27,6 +30,37 @@ def _add_access_control_headers(request, response):
         response["Access-Control-Allow-Headers"] = requested_headers
 
 
+def get_referrer_url(request):
+    if request.META.get('HTTP_REFERER'):
+        # If available use HTTP_REFERER to infer the host as that will give us more
+        # information if Kolibri is behind a proxy.
+        return urlparse(request.META.get('HTTP_REFERER'))
+
+
+def generate_image_prefix_url(request, zipped_filename):
+    parsed_referrer_url = get_referrer_url(request)
+    # Remove trailing slash
+    zipcontent = reverse(
+        'kolibri:core:zipcontent',
+        kwargs={
+            "zipped_filename": zipped_filename,
+            "embedded_filepath": ''
+        })[:-1]
+    if parsed_referrer_url:
+        # Reconstruct the parsed URL using a blank scheme and host + port(1)
+        zipcontent = urlunparse(('', parsed_referrer_url[1], zipcontent, '', '', ''))
+    return zipcontent.encode()
+
+
+def get_host(request):
+    parsed_referrer_url = get_referrer_url(request)
+    if parsed_referrer_url:
+        host = urlunparse((parsed_referrer_url[0], parsed_referrer_url[1], '', '', '', ''))
+    else:
+        host = request.build_absolute_uri(OPTIONS['Deployment']['URL_PATH_PREFIX'])
+    return host.strip("/")
+
+
 class ZipContentView(View):
 
     @xframe_options_exempt
@@ -79,11 +113,11 @@ def get(self, request, zipped_filename, embedded_filepath):
                 response = FileResponse(zf.open(info), content_type=content_type)
                 file_size = info.file_size
             else:
+                image_prefix_url = generate_image_prefix_url(request, zipped_filename)
                 # load the stream from json file into memory, replace the path_place_holder.
                 content = zf.open(info).read()
                 str_to_be_replaced = ('$' + exercises.IMG_PLACEHOLDER).encode()
-                zipcontent = ('/' + request.resolver_match.url_name + "/" + zipped_filename).encode()
-                content_with_path = content.replace(str_to_be_replaced, zipcontent)
+                content_with_path = content.replace(str_to_be_replaced, image_prefix_url)
                 response = HttpResponse(content_with_path, content_type=content_type)
                 file_size = len(content_with_path)
 
@@ -99,7 +133,7 @@ def get(self, request, zipped_filename, embedded_filepath):
 
         # restrict CSP to only allow resources to be loaded from the Kolibri host, to prevent info leakage
         # (e.g. via passing user info out as GET parameters to an attacker's server), or inadvertent data usage
-        host = request.build_absolute_uri(OPTIONS['Deployment']['URL_PATH_PREFIX']).strip("/")
+        host = get_host(request)
         response["Content-Security-Policy"] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: " + host
 
         return response


Test patch:
diff --git a/kolibri/core/content/test/test_zipcontent.py b/kolibri/core/content/test/test_zipcontent.py
--- a/kolibri/core/content/test/test_zipcontent.py
+++ b/kolibri/core/content/test/test_zipcontent.py
@@ -5,6 +5,7 @@
 
 from django.test import Client
 from django.test import TestCase
+from le_utils.constants import exercises
 
 from ..models import LocalFile
 from ..utils.paths import get_content_storage_file_path
@@ -22,6 +23,8 @@ class ZipContentTestCase(TestCase):
     test_str_1 = "This is a test!"
     test_name_2 = "testfile2.txt"
     test_str_2 = "And another test..."
+    test_name_3 = "testfile3.json"
+    test_str_3 = "A test of image placeholder replacement ${placeholder}".format(placeholder=exercises.IMG_PLACEHOLDER)
 
     def setUp(self):
 
@@ -41,6 +44,7 @@ def setUp(self):
         with zipfile.ZipFile(self.zip_path, "w") as zf:
             zf.writestr(self.test_name_1, self.test_str_1)
             zf.writestr(self.test_name_2, self.test_str_2)
+            zf.writestr(self.test_name_3, self.test_str_3)
 
         self.zip_file_obj = LocalFile(id=self.hash, extension=self.extension, available=True)
         self.zip_file_base_url = self.zip_file_obj.get_storage_url()
@@ -82,6 +86,10 @@ def test_content_security_policy_header(self):
         response = self.client.get(self.zip_file_base_url + self.test_name_1)
         self.assertEqual(response.get("Content-Security-Policy"), "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: http://testserver")
 
+    def test_content_security_policy_header_http_referer(self):
+        response = self.client.get(self.zip_file_base_url + self.test_name_1, HTTP_REFERER="http://testserver:1234/iam/a/real/path/#thatsomeonemightuse")
+        self.assertEqual(response.get("Content-Security-Policy"), "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: http://testserver:1234")
+
     def test_access_control_allow_origin_header(self):
         response = self.client.get(self.zip_file_base_url + self.test_name_1)
         self.assertEqual(response.get("Access-Control-Allow-Origin"), "*")
@@ -98,3 +106,17 @@ def test_access_control_allow_headers(self):
         self.assertEqual(response.get("Access-Control-Allow-Headers", ""), headerval)
         response = self.client.get(self.zip_file_base_url + self.test_name_1, HTTP_ACCESS_CONTROL_REQUEST_HEADERS=headerval)
         self.assertEqual(response.get("Access-Control-Allow-Headers", ""), headerval)
+
+    def test_json_image_replacement_http_referer_header(self):
+        server_name = "http://testserver"
+        response = self.client.get(self.zip_file_base_url + self.test_name_3, HTTP_REFERER=server_name)
+        self.assertEqual(
+            response.content.decode('utf-8'),
+            self.test_str_3.replace("$" + exercises.IMG_PLACEHOLDER, (server_name.replace('http:', '') + self.zip_file_base_url)).strip("/"))
+
+    def test_json_image_replacement_no_http_referer_header(self):
+        server_name = "http://testserver"
+        response = self.client.get(self.zip_file_base_url + self.test_name_3)
+        self.assertEqual(
+            response.content.decode('utf-8'),
+            self.test_str_3.replace("$" + exercises.IMG_PLACEHOLDER, (server_name.replace('http:', '') + self.zip_file_base_url)).strip("/"))