Add warning at submission for listed MV3 uploads (#19893)

* Add warning at submission for listed MV3 uploads
This commit is contained in:
Mathieu Pillard 2022-11-15 18:31:21 +01:00 коммит произвёл GitHub
Родитель d04c031646
Коммит 94a1caf110
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 275 добавлений и 181 удалений

Просмотреть файл

@ -2,11 +2,14 @@ import waffle
from django.utils.translation import gettext
from olympia import amo
from olympia.amo.urlresolvers import linkify_and_clean
from olympia.versions.models import DeniedInstallOrigin
def insert_validation_message(
results,
*,
type_='error',
message='',
msg_id='',
@ -26,6 +29,11 @@ def insert_validation_message(
'message': message,
'description': description,
'compatibility_type': compatibility_type,
# Indicate that it's an extra message not coming from the linter:
# our JavaScript has some logic to display a checklist if there are
# linter warnings, so we want these custom messages we're inserting
# to be excluded from that.
'extra': True,
},
)
# Need to increment 'errors' or 'warnings' count, so add an extra 's' after
@ -69,7 +77,7 @@ def annotate_search_plugin_restriction(results, file_path, channel):
)
def annotate_validation_results(results, parsed_data):
def annotate_validation_results(*, results, parsed_data, channel):
"""Annotate validation results with potential add-on restrictions like
denied origins."""
if waffle.switch_is_active('record-install-origins'):
@ -84,4 +92,57 @@ def annotate_validation_results(results, parsed_data):
origin=origin
),
)
add_manifest_version_messages(results=results, channel=channel)
return results
def add_manifest_version_messages(*, results, channel):
mv = results.get('metadata', {}).get('manifestVersion')
if mv != 3:
return
if 'messages' not in results:
results['messages'] = []
enable_mv3_submissions = waffle.switch_is_active('enable-mv3-submissions')
if not enable_mv3_submissions:
msg = gettext(
'Manifest V3 is currently not supported for upload. '
'{start_href}Read more about the support timeline{end_href}.'
)
url = 'https://blog.mozilla.org/addons/2021/05/27/manifest-v3-update/'
start_href = f'<a href="{url}" target="_blank" rel="noopener">'
new_error_message = msg.format(start_href=start_href, end_href='</a>')
for index, message in enumerate(results['messages']):
if message.get('instancePath') == '/manifest_version':
# if we find the linter manifest_version=3 warning, replace it
results['messages'][index]['message'] = new_error_message
break
else:
# otherwise insert a new error at the start of the errors
insert_validation_message(
results, message=new_error_message, msg_id='mv3_not_supported_yet'
)
elif channel == amo.CHANNEL_LISTED:
# If submitting a listed upload and mv3 switch is on, we want to warn
# about using unlisted instead for now.
insert_validation_message(
results,
type_='warning',
message=gettext('Manifest V3 compatibility warning'),
description=[
gettext(
'Firefox is adding support for manifest version 3 (MV3) extensions '
'in Firefox {version}, however, older versions of Firefox are only '
'compatible with manifest version 2 (MV2) extensions. We recommend '
'uploading Manifest V3 extensions as self-hosted for now to not '
'break compatibility for your users.'
).format(version=amo.DEFAULT_WEBEXT_MIN_VERSION_MV3_FIREFOX),
linkify_and_clean(
gettext(
'For more information about the MV3 extension roll-out or '
'self-hosting MV3 extensions, visit https://mzl.la/3hIwQXX'
)
),
],
msg_id='_MV3_COMPATIBILITY',
)

Просмотреть файл

@ -255,7 +255,9 @@ def validate_file_path(path, channel):
log.info('Running linter on %s', path)
results = run_addons_linter(path, channel=channel)
annotations.annotate_validation_results(results, parsed_data)
annotations.annotate_validation_results(
results=results, parsed_data=parsed_data, channel=channel
)
return json.dumps(results)

Просмотреть файл

