Repository: wagtail/wagtail

Issue:
ConfigException - "UNDERLINE" is not in the config and has no fallback
### Issue Summary

Not entirely sure *how* this happens - it's something I can sort of recreate by hacking around with the data sent in the admin edit form, but can't recreate in the same way as an end user. I've seen it on 2 different projects with Sentry reports.

Somehow underline content is coming through rich text fields, maybe through a copy/paste (but it seems to strip invalid styles for me), and ends up with a 500:

```
Internal Server Error: /admin/pages/4/edit/
Traceback (most recent call last):
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/options.py", line 85, in get
    return options[type_]
KeyError: 'UNDERLINE'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/options.py", line 88, in get
    return options[fallback_key]
KeyError: 'FALLBACK'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/urls/__init__.py", line 108, in wrapper
    return view_func(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/auth.py", line 193, in decorated_view
    return view_func(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/views/pages/edit.py", line 224, in dispatch
    return super().dispatch(request)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/views/pages/edit.py", line 310, in post
    if self.form.is_valid() and not self.page_perms.page_locked():
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/forms/pages.py", line 150, in is_valid
    return super().is_valid()
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/modelcluster/forms.py", line 316, in is_valid
    form_is_valid = super(ClusterForm, self).is_valid()
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/forms/forms.py", line 175, in is_valid
    return self.is_bound and not self.errors
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/forms/forms.py", line 170, in errors
    self.full_clean()
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/forms/forms.py", line 372, in full_clean
    self._clean_fields()
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/forms/forms.py", line 384, in _clean_fields
    value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/core/blocks/base.py", line 535, in value_from_datadict
    return self.block_def.value_from_datadict(data, files, name)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/core/blocks/stream_block.py", line 114, in value_from_datadict
    child_block.value_from_datadict(data, files, '%s-%d-value' % (prefix, i)),
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/core/blocks/field_block.py", line 48, in value_from_datadict
    return self.value_from_form(self.field.widget.value_from_datadict(data, files, prefix))
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/rich_text/editors/draftail/__init__.py", line 73, in value_from_datadict
    return self.converter.to_database_format(original_value)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/rich_text/converters/contentstate.py", line 132, in to_database_format
    return self.exporter.render(json.loads(contentstate_json))
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/html.py", line 52, in render
    elt = self.render_block(block, entity_map, wrapper_state)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/html.py", line 84, in render_block
    styled_node = style_state.render_styles(decorated_node, block, wrapper_state.blocks)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/style_state.py", line 34, in render_styles
    options = Options.get(self.style_options, style, INLINE_STYLES.FALLBACK)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/options.py", line 90, in get
    raise ConfigException('"%s" is not in the config and has no fallback' % type_)
draftjs_exporter.error.ConfigException: "UNDERLINE" is not in the config and has no fallback
```

### Steps to Reproduce

1. Edit a rich text field, add some bold text: <img width="1124" alt="Screenshot 2021-08-01 at 22 14 06" src="https://user-images.githubusercontent.com/177332/127785617-04d56bdf-1c7a-4807-a28f-568a251427f6.png">
2. Change BOLD to UNDERLINE (note: you may have to lose focus on the text area input, otherwise it tries to revert it back to BOLD, and also change the outer HTML attribute quotes to `'` for this to work): 
<img width="1127" alt="Screenshot 2021-08-01 at 22 14 22" src="https://user-images.githubusercontent.com/177332/127785623-166074e4-f018-4f25-8037-98cce93bd21b.png">
3. Click publish (or preview) - a 500 will occur

I know this is a bit obscure, but an editor shouldn't be able to cause a 500 error.

### Technical details

* Python version: 3.9 (any)
* Django version: 3.2 (any)
* Wagtail version: main, 2.11.7

ConfigException - "UNDERLINE" is not in the config and has no fallback
### Issue Summary

Not entirely sure *how* this happens - it's something I can sort of recreate by hacking around with the data sent in the admin edit form, but can't recreate in the same way as an end user. I've seen it on 2 different projects with Sentry reports.

Somehow underline content is coming through rich text fields, maybe through a copy/paste (but it seems to strip invalid styles for me), and ends up with a 500:

