Repository: learningequality/kolibri

Issue:
Attempting to view an unlisted channel with zero contents crashes app
### Observed behavior

1. Published a private channel on Kolibri Studio, but put no contents in it.
1. On Kolibri, attempted to view it by providing my human readable token (kumor-sahar) to the unlisted channel modal. (The token works now, since I added one file to it.)
1. App crashes (stack trace in details below)
1. On Restart, `api/channels` returns 500 error (Stack trace looks similar to that in previous step)
1. To fix things, I delete my KOLIBRI_HOME folder.

This channel has no contents `nokum-sugal` and should cause the app to crash.

### Expected behavior

I guess I just assumed that there would be no crash, and that there would be no contents to browse/import.

### User-facing consequences

This will probably not happen with a well-known public Channel like Khan Academy, but very likely to happen for certain users who want to try out the unlisted channel functionality.

### Errors and logs


```
ERROR    Internal Server Error: /api/channel/ab53c7c5c7564f6f96ff4d09cac8104c/
Traceback (most recent call last):
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/core/handlers/base.py", line 149, in get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/core/handlers/base.py", line 147, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/viewsets.py", line 87, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/views.py", line 466, in dispatch
    response = self.handle_exception(exc)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/views.py", line 463, in dispatch
    response = handler(request, *args, **kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/mixins.py", line 58, in retrieve
    return Response(serializer.data)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 503, in data
    ret = super(Serializer, self).data
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 239, in data
    self._data = self.to_representation(self.instance)
  File "kolibri/content/serializers.py", line 18, in to_representation
    value = super(ChannelMetadataSerializer, self).to_representation(instance)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 472, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/fields.py", line 1653, in to_representation
    return method(value)
  File "kolibri/content/serializers.py", line 41, in get_lang_code
    if instance.root.lang is None:
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/db/models/fields/related_descriptors.py", line 169, in __get__
    rel_obj = qs.get()
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/db/models/query.py", line 387, in get
    self.model._meta.object_name
DoesNotExist: ContentNode matching query does not exist.
ERROR    Internal Server Error: /api/channel/
Traceback (most recent call last):
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/core/handlers/base.py", line 149, in get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/core/handlers/base.py", line 147, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/viewsets.py", line 87, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/views.py", line 466, in dispatch
    response = self.handle_exception(exc)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/views.py", line 463, in dispatch
    response = handler(request, *args, **kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/mixins.py", line 48, in list
    return Response(serializer.data)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 674, in data
    ret = super(ListSerializer, self).data
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 239, in data
    self._data = self.to_representation(self.instance)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 614, in to_representation
    self.child.to_representation(item) for item in iterable
  File "kolibri/content/serializers.py", line 18, in to_representation
    value = super(ChannelMetadataSerializer, self).to_representation(instance)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 472, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/fields.py", line 1653, in to_representation
    return method(value)
  File "kolibri/content/serializers.py", line 41, in get_lang_code
    if instance.root.lang is None:
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/db/models/fields/related_descriptors.py", line 169, in __get__
    rel_obj = qs.get()
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/db/models/query.py", line 387, in get
    self.model._meta.object_name
DoesNotExist: ContentNode matching query does not exist.
[29/Nov/2017 15:45:19] "GET /api/channel/?file_sizes=true&1511999119306=1511999119306 HTTP/1.1" 500 134611
[29/Nov/2017 15:45:19] "GET /api/channel/ab53c7c5c7564f6f96ff4d09cac8104c/?file_sizes=true&1511999119053=1511999119053 HTTP/1.1" 500 129870
[29/Nov/2017 15:45:19] "GET /api/tasks/?1511999119715=1511999119715 HTTP/1.1" 200 2
ERROR    Internal Server Error: /api/channel/
Traceback (most recent call last):
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/core/handlers/base.py", line 149, in get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/core/handlers/base.py", line 147, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/viewsets.py", line 87, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/views.py", line 466, in dispatch
    response = self.handle_exception(exc)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/views.py", line 463, in dispatch
    response = handler(request, *args, **kwargs)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/mixins.py", line 48, in list
    return Response(serializer.data)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 674, in data
    ret = super(ListSerializer, self).data
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 239, in data
    self._data = self.to_representation(self.instance)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 614, in to_representation
    self.child.to_representation(item) for item in iterable
  File "kolibri/content/serializers.py", line 18, in to_representation
    value = super(ChannelMetadataSerializer, self).to_representation(instance)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/serializers.py", line 472, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/rest_framework/fields.py", line 1653, in to_representation
    return method(value)
  File "kolibri/content/serializers.py", line 41, in get_lang_code
    if instance.root.lang is None:
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/db/models/fields/related_descriptors.py", line 169, in __get__
    rel_obj = qs.get()
  File "/Users/jon/.venvs/kolibri/lib/python2.7/site-packages/django/db/models/query.py", line 387, in get
    self.model._meta.object_name
DoesNotExist: ContentNode matching query does not exist.
```