@ -2,6 +2,7 @@ from copy import deepcopy
from waffle.testutils import override_switch
from olympia import amo
from olympia.amo.tests import TestCase
from olympia.constants.base import VALIDATOR_SKELETON_RESULTS
from olympia.devhub.file_validation_annotations import annotate_validation_results
@ -15,7 +16,9 @@ class TestDeniedOrigins(TestCase):
DeniedInstallOrigin.objects.create(hostname_pattern='foo.com')
results = deepcopy(VALIDATOR_SKELETON_RESULTS)
data = {'install_origins': ['https://foo.com']}
return_value = annotate_validation_results(results, data)
return_value = annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
assert return_value == results
assert results['errors'] == 0
assert len(results['messages']) == 0
@ -24,7 +27,9 @@ class TestDeniedOrigins(TestCase):
DeniedInstallOrigin.objects.create(hostname_pattern='foo.com')
results = deepcopy(VALIDATOR_SKELETON_RESULTS)
data = {'install_origins': ['https://bar.com']}
return_value = annotate_validation_results(results, data)
return_value = annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
assert return_value == results
assert results['errors'] == 0
assert len(results['messages']) == 0
@ -33,7 +38,9 @@ class TestDeniedOrigins(TestCase):
DeniedInstallOrigin.objects.create(hostname_pattern='foo.com')
results = deepcopy(VALIDATOR_SKELETON_RESULTS)
data = {'install_origins': ['https://bar.com', 'https://example.com']}
return_value = annotate_validation_results(results, data)
return_value = annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
assert return_value == results
assert results['errors'] == 0
assert len(results['messages']) == 0
@ -42,7 +49,9 @@ class TestDeniedOrigins(TestCase):
DeniedInstallOrigin.objects.create(hostname_pattern='foo.com')
results = deepcopy(VALIDATOR_SKELETON_RESULTS)
data = {'install_origins': ['https://bar.com', 'https://foo.com']}
return_value = annotate_validation_results(results, data)
return_value = annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
assert return_value == results
assert results['errors'] == 1
assert results['messages'][0] == {
@ -51,6 +60,7 @@ class TestDeniedOrigins(TestCase):
'id': ['validation', 'messages', ''],
'message': 'The install origin https://foo.com is not permitted.',
'description': [],
'extra': True, # This didn't come from the linter.
'compatibility_type': None,
}
@ -59,7 +69,9 @@ class TestDeniedOrigins(TestCase):
DeniedInstallOrigin.objects.create(hostname_pattern='foo.*')
results = deepcopy(VALIDATOR_SKELETON_RESULTS)
data = {'install_origins': ['https://bar.com', 'https://foo.com']}
return_value = annotate_validation_results(results, data)
return_value = annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
assert return_value == results
assert results['errors'] == 1
assert results['messages'][0] == {
@ -68,6 +80,7 @@ class TestDeniedOrigins(TestCase):
'id': ['validation', 'messages', ''],
'message': 'The install origin https://foo.com is not permitted.',
'description': [],
'extra': True, # This didn't come from the linter.
'compatibility_type': None,
}
@ -82,7 +95,9 @@ class TestDeniedOrigins(TestCase):
'https://foo.com',
]
}
return_value = annotate_validation_results(results, data)
return_value = annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
assert return_value == results
assert results['errors'] == 2
assert results['messages'][0] == {
@ -91,6 +106,7 @@ class TestDeniedOrigins(TestCase):
'id': ['validation', 'messages', ''],
'message': 'The install origin https://foo.fr is not permitted.',
'description': [],
'extra': True, # This didn't come from the linter.
'compatibility_type': None,
}
assert results['messages'][1] == {
@ -99,6 +115,7 @@ class TestDeniedOrigins(TestCase):
'id': ['validation', 'messages', ''],
'message': 'The install origin https://foo.com is not permitted.',
'description': [],
'extra': True, # This didn't come from the linter.
'compatibility_type': None,
}
@ -107,7 +124,9 @@ class TestDeniedOrigins(TestCase):
DeniedInstallOrigin.objects.create(hostname_pattern='foo.*')
results = deepcopy(VALIDATOR_SKELETON_RESULTS)
data = {'install_origins': ['https://bar.com', 'https://foo.com']}
return_value = annotate_validation_results(results, data)
return_value = annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
assert return_value == results
assert results['errors'] == 2
assert results['messages'][0] == {
@ -116,6 +135,7 @@ class TestDeniedOrigins(TestCase):
'id': ['validation', 'messages', ''],
'message': 'The install origin https://foo.com is not permitted.',
'description': [],
'extra': True, # This didn't come from the linter.
'compatibility_type': None,
}
assert results['messages'][1] == {
@ -124,6 +144,7 @@ class TestDeniedOrigins(TestCase):
'id': ['validation', 'messages', ''],
'message': 'The install origin https://bar.com is not permitted.',
'description': [],
'extra': True, # This didn't come from the linter.
'compatibility_type': None,
}
@ -131,7 +152,102 @@ class TestDeniedOrigins(TestCase):
DeniedInstallOrigin.objects.create(hostname_pattern='foo.com')
results = deepcopy(VALIDATOR_SKELETON_RESULTS)
data = {}
return_value = annotate_validation_results(results, data)
return_value = annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
assert return_value == results
assert results['errors'] == 0
assert len(results['messages']) == 0
class TestAddManifestVersionMessages(TestCase):
def test_add_manifest_version_message_not_listed(self):
results = deepcopy(amo.VALIDATOR_SKELETON_EXCEPTION_WEBEXT)
results['messages'] = []
results['metadata']['manifestVersion'] = 3
data = {}
annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
# mv3 submission switch is off so we should have added the message even
# for an unlisted submission.
assert len(results['messages']) == 1
# It should be inserted at the top.
assert 'https://blog.mozilla.org/addons/2021/05/27/manifest-v3-update/' in (
results['messages'][0]['message']
)
def test_add_manifest_version_message_not_mv3(self):
results = deepcopy(amo.VALIDATOR_SKELETON_EXCEPTION_WEBEXT)
results['messages'] = []
results['metadata']['manifestVersion'] = 2
data = {}
annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_LISTED
)
assert results['messages'] == []
def test_add_manifest_version_message_switch_enabled(self):
results = deepcopy(amo.VALIDATOR_SKELETON_EXCEPTION_WEBEXT)
results['messages'] = []
results['metadata']['manifestVersion'] = 3
data = {}
with override_switch('enable-mv3-submissions', active=True):
annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_UNLISTED
)
assert results['messages'] == []
# For listed mv3, we want our message suggesting to use unlisted for
# the time being.
annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_LISTED
)
assert len(results['messages']) == 1
assert (
results['messages'][0]['message'] == 'Manifest V3 compatibility warning'
)
assert 'https://mzl.la/3hIwQXX' in (
# description is a list of strings, the link is in the second one.
results['messages'][0]['description'][1]
)
def test_add_manifest_version_message(self):
results = deepcopy(amo.VALIDATOR_SKELETON_EXCEPTION_WEBEXT)
len(results['messages']) == 1
# Add the error message when the manifest_version is 3 and the switch to
# enable mv3 submissions is off (the default).
# The manifest_version error isn't in VALIDATOR_SKELETON_EXCEPTION_WEBEXT.
results['metadata']['manifestVersion'] = 3
data = {}
annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_LISTED
)
assert len(results['messages']) == 2 # we added it
# It should be inserted at the top.
assert 'https://blog.mozilla.org/addons/2021/05/27/manifest-v3-update/' in (
results['messages'][0]['message']
)
def test_add_manifest_version_message_replace(self):
results = deepcopy(amo.VALIDATOR_SKELETON_EXCEPTION_WEBEXT)
# When the linter error is already there, replace it
results['messages'] = [
{
'message': '"/manifest_version" should be &lt;= 2',
'description': ['Your JSON file could not be parsed.'],
'instancePath': '/manifest_version',
'type': 'error',
'tier': 1,
}
]
results['metadata']['manifestVersion'] = 3
data = {}
annotate_validation_results(
results=results, parsed_data=data, channel=amo.CHANNEL_LISTED
)
assert len(results['messages']) == 1 # we replaced it and not added it.
assert 'https://blog.mozilla.org/addons/2021/05/27/manifest-v3-update/' in (
results['messages'][0]['message']
)

