Repository: wagtail/wagtail

Issue:
Draftail editor allows to link to root page rendering page non-functional
### Issue Summary

The following has come to my attention as a user has been complaining about a non-functional editing interface for a seemingly random sub-page.  The Draftail rich text editor allows to link to the root page of the installation:

![bildschirmfoto 2018-06-13 um 15 06 05](https://user-images.githubusercontent.com/1186959/41353073-6ea89ae8-6f1b-11e8-8b26-5249b40f3ff1.png)

Consequently, a non-functional link is insert as follows:

![bildschirmfoto 2018-06-13 um 14 45 46](https://user-images.githubusercontent.com/1186959/41352571-1f4069fa-6f1a-11e8-992e-f73a66a0f337.png)

Saving this page renders an error - nevertheless the page seems to be persisted. Its impossible to edit the page thereafter.

### Steps to Reproduce

1. Start a new project with `wagtail start myproject`
2. Edit models.py to include a page model with a RichTextField with the `link` feature enabled.
3. Add a link to the fields text clicking the "Root" page in the "Link"-popup.

I'm not entirely sure I'm using the feature correctly. Is it possible to reduce the available pages in the richtexts link popup to not include the root page?

### Technical details

* Python version: 3.6.5
* Django version: Look in your requirements.txt, or run `2.0.2`.
* Wagtail version: 2.1
* Browser version: Safari 11.1.1


Discussion:
Hey @bentrm, thank you for taking the time to review this.

I think this is because of the `can_choose_root` parameter defined where Draftail loads the link chooser:

https://github.com/wagtail/wagtail/blob/cab90e5d1b7ba61ff8da1ca372e595187bec301a/client/src/components/Draftail/sources/ModalWorkflowSource.js#L31-L37

I can see this call is done differently with the previous editor: 

https://github.com/wagtail/wagtail/blob/c7a418b1c360204ab8393386a2dd72312e21993a/wagtail/admin/static_src/wagtailadmin/js/page-chooser.js#L14-L16

I imagine the new definition with `can_choose_root=false` might clash with this server-side code: 

https://github.com/wagtail/wagtail/blob/d483c8d46543cd75caac8d8b4067173646072c81/wagtail/admin/views/chooser.py#L52-L60

…where a string of value `false` will be truthy. 

---

Looking at the history of this code, setting `can_choose_root: 'false',` dates back to the early developments of the new editor. I can't tell exactly what are the implications of this, whether we need to remove this parameter or instead support it like the previous editor does.
`can_choose_root` should just be omitted from the `urlParams` dict, I think - `can_choose_root=False` is the default behaviour of the page chooser (as per [this line](https://github.com/wagtail/wagtail/blob/d483c8d46543cd75caac8d8b4067173646072c81/wagtail/admin/views/chooser.py#L107)).

While the ability to choose the root is indeed an error, I'm more surprised that doing so causes the page to break completely, rather than just being treated as a broken link like any other page without a routable URL... on closer inspection, that's because we call `page.get_parent().id` without a check for the parent being `None`. We should fix that too.
The issue has the status flag of "needs info". Is there any more info I can add to help apart from a pull request?

I just checked the database and can find the following outcome in the text block of that page (a stream field with a rich text component in this case).

````
<a id="1" linktype="page">www.google.de</a>
````

I removed some escaping and changed the URL that has been tried to link to. I think it was meant to be an external link and the user misinterpreted the meaning of "root" in the interface.
@bentrm sorry for the confusion, the part that needed info was whether `can_choose_root: 'false',` should be removed, or whether Draftail's integration should try to match that of Hallo, which @gasman clarified above.

Resolving patch:
diff --git a/wagtail/admin/rich_text/converters/html_to_contentstate.py b/wagtail/admin/rich_text/converters/html_to_contentstate.py
--- a/wagtail/admin/rich_text/converters/html_to_contentstate.py
+++ b/wagtail/admin/rich_text/converters/html_to_contentstate.py
@@ -206,12 +206,19 @@ def get_attribute_data(self, attrs):
         try:
             page = Page.objects.get(id=attrs['id']).specific
         except Page.DoesNotExist:
-            return {}
+            # retain ID so that it's still identified as a page link (albeit a broken one)
+            return {
+                'id': int(attrs['id']),
+                'url': None,
+                'parentId': None
+            }
+
+        parent_page = page.get_parent()
 
         return {
             'id': page.id,
             'url': page.url,
-            'parentId': page.get_parent().id,
+            'parentId': parent_page.id if parent_page else None,
         }
 
 


Test patch:
diff --git a/client/src/components/Draftail/decorators/TooltipEntity.test.js b/client/src/components/Draftail/decorators/TooltipEntity.test.js
--- a/client/src/components/Draftail/decorators/TooltipEntity.test.js
+++ b/client/src/components/Draftail/decorators/TooltipEntity.test.js
@@ -37,6 +37,23 @@ describe('TooltipEntity', () => {
       .text()).toBe('www.example.example.…');
   });
 
+  it('empty label', () => {
+    expect(shallow((
+      <TooltipEntity
+        entityKey="1"
+        onEdit={() => {}}
+        onRemove={() => {}}
+        icon="#icon-test"
+        url="https://www.example.com/"
+        label=""
+      >
+        test
+      </TooltipEntity>
+    )).setState({
+      showTooltipAt: document.createElement('div').getBoundingClientRect(),
+    }).find('Tooltip a').length).toBe(0);
+  });
+
   it('#openTooltip', () => {
     const wrapper = shallow((
       <TooltipEntity
diff --git a/client/src/components/Draftail/sources/ModalWorkflowSource.test.js b/client/src/components/Draftail/sources/ModalWorkflowSource.test.js
--- a/client/src/components/Draftail/sources/ModalWorkflowSource.test.js
+++ b/client/src/components/Draftail/sources/ModalWorkflowSource.test.js
@@ -61,7 +61,13 @@ describe('ModalWorkflowSource', () => {
 
       it('page', () => {
         expect(getChooserConfig({ type: 'LINK' }, {
-          getData: () => ({ id: 1, parentId: 0 })
+          getData: () => ({ id: 2, parentId: 1 })
+        }, '')).toMatchSnapshot();
+      });
+
+      it('root page', () => {
+        expect(getChooserConfig({ type: 'LINK' }, {
+          getData: () => ({ id: 1, parentId: null })
         }, '')).toMatchSnapshot();
       });
 
diff --git a/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap b/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap
--- a/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap
+++ b/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap
@@ -57,7 +57,6 @@ Object {
   "urlParams": Object {
     "allow_email_link": true,
     "allow_external_link": true,
-    "can_choose_root": "false",
     "link_text": "",
     "link_url": "https://www.example.com/",
     "page_type": "wagtailcore.page",
@@ -74,7 +73,6 @@ Object {
   "urlParams": Object {
     "allow_email_link": true,
     "allow_external_link": true,
-    "can_choose_root": "false",
     "link_text": "",
     "link_url": "test@example.com",
     "page_type": "wagtailcore.page",
@@ -91,7 +89,6 @@ Object {
   "urlParams": Object {
     "allow_email_link": true,
     "allow_external_link": true,
-    "can_choose_root": "false",
     "link_text": "",
     "page_type": "wagtailcore.page",
   },
@@ -103,11 +100,25 @@ Object {
   "onload": Object {
     "type": "page",
   },
-  "url": "/admin/choose-page/0/",
+  "url": "/admin/choose-page/1/",
+  "urlParams": Object {
+    "allow_email_link": true,
+    "allow_external_link": true,
+    "link_text": "",
+    "page_type": "wagtailcore.page",
+  },
+}
+`;
+
+exports[`ModalWorkflowSource #getChooserConfig LINK root page 1`] = `
+Object {
+  "onload": Object {
+    "type": "page",
+  },
+  "url": "/admin/choose-page/",
   "urlParams": Object {
     "allow_email_link": true,
     "allow_external_link": true,
-    "can_choose_root": "false",
     "link_text": "",
     "page_type": "wagtailcore.page",
   },
diff --git a/wagtail/admin/tests/test_contentstate.py b/wagtail/admin/tests/test_contentstate.py
--- a/wagtail/admin/tests/test_contentstate.py
+++ b/wagtail/admin/tests/test_contentstate.py
@@ -326,7 +326,31 @@ def test_broken_page_link(self):
             'entityMap': {
                 '0': {
                     'mutability': 'MUTABLE', 'type': 'LINK',
-                    'data': {}
+                    'data': {
+                        'id': 9999, 'url': None, 'parentId': None,
+                    }
+                }
+            },
+            'blocks': [
+                {
+                    'inlineStyleRanges': [], 'text': 'an internal link', 'depth': 0, 'type': 'unstyled', 'key': '00000',
+                    'entityRanges': [{'offset': 3, 'length': 8, 'key': 0}]
+                },
+            ]
+        })
+
+    def test_link_to_root_page(self):
+        converter = ContentstateConverter(features=['link'])
+        result = json.loads(converter.from_database_format(
+            '''
+            <p>an <a linktype="page" id="1">internal</a> link</p>
+            '''
+        ))
+        self.assertContentStateEqual(result, {
+            'entityMap': {
+                '0': {
+                    'mutability': 'MUTABLE', 'type': 'LINK',
+                    'data': {'id': 1, 'url': None, 'parentId': None}
                 }
             },
             'blocks': [