### Steps to reproduce

precise steps that someone else can follow in order to see this behavior

### Context

* Kolibri version: ?
* Operating system: ?
* Browser: ?



Discussion:

Resolving patch:
diff --git a/kolibri/content/utils/channel_import.py b/kolibri/content/utils/channel_import.py
--- a/kolibri/content/utils/channel_import.py
+++ b/kolibri/content/utils/channel_import.py
@@ -7,7 +7,7 @@
 from kolibri.utils.time import local_now
 from sqlalchemy.exc import SQLAlchemyError
 
-from .annotation import set_leaf_node_availability_from_local_file_availability, recurse_availability_up_tree
+from .annotation import recurse_availability_up_tree, set_leaf_node_availability_from_local_file_availability
 from .channels import read_channel_metadata_from_db_file
 from .paths import get_content_database_file_path
 from .sqlalchemybridge import Bridge, ClassNotFoundError
@@ -357,4 +357,14 @@ def import_channel_from_local_db(channel_id):
 
     channel = ChannelMetadata.objects.get(id=channel_id)
     channel.last_updated = local_now()
+    try:
+        assert channel.root
+    except ContentNode.DoesNotExist:
+        node_id = channel.root_id
+        ContentNode.objects.create(
+            id=node_id,
+            title=channel.name,
+            content_id=node_id,
+            channel_id=channel_id,
+        )
     channel.save()


Test patch:
diff --git a/kolibri/content/test/test_channel_import.py b/kolibri/content/test/test_channel_import.py
--- a/kolibri/content/test/test_channel_import.py
+++ b/kolibri/content/test/test_channel_import.py
@@ -6,8 +6,8 @@
 from django.test import TestCase, TransactionTestCase
 
 from kolibri.content import models as content
-from kolibri.content.models import NO_VERSION, V020BETA1, V040BETA3
-from kolibri.content.utils.channel_import import ChannelImport, mappings
+from kolibri.content.models import NO_VERSION, V020BETA1, V040BETA3, ChannelMetadata, ContentNode
+from kolibri.content.utils.channel_import import ChannelImport, mappings, import_channel_from_local_db
 from kolibri.content.utils.sqlalchemybridge import get_default_db_string
 
 from mock import patch, MagicMock, Mock, call
@@ -283,6 +283,31 @@ def test_base_table_mapper(self, apps_mock, tree_id_mock, BridgeMock):
             call.session.query().all()
         ])
 
+class MaliciousDatabaseTestCase(TestCase):
+
+    @patch('kolibri.content.utils.channel_import.set_leaf_node_availability_from_local_file_availability')
+    @patch('kolibri.content.utils.channel_import.recurse_availability_up_tree')
+    @patch('kolibri.content.utils.channel_import.initialize_import_manager')
+    def test_non_existent_root_node(self, initialize_manager_mock, recurse_mock, leaf_mock):
+        import_mock = MagicMock()
+        initialize_manager_mock.return_value = import_mock
+        channel_id = '6199dde695db4ee4ab392222d5af1e5c'
+
+        def create_channel():
+            ChannelMetadata.objects.create(
+                id=channel_id,
+                name='test',
+                min_schema_version='1',
+                root_id=channel_id,
+            )
+        import_mock.import_channel_data.side_effect = create_channel
+        import_channel_from_local_db(channel_id)
+        try:
+            channel = ChannelMetadata.objects.get(id=channel_id)
+            assert channel.root
+        except ContentNode.DoesNotExist:
+            self.fail('Channel imported without a valid root node')
+
 
 SCHEMA_PATH_TEMPLATE = os.path.join(os.path.dirname(__file__), '../fixtures/{name}_content_schema')