Просмотреть файл

@ -540,7 +540,9 @@ class TestValidateFilePath(ValidatorTestCase):
assert run_addons_linter_mock.call_count == 1
assert annotate_validation_results_mock.call_count == 1
annotate_validation_results_mock.assert_called_with(
run_addons_linter_mock.return_value, parse_addon_mock.return_value
results=run_addons_linter_mock.return_value,
parsed_data=parse_addon_mock.return_value,
channel=amo.CHANNEL_UNLISTED,
)

Просмотреть файл

@ -1,6 +1,5 @@
import json
import os.path
from copy import deepcopy
from datetime import datetime, timedelta
from unittest import mock
@ -11,7 +10,6 @@ import pytest
from celery import chord
from celery.result import AsyncResult
from waffle.testutils import override_switch
from olympia import amo
from olympia.addons.models import Addon
@ -524,47 +522,6 @@ class TestValidator(UploadMixin, TestCase):
assert scanners_chord.body.name == 'olympia.scanners.tasks.call_mad_api'
def test_add_manifest_version_error():
validation = deepcopy(amo.VALIDATOR_SKELETON_EXCEPTION_WEBEXT)
len(validation['messages']) == 1
# Add the error message when the manifest_version is 3.
# The manifest_version error isn't in VALIDATOR_SKELETON_EXCEPTION_WEBEXT.
validation['metadata']['manifestVersion'] = 3
utils.add_manifest_version_error(validation)
assert 'https://blog.mozilla.org/addons/2021/05/27/manifest-v3-update/' in (
validation['messages'][0]['message']
)
assert len(validation['messages']) == 2 # we added it
# When the linter error is already there, replace it
validation['messages'] = [
{
'message': '"/manifest_version" should be &lt;= 2',
'description': ['Your JSON file could not be parsed.'],
'dataPath': '/manifest_version',
'type': 'error',
'tier': 1,
}
]
utils.add_manifest_version_error(validation)
assert 'https://blog.mozilla.org/addons/2021/05/27/manifest-v3-update/' in (
validation['messages'][0]['message']
)
assert len(validation['messages']) == 1 # we replaced it
# Not if the mv3 waffle switch is enabled though
with override_switch('enable-mv3-submissions', active=True):
validation['messages'] = []
utils.add_manifest_version_error(validation)
assert validation['messages'] == []
# Or if the manifest_version != 3
validation['metadata']['manifestVersion'] = 2
utils.add_manifest_version_error(validation)
assert validation['messages'] == []
class TestCreateVersionForUpload(UploadMixin, TestCase):
fixtures = ['base/addon_3615']