```
Internal Server Error: /admin/pages/4/edit/
Traceback (most recent call last):
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/options.py", line 85, in get
    return options[type_]
KeyError: 'UNDERLINE'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/options.py", line 88, in get
    return options[fallback_key]
KeyError: 'FALLBACK'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/urls/__init__.py", line 108, in wrapper
    return view_func(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/auth.py", line 193, in decorated_view
    return view_func(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/views/pages/edit.py", line 224, in dispatch
    return super().dispatch(request)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/views/pages/edit.py", line 310, in post
    if self.form.is_valid() and not self.page_perms.page_locked():
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/forms/pages.py", line 150, in is_valid
    return super().is_valid()
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/modelcluster/forms.py", line 316, in is_valid
    form_is_valid = super(ClusterForm, self).is_valid()
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/forms/forms.py", line 175, in is_valid
    return self.is_bound and not self.errors
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/forms/forms.py", line 170, in errors
    self.full_clean()
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/forms/forms.py", line 372, in full_clean
    self._clean_fields()
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/django/forms/forms.py", line 384, in _clean_fields
    value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/core/blocks/base.py", line 535, in value_from_datadict
    return self.block_def.value_from_datadict(data, files, name)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/core/blocks/stream_block.py", line 114, in value_from_datadict
    child_block.value_from_datadict(data, files, '%s-%d-value' % (prefix, i)),
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/core/blocks/field_block.py", line 48, in value_from_datadict
    return self.value_from_form(self.field.widget.value_from_datadict(data, files, prefix))
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/rich_text/editors/draftail/__init__.py", line 73, in value_from_datadict
    return self.converter.to_database_format(original_value)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/wagtail/admin/rich_text/converters/contentstate.py", line 132, in to_database_format
    return self.exporter.render(json.loads(contentstate_json))
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/html.py", line 52, in render
    elt = self.render_block(block, entity_map, wrapper_state)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/html.py", line 84, in render_block
    styled_node = style_state.render_styles(decorated_node, block, wrapper_state.blocks)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/style_state.py", line 34, in render_styles
    options = Options.get(self.style_options, style, INLINE_STYLES.FALLBACK)
  File "/Users/tomkins/.virtualenvs/tmp-826a0d9266372eb/lib/python3.9/site-packages/draftjs_exporter/options.py", line 90, in get
    raise ConfigException('"%s" is not in the config and has no fallback' % type_)
draftjs_exporter.error.ConfigException: "UNDERLINE" is not in the config and has no fallback
```

### Steps to Reproduce

1. Edit a rich text field, add some bold text: <img width="1124" alt="Screenshot 2021-08-01 at 22 14 06" src="https://user-images.githubusercontent.com/177332/127785617-04d56bdf-1c7a-4807-a28f-568a251427f6.png">
2. Change BOLD to UNDERLINE (note: you may have to lose focus on the text area input, otherwise it tries to revert it back to BOLD, and also change the outer HTML attribute quotes to `'` for this to work): 
<img width="1127" alt="Screenshot 2021-08-01 at 22 14 22" src="https://user-images.githubusercontent.com/177332/127785623-166074e4-f018-4f25-8037-98cce93bd21b.png">
3. Click publish (or preview) - a 500 will occur

I know this is a bit obscure, but an editor shouldn't be able to cause a 500 error.

### Technical details

* Python version: 3.9 (any)
* Django version: 3.2 (any)
* Wagtail version: main, 2.11.7



Discussion:

Resolving patch:
diff --git a/wagtail/admin/rich_text/converters/contentstate.py b/wagtail/admin/rich_text/converters/contentstate.py
--- a/wagtail/admin/rich_text/converters/contentstate.py
+++ b/wagtail/admin/rich_text/converters/contentstate.py
@@ -47,6 +47,12 @@ def entity_fallback(props):
     return None
 
 
+def style_fallback(props):
+    type_ = props['inline_style_range']['style']
+    logging.warn('Missing config for "%s". Deleting style.' % type_)
+    return props['children']
+
+
 def persist_key_for_block(config):
     # For any block level element config for draft js exporter, return a config that retains the
     # block key in a data attribute
@@ -90,7 +96,9 @@ def __init__(self, features=None):
                 'atomic': render_children,
                 'fallback': block_fallback,
             },
-            'style_map': {},
+            'style_map': {
+                'FALLBACK': style_fallback,
+            },
             'entity_decorators': {
                 'FALLBACK': entity_fallback,
             },


Test patch:
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
@@ -1036,3 +1036,21 @@ def test_wrap_block_function(self):
             <p data-block-key='00000'>Hello world!</p>
             <p data-block-key='00001'>Goodbye world!</p>
             ''')
+
+    def test_style_fallback(self):
+        # Test a block which uses an invalid inline style, and will be removed
+        converter = ContentstateConverter(features=[])
+        result = converter.to_database_format(json.dumps({
+            'entityMap': {},
+            'blocks': [
+                {
+                    'inlineStyleRanges': [{'offset': 0, 'length': 12, 'style': 'UNDERLINE'}],
+                    'text': 'Hello world!', 'depth': 0, 'type': 'unstyled', 'key': '00000', 'entityRanges': []
+                },
+            ]
+        }))
+        self.assertHTMLEqual(result, '''
+            <p data-block-key="00000">
+                Hello world!
+            </p>
+        ''')