From 37d13648bdf62f3059faf9790b4362f52bc29b28 Mon Sep 17 00:00:00 2001
From: Calum Mackervoy <c.mackervoy@gmail.com>
Date: Tue, 7 Jul 2020 08:35:55 +0000
Subject: [PATCH] bugfix: Model.get_or_create_external

---
 djangoldp/activities/services.py |  2 +-
 djangoldp/models.py              | 20 ++++++++++++++++++--
 djangoldp/serializers.py         |  2 +-
 djangoldp/tests/tests_inbox.py   | 24 ++++++++++++++++++++++++
 djangoldp/views.py               |  7 +++++--
 5 files changed, 49 insertions(+), 6 deletions(-)

diff --git a/djangoldp/activities/services.py b/djangoldp/activities/services.py
index 4b817d9f..c478eac3 100644
--- a/djangoldp/activities/services.py
+++ b/djangoldp/activities/services.py
@@ -210,7 +210,6 @@ class ActivityPubService(object):
         for field_name, relation_info in info.relations.items():
             if not relation_info.to_many:
                 value = getattr(instance, field_name, None)
-                logger.debug('[Sender] model has relation ' + str(value))
                 if value is not None and Model.is_external(value):
                     target_type = Model.get_model_rdf_type(type(value))
 
@@ -218,6 +217,7 @@ class ActivityPubService(object):
                         continue
 
                     targets.add(value.urlid)
+                    logger.debug('[Sender] model has external relation ' + str(value.urlid))
 
         return targets
 
diff --git a/djangoldp/models.py b/djangoldp/models.py
index d970ac35..65638a41 100644
--- a/djangoldp/models.py
+++ b/djangoldp/models.py
@@ -192,6 +192,16 @@ class Model(models.Model):
                 field_tuples['username'] = str(uuid.uuid4())
             return model.objects.create(urlid=urlid, is_backlink=True, **field_tuples)
 
+    @classonlymethod
+    def get_or_create_external(cls, model, urlid, **kwargs):
+        '''
+        checks that the parameterised urlid is external and then returns the result of Model.get_or_create
+        :raises ObjectDoesNotExist: if the urlid is not external and the object doesn't exist
+        '''
+        if not Model.is_external(urlid) and not model.objects.filter(urlid=urlid).exists():
+            raise ObjectDoesNotExist
+        return Model.get_or_create(model, urlid, **kwargs)
+
     @classonlymethod
     def get_model_rdf_type(cls, model):
         if model is get_user_model():
@@ -234,9 +244,15 @@ class Model(models.Model):
 
     @classmethod
     def is_external(cls, value):
-        '''returns True if the urlid of the value passed is from an external source'''
+        '''
+        :param value: string urlid or an instance with urlid field
+        :return: True if the urlid is external to the server, False otherwise
+        '''
         try:
-            return value.urlid is not None and not value.urlid.startswith(settings.SITE_URL)
+            if not isinstance(value, str):
+                value = value.urlid
+
+            return value is not None and not value.startswith(settings.SITE_URL)
         except:
             return False
 
diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py
index 62543396..b7da7239 100644
--- a/djangoldp/serializers.py
+++ b/djangoldp/serializers.py
@@ -627,7 +627,7 @@ class LDPSerializer(HyperlinkedModelSerializer):
                         model, sub_inst = Model.resolve(field_dict['urlid'])
                 # remote resource - get backlinked copy
                 elif hasattr(field_model, 'urlid'):
-                    sub_inst = Model.get_or_create(field_model, field_dict['urlid'])
+                    sub_inst = Model.get_or_create_external(field_model, field_dict['urlid'])
             # try slug field, assuming that this is a local resource
             elif slug_field in field_dict:
                 kwargs = {slug_field: field_dict[slug_field]}
diff --git a/djangoldp/tests/tests_inbox.py b/djangoldp/tests/tests_inbox.py
index b11f7ddb..834228e5 100644
--- a/djangoldp/tests/tests_inbox.py
+++ b/djangoldp/tests/tests_inbox.py
@@ -1,5 +1,6 @@
 import json
 from django.contrib.auth import get_user_model
+from django.conf import settings
 from django.db import IntegrityError
 from rest_framework.test import APIClient, APITestCase
 from djangoldp.tests.models import Circle, CircleMember, Project, UserProfile, DateModel, DateChild
@@ -75,6 +76,29 @@ class TestsInbox(APITestCase):
         self.assertEqual(circles[0].owner, self.user)
         self._assert_activity_created(response)
 
+    # sender has sent a circle with a local user that doesn't exist
+    def test_create_activity_circle_local(self):
+        urlid = '{}{}'.format(settings.SITE_URL, 'someonewhodoesntexist')
+        obj = {
+            "@type": "hd:circle",
+            "@id": "https://distant.com/circles/1/",
+            "owner": {
+                "@type": "foaf:user",
+                "@id": urlid
+            }
+        }
+        payload = self._get_activity_request_template("Create", obj)
+
+        prior_users_length = get_user_model().objects.count()
+
+        response = self.client.post('/inbox/',
+                                    data=json.dumps(payload), content_type='application/ld+json')
+        self.assertEqual(response.status_code, 404)
+
+        # assert that the circle was not created neither a backlinked user
+        self.assertEquals(Circle.objects.count(), 0)
+        self.assertEquals(get_user_model().objects.count(), prior_users_length)
+
     #
     #   ADD ACTIVITIES
     #
diff --git a/djangoldp/views.py b/djangoldp/views.py
index 5f056fd5..6a2dfb8b 100644
--- a/djangoldp/views.py
+++ b/djangoldp/views.py
@@ -4,7 +4,7 @@ from django.apps import apps
 from django.conf import settings
 from django.conf.urls import url, include
 from django.contrib.auth import get_user_model
-from django.core.exceptions import FieldDoesNotExist
+from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
 from django.core.urlresolvers import get_resolver
 from django.db import IntegrityError, transaction
 from django.http import JsonResponse, Http404
@@ -157,7 +157,10 @@ class InboxView(APIView):
                 branches[item[0]] = backlink
 
         # get or create the backlink
-        return Model.get_or_create(object_model, obj['@id'], update=update, **branches)
+        try:
+            return Model.get_or_create_external(object_model, obj['@id'], update=update, **branches)
+        except ObjectDoesNotExist:
+            raise Http404()
 
     # TODO: a fallback here? Saving the backlink as Object or similar
     def _get_subclass_with_rdf_type_or_404(self, rdf_type):
-- 
GitLab