Просмотреть файл

@ -1,13 +1,11 @@
import json
from copy import deepcopy
from datetime import datetime
from django.core.files.storage import default_storage as storage
from django.urls import reverse
from unittest import mock
import waffle
from unittest import mock
from pyquery import PyQuery as pq
from olympia import amo
@ -187,21 +185,6 @@ class TestUploadErrors(UploadMixin, TestCase):
xpi_info = check_xpi_info({'guid': long_guid, 'version': '1.0'})
assert xpi_info['guid'] == long_guid
def test_mv3_error_added(self):
validation = deepcopy(amo.VALIDATOR_SKELETON_EXCEPTION_WEBEXT)
validation['metadata']['manifestVersion'] = 3
xpi = self.get_upload(
'webextension_mv3.xpi',
with_validation=True,
validation=json.dumps(validation),
user=self.user,
)
res = self.client.get(reverse('devhub.upload_detail', args=[xpi.uuid, 'json']))
assert b'https://blog.mozilla.org/addons/2021/05/27/manifest-v3-update/' in (
res.content
)
class TestFileValidation(TestCase):
fixtures = ['base/users', 'devhub/addon-validation-1']

Просмотреть файл

@ -310,40 +310,6 @@ def wizard_unsupported_properties(data, wizard_fields):
return unsupported
def add_manifest_version_error(validation):
mv = validation.get('metadata', {}).get('manifestVersion')
if (
mv != 3
or waffle.switch_is_active('enable-mv3-submissions')
or 'messages' not in validation
):
return
msg = gettext(
'Manifest V3 is currently not supported for upload. '
'{start_href}Read more about the support timeline{end_href}.'
)
url = 'https://blog.mozilla.org/addons/2021/05/27/manifest-v3-update/'
start_href = f'<a href="{url}" target="_blank" rel="noopener">'
new_error_message = msg.format(start_href=start_href, end_href='</a>')
for index, message in enumerate(validation['messages']):
if message.get('dataPath') == '/manifest_version':
# if we find the linter manifest_version=3 warning, replace it
validation['messages'][index]['message'] = new_error_message
break
else:
# otherwise insert a new error at the start of the errors
validation['messages'].insert(
0,
{
'type': 'error',
'message': new_error_message,
'tier': 1,
'fatal': True,
},
)
@transaction.atomic
def create_version_for_upload(addon, upload, channel, parsed_data=None):
fileupload_exists = addon.fileupload_set.filter(

Просмотреть файл

@ -56,7 +56,6 @@ from olympia.api.models import APIKey, APIKeyConfirmation
from olympia.devhub.decorators import dev_required, no_admin_disabled
from olympia.devhub.models import BlogPost, RssKey
from olympia.devhub.utils import (
add_manifest_version_error,
extract_theme_properties,
wizard_unsupported_properties,
)
@ -757,6 +756,10 @@ def json_upload_detail(request, upload, addon_slug=None):
i,
{
'type': 'error',
# Actual validation messages coming from the linter are
# already escaped because they are coming from
# `processed_validation`, but we need to do that for
# those coming from ValidationError exceptions as well.
'message': escape_all(msg),
'tier': 1,
'fatal': True,
@ -768,7 +771,6 @@ def json_upload_detail(request, upload, addon_slug=None):
return json_view.error(result)
else:
result['addon_type'] = pkg.get('type', '')
add_manifest_version_error(result['validation'])
return result

Просмотреть файл

@ -1009,64 +1009,39 @@ pre.email_comment {
color: #777777;
}
#upload-file .submission-checklist {
#upload-file .submission-warning {
margin: 10px;
padding: 10px;
padding: 10px 10px 10px 52px;
border-radius: 10px;
border: 1px solid #F0B500;
}
#upload-file .submission-checklist h5 {
#upload-file .submission-warning h5 {
line-height: 32px;
}
#upload-file .submission-checklist h5:before {
#upload-file .submission-warning h5:before {
background-image: url('../../img/developers/test-warning.png');
width: 32px;
height: 32px;
margin-left: -42px;
margin-right: 10px;
content: " ";
display: inline-block;
vertical-align: middle;
}
#upload-file .submission-checklist ul {
#upload-file .submission-warning ul {
margin-left: 0;
}
#upload-file .submission-checklist li {
#upload-file .submission-warning li {
float: none;
}
#upload-file .submission-checklist a.review-process-overview {
#upload-file .submission-warning a.review-process-overview {
font-style: italic;
display: block;
margin-top: 10px;
}
#upload-file .important-warning {
margin: 10px;
padding: 10px;
border-radius: 10px;
border: 1px solid #F0B500;
}
#upload-file .important-warning h5 {
line-height: 32px;
}
#upload-file .important-warning h5:before {
background-image: url('../../img/developers/test-warning-important.png');
width: 32px;
height: 32px;
margin-right: 10px;
content: " ";
display: inline-block;
vertical-align: middle;
}
#upload-file .important-warning a.review-process-overview {
display: block;
margin-top: 10px;
}
/* @end */
/* @group Recent Activity */

