Repository: wagtail/wagtail

Issue:
2.0b1 Bug: Draftail crash when rich text field is named "description"
### Issue Summary

If a richtext field is called "description" (and presumably also "viewport") the script fails, because 
the following
```js
const field = document.querySelector(`[name="${fieldName}"]`);
```
matches the first element on the page with the attribute name matching `fieldName`. Since the admin template contains the following
```html
    <meta name="description" content="" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
```
If a rich text field is named "description" or presumably also "viewport" the query selector will match these meta tags instead of the correct input field. Then it will crash when it tries to parse `undefined` at

```js
const rawContentState = JSON.parse(field.value);
```
since the meta tag is not the correct input tag and does not have a value attribute.

### Steps to Reproduce

Make a model with a rich text field named `description`:
```
    description = RichTextField(_('description'))
```
Open the editor

### Technical details
* Python version: 3.6.3 (irrelevant since this is a js bug)
* Django version: 1.11.10 (irrelevant since this is a js bug)
* Wagtail version: Version: 2.0b1
* Browser version: Google Chrome  64.0.3282.140 (though `JSON.parse(undefined)` will fail in any browser)

### Proposed fix

The minimal fix is simply to add the tagname to the querySelector string like so (assuming rich text fields are always input tags):
```js
const field = document.querySelector(`input[name="${fieldName}"]`);
```
(I'm testing this fix now, by modifying the minified file directly on localhost, so I can see if it causes any other issues; if it doesn't I'm happy to make a PR for it later)

Alternatively the script could be modified to use getElementById instead of queryselector, which is slightly faster and less likely to accidentally match a random tag on the page.

2.0b1 Bug: Draftail crash when rich text field is named "description"
### Issue Summary

If a richtext field is called "description" (and presumably also "viewport") the script fails, because 
the following
```js
const field = document.querySelector(`[name="${fieldName}"]`);
```
matches the first element on the page with the attribute name matching `fieldName`. Since the admin template contains the following
```html
    <meta name="description" content="" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
```
If a rich text field is named "description" or presumably also "viewport" the query selector will match these meta tags instead of the correct input field. Then it will crash when it tries to parse `undefined` at

```js
const rawContentState = JSON.parse(field.value);
```
since the meta tag is not the correct input tag and does not have a value attribute.

### Steps to Reproduce

Make a model with a rich text field named `description`:
```
    description = RichTextField(_('description'))
```
Open the editor

### Technical details
* Python version: 3.6.3 (irrelevant since this is a js bug)
* Django version: 1.11.10 (irrelevant since this is a js bug)
* Wagtail version: Version: 2.0b1
* Browser version: Google Chrome  64.0.3282.140 (though `JSON.parse(undefined)` will fail in any browser)

### Proposed fix

The minimal fix is simply to add the tagname to the querySelector string like so (assuming rich text fields are always input tags):
```js
const field = document.querySelector(`input[name="${fieldName}"]`);
```
(I'm testing this fix now, by modifying the minified file directly on localhost, so I can see if it causes any other issues; if it doesn't I'm happy to make a PR for it later)

Alternatively the script could be modified to use getElementById instead of queryselector, which is slightly faster and less likely to accidentally match a random tag on the page.

2.0b1 Bug: Draftail crash when rich text field is named "description"
### Issue Summary

If a richtext field is called "description" (and presumably also "viewport") the script fails, because 
the following
```js
const field = document.querySelector(`[name="${fieldName}"]`);
```
matches the first element on the page with the attribute name matching `fieldName`. Since the admin template contains the following
```html
    <meta name="description" content="" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
```
If a rich text field is named "description" or presumably also "viewport" the query selector will match these meta tags instead of the correct input field. Then it will crash when it tries to parse `undefined` at

```js
const rawContentState = JSON.parse(field.value);
```
since the meta tag is not the correct input tag and does not have a value attribute.

### Steps to Reproduce

Make a model with a rich text field named `description`:
```
    description = RichTextField(_('description'))
```
Open the editor

### Technical details
* Python version: 3.6.3 (irrelevant since this is a js bug)
* Django version: 1.11.10 (irrelevant since this is a js bug)
* Wagtail version: Version: 2.0b1
* Browser version: Google Chrome  64.0.3282.140 (though `JSON.parse(undefined)` will fail in any browser)

### Proposed fix

The minimal fix is simply to add the tagname to the querySelector string like so (assuming rich text fields are always input tags):
```js
const field = document.querySelector(`input[name="${fieldName}"]`);
```
(I'm testing this fix now, by modifying the minified file directly on localhost, so I can see if it causes any other issues; if it doesn't I'm happy to make a PR for it later)

Alternatively the script could be modified to use getElementById instead of queryselector, which is slightly faster and less likely to accidentally match a random tag on the page.



Discussion:
BTW, it would be nice if the script files were build with sourcemaps and the source files available (at least for beta builds like this), so you could jump directly to a sensibly formatted file in the debugger rather than having to grep for it manually. 

Ideally it should have sourcemaps all the time. If the source map file is in a separate file, only referenced in the bottom of the file with a sourcemaps comment, it doesn't really affect the performance at all unless you open the debugger; in which case having a sourcemap file actually improves performance because it's easier on the debugger to jump to source files rather than to a point in a single line, million character minified file.
👍 I'll have a look at this, probably switch to the field's `id` which is more likely to be unique.

I'm not sure why source maps haven't been included so far, it would definitely make debugging much easier. It should be a straightforward change so I'll make a separate PR for that and we can further discuss there.
It's important that we don't build front-end assets on beta / RC versions any differently from the final releases, because then we'll miss bugs that occur in the build process, like we did in #3944.
@gasman  Sure, but I don't see any reason not to have external sourcemaps in production as well. 

And by "external", i mean the kind where only a short comment is added to the minified file like this:
```js
//# sourceMappingURL=somefilename.js.map
```
This adds just a few bytes to the minimized file, but makes debugging an js error a lot easier if it should occur. The only "disadvantage" besides a slightly slower build, is that people can see your unminified source code easily; but this is open source anyway, so...

I think adding `config.devtool = 'source-map';` to the prod file or something should do that; (I have some trouble getting node up and running correctly, so I haven't testet it yet).
Instead of `document.querySelector`, I suggest using `document.forms['form id'].elements['field name']`, since it's an API specific to form elements.
BTW, it would be nice if the script files were build with sourcemaps and the source files available (at least for beta builds like this), so you could jump directly to a sensibly formatted file in the debugger rather than having to grep for it manually. 

Ideally it should have sourcemaps all the time. If the source map file is in a separate file, only referenced in the bottom of the file with a sourcemaps comment, it doesn't really affect the performance at all unless you open the debugger; in which case having a sourcemap file actually improves performance because it's easier on the debugger to jump to source files rather than to a point in a single line, million character minified file.
👍 I'll have a look at this, probably switch to the field's `id` which is more likely to be unique.

I'm not sure why source maps haven't been included so far, it would definitely make debugging much easier. It should be a straightforward change so I'll make a separate PR for that and we can further discuss there.
It's important that we don't build front-end assets on beta / RC versions any differently from the final releases, because then we'll miss bugs that occur in the build process, like we did in #3944.
@gasman  Sure, but I don't see any reason not to have external sourcemaps in production as well. 

And by "external", i mean the kind where only a short comment is added to the minified file like this:
```js
//# sourceMappingURL=somefilename.js.map
```
This adds just a few bytes to the minimized file, but makes debugging an js error a lot easier if it should occur. The only "disadvantage" besides a slightly slower build, is that people can see your unminified source code easily; but this is open source anyway, so...

I think adding `config.devtool = 'source-map';` to the prod file or something should do that; (I have some trouble getting node up and running correctly, so I haven't testet it yet).
Instead of `document.querySelector`, I suggest using `document.forms['form id'].elements['field name']`, since it's an API specific to form elements.
BTW, it would be nice if the script files were build with sourcemaps and the source files available (at least for beta builds like this), so you could jump directly to a sensibly formatted file in the debugger rather than having to grep for it manually. 

Ideally it should have sourcemaps all the time. If the source map file is in a separate file, only referenced in the bottom of the file with a sourcemaps comment, it doesn't really affect the performance at all unless you open the debugger; in which case having a sourcemap file actually improves performance because it's easier on the debugger to jump to source files rather than to a point in a single line, million character minified file.
👍 I'll have a look at this, probably switch to the field's `id` which is more likely to be unique.

I'm not sure why source maps haven't been included so far, it would definitely make debugging much easier. It should be a straightforward change so I'll make a separate PR for that and we can further discuss there.
It's important that we don't build front-end assets on beta / RC versions any differently from the final releases, because then we'll miss bugs that occur in the build process, like we did in #3944.
@gasman  Sure, but I don't see any reason not to have external sourcemaps in production as well. 

And by "external", i mean the kind where only a short comment is added to the minified file like this:
```js
//# sourceMappingURL=somefilename.js.map
```
This adds just a few bytes to the minimized file, but makes debugging an js error a lot easier if it should occur. The only "disadvantage" besides a slightly slower build, is that people can see your unminified source code easily; but this is open source anyway, so...

I think adding `config.devtool = 'source-map';` to the prod file or something should do that; (I have some trouble getting node up and running correctly, so I haven't testet it yet).
Instead of `document.querySelector`, I suggest using `document.forms['form id'].elements['field name']`, since it's an API specific to form elements.

Resolving patch:
diff --git a/wagtail/admin/rich_text/editors/draftail/__init__.py b/wagtail/admin/rich_text/editors/draftail/__init__.py
--- a/wagtail/admin/rich_text/editors/draftail/__init__.py
+++ b/wagtail/admin/rich_text/editors/draftail/__init__.py
@@ -52,8 +52,8 @@ def render(self, name, value, attrs=None):
         return super().render(name, translated_value, attrs)
 
     def render_js_init(self, id_, name, value):
-        return "window.draftail.initEditor('{name}', {opts})".format(
-            name=name, opts=json.dumps(self.options))
+        return "window.draftail.initEditor('#{id}', {opts}, document.currentScript)".format(
+            id=id_, opts=json.dumps(self.options))
 
     def value_from_datadict(self, data, files, name):
         original_value = super().value_from_datadict(data, files, name)


Test patch:
diff --git a/client/src/components/Draftail/index.test.js b/client/src/components/Draftail/index.test.js
--- a/client/src/components/Draftail/index.test.js
+++ b/client/src/components/Draftail/index.test.js
@@ -10,27 +10,24 @@ import draftail, {
 describe('Draftail', () => {
   describe('#initEditor', () => {
     beforeEach(() => {
+      document.head.innerHTML = '';
       document.body.innerHTML = '';
     });
 
     it('works', () => {
-      const field = document.createElement('input');
-      field.name = 'test';
-      field.value = 'null';
-      document.body.appendChild(field);
+      document.body.innerHTML = '<input type="text" id="test" value="null" />';
+      const field = document.querySelector('#test');
 
-      draftail.initEditor('test', {});
+      draftail.initEditor('#test', {});
 
       expect(field.draftailEditor).toBeDefined();
     });
 
     it('onSave', () => {
-      const field = document.createElement('input');
-      field.name = 'test';
-      field.value = 'null';
-      document.body.appendChild(field);
+      document.body.innerHTML = '<input id="test" value="null" />';
+      const field = document.querySelector('#test');
 
-      draftail.initEditor('test', {});
+      draftail.initEditor('#test', {});
 
       field.draftailEditor.saveState();
 
@@ -38,10 +35,8 @@ describe('Draftail', () => {
     });
 
     it('options', () => {
-      const field = document.createElement('input');
-      field.name = 'test';
-      field.value = 'null';
-      document.body.appendChild(field);
+      document.body.innerHTML = '<input id="test" value="null" />';
+      const field = document.querySelector('#test');
 
       draftail.registerPlugin({
         type: 'IMAGE',
@@ -49,7 +44,7 @@ describe('Draftail', () => {
         block: () => {},
       });
 
-      draftail.initEditor('test', {
+      draftail.initEditor('#test', {
         entityTypes: [
           { type: 'IMAGE' },
         ],
@@ -58,6 +53,42 @@ describe('Draftail', () => {
 
       expect(field.draftailEditor.props).toMatchSnapshot();
     });
+
+    describe('selector conflicts', () => {
+      it('fails to instantiate on the right field', () => {
+        document.body.innerHTML = '<meta name="description" content="null" /><input name="description" value="null" />';
+
+        expect(() => {
+          draftail.initEditor('[name="description"]', {}, document.body);
+        }).toThrow(SyntaxError);
+      });
+
+      it('fails to instantiate on the right field when currentScript is not used', () => {
+        window.draftail = draftail;
+        document.body.innerHTML = `
+          <input name="first" id="description" value="null" />
+          <div>
+            <input name="last" id="description" value="null" />
+            <script>window.draftail.initEditor('#description', {});</script>
+          </div>
+        `;
+
+        expect(document.querySelector('[name="last"]').draftailEditor).not.toBeDefined();
+      });
+
+      it('has no conflict when currentScript is used', () => {
+        window.draftail = draftail;
+        document.body.innerHTML = `
+          <input name="first" id="description" value="null" />
+          <div>
+            <input name="last" id="description" value="null" />
+            <script>window.draftail.initEditor('#description', {}, document.currentScript);</script>
+          </div>
+        `;
+
+        expect(document.querySelector('[name="last"]').draftailEditor).toBeDefined();
+      });
+    });
   });
 
   describe('#wrapWagtailIcon', () => {
diff --git a/wagtail/admin/tests/test_rich_text.py b/wagtail/admin/tests/test_rich_text.py
--- a/wagtail/admin/tests/test_rich_text.py
+++ b/wagtail/admin/tests/test_rich_text.py
@@ -102,7 +102,7 @@ def test_default_editor_in_rich_text_field(self):
         self.assertEqual(response.status_code, 200)
 
         # Check that draftail (default editor) initialisation is applied
-        self.assertContains(response, "window.draftail.initEditor('body',")
+        self.assertContains(response, "window.draftail.initEditor('#id_body',")
 
         # check that media for draftail is being imported
         self.assertContains(response, 'wagtailadmin/js/draftail.js')
@@ -121,7 +121,7 @@ def test_default_editor_in_rich_text_block(self):
         self.assertEqual(response.status_code, 200)
 
         # Check that draftail (default editor) initialisation is applied
-        self.assertContains(response, "window.draftail.initEditor('__PREFIX__-value',")
+        self.assertContains(response, "window.draftail.initEditor('#__PREFIX__-value',")
 
         # check that media for draftail is being imported
         self.assertContains(response, 'wagtailadmin/js/draftail.js')