Просмотреть файл

@ -539,10 +539,67 @@
$('<strong>').text(message).appendTo(upload_results);
let checklistWarningsIds = [
'NO_DOCUMENT_WRITE',
'DANGEROUS_EVAL',
'NO_IMPLIED_EVAL',
'UNSAFE_VAR_ASSIGNMENT',
'MANIFEST_CSP',
],
mv3NoticeId = '_MV3_COMPATIBILITY',
checklistMessages = [],
mv3CompatibilityMessage,
// this.id is in the form ["abc_def_ghi', 'foo_bar', 'something'],
// we usually only match one of the elements.
matchId = function (id) {
return this.hasOwnProperty('id') && _.contains(this.id, id);
};
if (results.validation.messages) {
for (var i = 0; i < results.validation.messages.length; i++) {
let current = results.validation.messages[i];
if (current.extra) {
// We want to ignore messages that are not coming from the
// linter in the logic that decides whether or not to show the
// submission checklist box. Those are tagged with extra: true.
messageCount--;
}
// Check for warnings we want to higlight specifically.
let matched = _.find(checklistWarningsIds, matchId, current);
if (matched) {
checklistMessages.push(gettext(current.message));
// We want only once every possible warning hit.
checklistWarningsIds.splice(
checklistWarningsIds.indexOf(matched),
1,
);
if (!checklistWarningsIds.length) break;
}
// Manifest v3 warning is a custom one added by addons-server
// that should be added once, regardless of whether or not we're
// displaying the submission warning box.
if (_.find([mv3NoticeId], matchId, current)) {
let mv3CompatibilityBox = $('<div>')
.attr('class', 'submission-warning')
.appendTo(upload_results);
$('<h5>').text(current.message).appendTo(mv3CompatibilityBox);
// That description is split into several paragraphs and can
// contain HTML for links.
current.description.forEach(function (item) {
$('<p>').html(item).appendTo(mv3CompatibilityBox);
});
}
}
}
if (messageCount > 0) {
// Validation checklist
var checklist_box = $('<div>')
.attr('class', 'submission-checklist')
// Validation checklist should be displayed if there is at least
// one message coming from the linter.
let checklist_box = $('<div>')
.attr('class', 'submission-warning')
.appendTo(upload_results),
checklist = [
gettext(
@ -551,25 +608,7 @@
gettext(
'If your add-on requires an account to a website in order to be fully tested, include a test username and password in the Notes to Reviewer (this can be done in the next step).',
),
],
warnings_id = [
'NO_DOCUMENT_WRITE',
'DANGEROUS_EVAL',
'NO_IMPLIED_EVAL',
'UNSAFE_VAR_ASSIGNMENT',
'MANIFEST_CSP',
'set_innerHTML',
'namespace_pollution',
'dangerous_global',
],
current,
matched,
messages = [],
// this.id is in the form ["testcases_javascript_instanceactions", "_call_expression", "createelement_variable"],
// we usually only match one of the elements.
matchId = function (id) {
return this.hasOwnProperty('id') && _.contains(this.id, id);
};
];
$('<h5>')
.text(gettext('Add-on submission checklist'))
@ -582,28 +621,18 @@
)
.appendTo(checklist_box);
if (results.validation.metadata.contains_binary_extension) {
messages.push(
checklistMessages.push(
gettext(
'Minified, concatenated or otherwise machine-generated scripts (excluding known libraries) need to have their sources submitted separately for review. Make sure that you use the source code upload field to avoid having your submission rejected.',
),
);
}
for (var i = 0; i < results.validation.messages.length; i++) {
current = results.validation.messages[i];
matched = _.find(warnings_id, matchId, current);
if (matched) {
messages.push(gettext(current.message));
// We want only once every possible warning hit.
warnings_id.splice(warnings_id.indexOf(matched), 1);
if (!warnings_id.length) break;
}
}
var checklist_ul = $('<ul>');
$.each(checklist, function (i) {
$('<li>').text(checklist[i]).appendTo(checklist_ul);
});
checklist_ul.appendTo(checklist_box);
if (messages.length) {
if (checklistMessages.length) {
$('<h6>')
.text(
gettext(
@ -612,12 +641,13 @@
)
.appendTo(checklist_box);
var messages_ul = $('<ul>');
$.each(messages, function (i) {
// Note: validation messages are supposed to be already escaped by
// devhub.views.json_upload_detail(), which does an escape_all()
// call on messages. So we need to use html() and not text() to
// display them, since they can contain HTML entities.
$('<li>').html(messages[i]).appendTo(messages_ul);
$.each(checklistMessages, function (i) {
// Note: validation messages can contain HTML, in the form of
// links or entities, because devhub.views.json_upload_detail()
// uses processed_validation with escapes and linkifies linter
// messages (and escape_all() on non-linter messages).
// So we need to use html() and not text() to display them.
$('<li>').html(checklistMessages[i]).appendTo(messages_ul);
});
messages_ul.appendTo(checklist_box);
}