Sanitize HTML content.

master
Pacman Ghost 2 years ago committed by Pacman Ghost
parent c420c168d1
commit 15c632b522
  1. 8
      vasl_templates/webapp/main.py
  2. 1679
      vasl_templates/webapp/static/DOMPurify/purify.js
  3. 1
      vasl_templates/webapp/static/DOMPurify/purify.js.map
  4. 3
      vasl_templates/webapp/static/DOMPurify/purify.min.js
  5. 1
      vasl_templates/webapp/static/DOMPurify/purify.min.js.map
  6. 22
      vasl_templates/webapp/static/html-editor.js
  7. 9
      vasl_templates/webapp/static/snippets.js
  8. 1
      vasl_templates/webapp/static/vassal.js
  9. 1
      vasl_templates/webapp/templates/index.html
  10. 4
      vasl_templates/webapp/tests/test_capabilities.py
  11. 347
      vasl_templates/webapp/tests/test_sanitize.py
  12. 6
      vasl_templates/webapp/tests/test_snippets.py
  13. 100
      vasl_templates/webapp/tests/test_vassal.py
  14. 2
      vasl_templates/webapp/tests/test_vehicles_ordnance.py

@ -167,6 +167,14 @@ def get_app_config():
# the Trumbowyg control is in a dialog, and given VASSAL's handling of images, we don't
# really want to be encouraging their use :-/
vals[ "trumbowyg" ] = {
# NOTE: Trumbowyg only allows tags to be black-listed, not attributes, but since we mostly
# do HTML sanitization using DOMPurify when loading/unloading the UI, what we configure here
# is only used to protect against the case where the user enters some malicious HTML into
# the editor i.e. the only risk is that they mess up their own session :-/
"tag-blacklist": get_json_val( "TRUMBOWYG_TAG_BLACKLIST", [
"script", "iframe", "form", "link", "style", "meta", "object", "applet",
"audio", "base", "bgsound", "embed", "isindex", "keygen", "layer", "svg", "template", "video",
] ),
"format-options": get_json_val( "TRUMBOWYG_FORMAT_OPTIONS", [
"h1", "h2", "h3",
] ),

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -53,6 +53,7 @@ function initTrumbowyg( $elem, buttons, $parentDlg )
},
},
},
tagsToRemove: gAppConfig.trumbowyg[ "tag-blacklist" ],
} ) ;
var $parent = $elem.parent() ;
var $btnPane = $parent.find( ".trumbowyg-button-pane" ) ;
@ -153,6 +154,7 @@ function unloadTrumbowyg( $elem, removeFirstPara )
val = strReplaceAll( val, "<br></p>", "</p>" ) ;
while ( val.substring( val.length-4 ) === "<br>" )
val = val.substring( 0, val.length-4 ).trim() ;
return val ;
}
@ -216,3 +218,23 @@ function updateTrumbowygFlagsDropdown( $elem )
if ( $btn )
$dropdown.prepend( $btn ) ;
}
// --------------------------------------------------------------------
function sanitizeParams( params )
{
// recursively sanitize the scenario params
for ( var key in params ) {
if ( ! params.hasOwnProperty( key ) )
continue ;
if ( typeof params[key] === "object" )
sanitizeParams( params[key] ) ;
else if ( typeof params[key] === "string" ) {
var sanitized = DOMPurify.sanitize(
params[key],
{ USE_PROFILES: { html: true } }
) ;
params[key] = sanitized ;
}
}
}

@ -43,6 +43,7 @@ function generate_snippet( $btn, as_image, extra_params )
// generate the snippet
var template_id = $btn.data( "id" ) ;
var params = unload_snippet_params( true, template_id ) ;
sanitizeParams( extra_params ) ;
var snippet = make_snippet( $btn, params, extra_params, true ) ;
// check if the user is requesting the snippet as an image
@ -1084,6 +1085,7 @@ function unload_snippet_params( unpack_scenario_date, template_id )
get_vo( "ordnance", 1, "OB_ORDNANCE_1", template_id === "ob_ordnance_1" ) ;
get_vo( "ordnance", 2, "OB_ORDNANCE_2", template_id === "ob_ordnance_2" ) ;
sanitizeParams( params ) ;
return params ;
}
@ -1716,6 +1718,12 @@ function do_load_scenario_data( params )
reset_scenario() ;
gScenarioCreatedTime = params._creation_time ;
// sanitize the HTML
if ( ! getUrlParam( "no_sanitize_load" ) ) {
// NOTE: This is optional to make it easier for the test suite to bulk-load unsafe content.
sanitizeParams( params ) ;
}
// auto-assign ID's to the OB setup notes and notes
// NOTE: We do this here to handle scenarios that were created before these ID's were implemented.
auto_assign_ids( params.SCENARIO_NOTES, "id" ) ;
@ -2096,6 +2104,7 @@ function unload_params_for_save( includeMetadata )
}
}
sanitizeParams( params ) ;
return params ;
}

@ -140,6 +140,7 @@ function _generate_snippets()
} else
snippet_id = template_id + "." + data.id ;
extra_params = get_simple_note_snippet_extra_params( $btn ) ;
sanitizeParams( extra_params ) ;
}
if ( ["ob_vehicle_note","ob_ordnance_note"].indexOf( template_id ) !== -1 ) {
data = $btn.parent().parent().data( "sortable2-data" ) ;

@ -137,6 +137,7 @@
<script src="{{url_for('static',filename='trumbowyg/plugins/indent/trumbowyg.indent.min.js')}}"></script>
<script src="{{url_for('static',filename='trumbowyg/plugins/specialchars/trumbowyg.specialchars.min.js')}}"></script>
<script src="{{url_for('static',filename='trumbowyg/plugins/table/trumbowyg.table.min.js')}}"></script>
<script src="{{url_for('static',filename='DOMPurify/purify.min.js')}}"></script>
<script>
gAppName = "{{APP_NAME}}" ;

@ -514,7 +514,7 @@ def test_custom_capabilities( webapp, webdriver ): #pylint: disable=too-many-sta
return elems
# check the vehicle's snippet
check_snippet( '"XYZ" "<span class=\'brewup\'>cs 4</span>"' )
check_snippet( '"XYZ" "<span class="brewup">cs 4</span>"' )
# edit the vehicle's capabilities
vehicles_sortable = find_child( "#ob_vehicles-sortable_1" )
@ -581,7 +581,7 @@ def test_custom_capabilities( webapp, webdriver ): #pylint: disable=too-many-sta
btn = find_child( "#vo_capabilities-reset" )
btn.click()
click_dialog_button( "OK" )
check_snippet( '"XYZ" "<span class=\'brewup\'>cs 4</span>"' )
check_snippet( '"XYZ" "<span class="brewup">cs 4</span>"' )
# make sure the custom capabilities are no longer saved in the scenario
saved_scenario2 = save_scenario()

@ -0,0 +1,347 @@
""" Test sanitizing HTML. """
import os
import re
from selenium.webdriver.common.action_chains import ActionChains
from vasl_templates.webapp.tests.utils import \
init_webapp, select_tab, click_dialog_button, load_trumbowyg, unload_trumbowyg, \
find_child, find_sortable_helper, wait_for_elem, wait_for_clipboard
from vasl_templates.webapp.tests.test_vassal import \
run_vassal_tests, update_vsav_and_dump, get_vsav_labels, disable_snippet_images
from vasl_templates.webapp.tests.test_scenario_persistence import load_scenario, save_scenario
# ---------------------------------------------------------------------
def test_sanitize_load_scenario( webapp, webdriver ):
"""Test sanitization of HTML content in scenarios as they are loaded."""
# initialize
# NOTE: The Trumbowyg tag black-list is active, which will affect results (by removing tags *and their contents).
webapp.control_tests.set_vo_notes_dir( "{TEST}" )
init_webapp( webapp, webdriver, scenario_persistence=1 )
# load a scenario with unsafe content
load_scenario( _make_scenario_params( False ) )
def check_val( name, expected ):
elem = find_child( ".param[name='{}']".format( name ) )
assert elem.get_attribute( "value" ) == expected
def check_trumbowyg( name, expected ):
assert unload_trumbowyg( name ) == expected
def check_sortable( sortable_sel, expected, expected_width ):
sortable = find_child( sortable_sel )
entry = find_child( "li", sortable ) # nb: we assume there's only 1 entry
assert entry.text == expected
if expected_width:
ActionChains( webdriver ).double_click( entry ).perform()
elem = find_child( ".ui-dialog-buttonpane input[name='width']" )
assert elem.get_attribute( "value" ) == expected_width
click_dialog_button( "OK" )
def check_custom_cap( sortable_sel, expected ):
elem = find_child( "{} li".format( sortable_sel ) ) # nb: we assume there's only 1 entry
ActionChains( webdriver ).double_click( elem ).perform()
elem = find_child( ".ui-dialog.edit-vo .sortable input" )
assert elem.get_attribute( "value" ) == expected
click_dialog_button( "Cancel" )
# check what was loaded into the UI
# NOTE: We can't use save_scenario), since that also sanitizes HTML.
check_val( "SCENARIO_NAME", "!scenario_name:#" )
check_val( "SCENARIO_ID", "!scenario_id:@@@#" )
check_val( "SCENARIO_LOCATION", "!scenario_location:<div style=\"text-align:right;\">@@@</div>#" )
check_val( "SCENARIO_WIDTH", "!scenario_width:@@@#" )
check_val( "PLAYER_1_DESCRIPTION", "!player1_description:#" )
check_val( "PLAYER_2_DESCRIPTION", "!player2_description:#" )
check_val( "PLAYERS_WIDTH", "!players_width:@@@#" )
check_trumbowyg( "VICTORY_CONDITIONS", "!victory_conditions:@@@#" )
check_val( "VICTORY_CONDITIONS_WIDTH", "!victory_conditions_width:@@@#" )
check_sortable( "#scenario_notes-sortable", "!scenario_note:#", "!scenario_note_width:@@@#" )
check_sortable( "#ssr-sortable", "!ssr:#", None )
check_val( "SSR_WIDTH", "!ssr_width:@@@#" )
# check what was loaded into the UI
for player_no in (1,2):
select_tab( "ob{}".format( player_no ) )
check_sortable( "#ob_setups-sortable_{}".format( player_no ), "!ob_setup:#", "!ob_setup_width:@@@#" )
check_sortable( "#ob_notes-sortable_{}".format( player_no ), "!ob_note:#", "!ob_note_width:@@@#" )
check_val( "OB_VEHICLES_WIDTH_{}".format( player_no ), "!ob_vehicles_width:@@@#" )
check_val( "OB_VEHICLES_MA_NOTES_WIDTH_{}".format( player_no ), "!ob_vehicles_ma_notes_width:@@@#" )
check_val( "OB_ORDNANCE_WIDTH_{}".format( player_no ), "!ob_ordnance_width:@@@#" )
check_val( "OB_ORDNANCE_MA_NOTES_WIDTH_{}".format( player_no ), "!ob_ordnance_ma_notes_width:@@@#" )
check_custom_cap( "#ob_vehicles-sortable_{}".format( player_no ), "!custom_cap:@@@#" )
check_custom_cap( "#ob_ordnance-sortable_{}".format( player_no ), "!custom_cap:@@@#" )
# ---------------------------------------------------------------------
def test_sanitize_save_scenario( webapp, webdriver, monkeypatch ):
"""Test sanitization of HTML content when saving scenarios."""
# initialize
monkeypatch.setitem( webapp.config, "TRUMBOWYG_TAG_BLACKLIST", "[]" )
webapp.control_tests.set_vo_notes_dir( "{TEST}" )
init_webapp( webapp, webdriver, no_sanitize_load=1, scenario_persistence=1 )
# load a scenario with unsafe content
load_scenario( _make_scenario_params( False ) )
# unload the scenario
params = save_scenario()
# NOTE: It's a bit tedious to have to list every single parameter in a save file, but this lets us detect
# the case where a new parameter has been added, and we haven't updated these sanitization tests for it.
for key in ( "SCENARIO_THEATER", "PLAYER_1_ELR", "PLAYER_1_SAN", "PLAYER_2_ELR", "PLAYER_2_SAN" ):
params.pop( key )
params = { k: v for k, v in params.items() if not k[0] == "_" }
assert params == {
"PLAYER_1": "german",
"PLAYER_2": "russian",
"SCENARIO_NAME": "!scenario_name:#",
"SCENARIO_ID": "!scenario_id:@@@#",
"SCENARIO_LOCATION": "!scenario_location:<div style=\"text-align:right;\">@@@</div>#",
"SCENARIO_WIDTH": "!scenario_width:@@@#",
"PLAYER_1_DESCRIPTION": "!player1_description:#",
"PLAYER_2_DESCRIPTION": "!player2_description:#",
"PLAYERS_WIDTH": "!players_width:@@@#",
"VICTORY_CONDITIONS": "!victory_conditions:@@@#",
"VICTORY_CONDITIONS_WIDTH": "!victory_conditions_width:@@@#",
"SCENARIO_NOTES": [ { "id": 1,
"caption": "!scenario_note:#",
"width": "!scenario_note_width:@@@#"
} ],
"SSR": [ "!ssr:#" ],
"SSR_WIDTH": "!ssr_width:@@@#",
#
"OB_SETUPS_1": [ { "id": 1, "caption": "!ob_setup:#", "width": "!ob_setup_width:@@@#" } ],
"OB_NOTES_1": [ { "id": 1, "caption": "!ob_note:#", "width": "!ob_note_width:@@@#" } ],
"OB_VEHICLES_1": [ { "seq_id": 1,
"id": "ge/v:990", "name": "a german vehicle",
"custom_capabilities": [ "!custom_cap:@@@#" ]
} ],
"OB_VEHICLES_WIDTH_1": "!ob_vehicles_width:@@@#",
"OB_VEHICLES_MA_NOTES_WIDTH_1": "!ob_vehicles_ma_notes_width:@@@#",
"OB_ORDNANCE_1": [ { "seq_id": 1,
"id": "ge/o:990", "name": "a german ordnance",
"custom_capabilities": [ "!custom_cap:@@@#" ]
} ],
"OB_ORDNANCE_WIDTH_1": "!ob_ordnance_width:@@@#",
"OB_ORDNANCE_MA_NOTES_WIDTH_1": "!ob_ordnance_ma_notes_width:@@@#",
#
"OB_SETUPS_2": [ { "id": 1, "caption": "!ob_setup:#", "width": "!ob_setup_width:@@@#" } ],
"OB_NOTES_2": [ { "id": 1, "caption": "!ob_note:#", "width": "!ob_note_width:@@@#" } ],
"OB_VEHICLES_2": [ { "seq_id": 1,
"id": "ru/v:990", "name": "a russian vehicle",
"custom_capabilities": [ "!custom_cap:@@@#" ]
} ],
"OB_VEHICLES_WIDTH_2": "!ob_vehicles_width:@@@#",
"OB_VEHICLES_MA_NOTES_WIDTH_2": "!ob_vehicles_ma_notes_width:@@@#",
"OB_ORDNANCE_2": [ { "seq_id": 1,
"id": "ru/o:990", "name": "a russian ordnance",
"custom_capabilities": [ "!custom_cap:@@@#" ]
} ],
"OB_ORDNANCE_WIDTH_2": "!ob_ordnance_width:@@@#",
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "!ob_ordnance_ma_notes_width:@@@#",
#
"ASA_ID": "", "ROAR_ID": "",
"COMPASS": "", "SCENARIO_DATE": "",
}
# ---------------------------------------------------------------------
def test_sanitize_update_vsav( webapp, webdriver, monkeypatch ):
"""Test sanitization of HTML content when updating a VASL save file."""
def do_test():
# initialize
monkeypatch.setitem( webapp.config, "TRUMBOWYG_TAG_BLACKLIST", "[]" )
webapp.control_tests \
.set_data_dir( "{REAL}" ) \
.set_vo_notes_dir( "{TEST}" )
init_webapp( webapp, webdriver, no_sanitize_load=1, scenario_persistence=1, vsav_persistence=1 )
disable_snippet_images()
# load a scenario with unsafe content
load_scenario( _make_scenario_params( True ) )
# update the VSAV, then dump it
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/empty.vsav" )
vsav_dump = update_vsav_and_dump( webapp, fname, { "created": 22 } )
labels, _ = get_vsav_labels( vsav_dump )
# remove labels we don't need to check
for player_no, nat in ( (1,"german"), (2,"russian") ):
for key in ( "{}/nat_caps_{}", "{}/ob_vehicle_note_{}.1", "{}/ob_ordnance_note_{}.1" ):
key = key.format( nat, player_no )
assert all(
tag not in labels[key]
for tag in ( "<script", "<iframe", "<object", "<applet", "<x" )
)
del labels[ key ]
# check the labels in the VSAV
expected = {
"scenario": [
r'width: !scenario_width:@@@# ;',
r'!scenario_name:#',
r'\(!scenario_id:@@@#\)',
r'!scenario_location:<div style="text-align:right;">@@@</div>#'
],
"players": [
r'width: !players_width:@@@# ;',
r'!player1_description:#',
r'!player2_description:#'
],
"victory_conditions": [
r'width: !victory_conditions_width:@@@# ;',
r'!victory_conditions:@@@#'
],
"ssr": [
r'width: !ssr_width:@@@# ;',
r'!ssr:#'
],
"scenario_note.1": [
r'width: !scenario_note_width:@@@# ;',
r'!scenario_note:#'
],
"german/ob_setup_1.1": [
r'width: !ob_setup_width:@@@# ;',
r'!ob_setup:#'
],
"german/ob_note_1.1": [
r'width: !ob_note_width:@@@# ;',
r'!ob_note:#'
],
"german/ob_vehicles_1": [
"width: !ob_vehicles_width:@@@# ;",
"!custom_cap:@@@#"
],
"german/ob_vehicles_ma_notes_1": [ "width: !ob_vehicles_ma_notes_width:@@@# ;" ],
"german/ob_ordnance_1": [
"width: !ob_ordnance_width:@@@# ;",
"!custom_cap:@@@#"
],
"german/ob_ordnance_ma_notes_1": [ "width: !ob_ordnance_ma_notes_width:@@@# ;" ],
"russian/ob_setup_2.1": [
r'width: !ob_setup_width:@@@# ;',
r'!ob_setup:#'
],
"russian/ob_note_2.1": [
r'width: !ob_note_width:@@@# ;',
r'!ob_note:#'
],
"russian/ob_vehicles_2": [
"width: !ob_vehicles_width:@@@# ;",
"!custom_cap:@@@#"
],
"russian/ob_vehicles_ma_notes_2": [ "width: !ob_vehicles_ma_notes_width:@@@# ;" ],
"russian/ob_ordnance_2": [
"width: !ob_ordnance_width:@@@# ;",
"!custom_cap:@@@#"
],
"russian/ob_ordnance_ma_notes_2": [ "width: !ob_ordnance_ma_notes_width:@@@# ;" ],
}
for snippet_id in list( labels.keys() ):
if snippet_id in expected:
label = labels.pop( snippet_id )
regex = re.compile( ".*".join( expected.pop( snippet_id ) ) )
assert regex.search( label )
assert len( labels ) == 0
# do the test
run_vassal_tests( webapp, do_test, all_combos=False )
# ---------------------------------------------------------------------
def test_sanitize_input( webapp, webdriver ):
"""Test sanitizing HTML as it is entered in the UI."""
# initialize
# NOTE: The Trumbowyg tag black-list is active, which will affect results (by removing tags *and their contents).
init_webapp( webapp, webdriver, scenario_persistence=1 )
# NOTE: We don't sanitize things like the scenario name, since it is entered into an <input> textbox,
# and so can't do any harm (these are sanitized when the scenario is unloaded from the UI).
# test sanitizing HTML in Trumbowyg controls
# NOTE: Trumbowyg only sanitizes tags, not attributes.
elem = find_child( ".param[name='VICTORY_CONDITIONS']" )
load_trumbowyg( elem, "foo <script>xyz</script> bar" )
find_child( ".trumbowyg-viewHTML-button" ).click()
# FUDGE! We need to switch back to raw HTML mode for the <textarea> to be updated with the sanitized HTML.
find_child( ".trumbowyg-viewHTML-button" ).click()
assert unload_trumbowyg( elem ) == "foo bar"
# test sanitizing HTML in a simple note dialog that hasn't been saved yet
sortable = find_child( "#scenario_notes-sortable" )
find_sortable_helper( sortable, "add" ).click()
dlg = wait_for_elem( 2, ".ui-dialog.edit-simple_note" )
load_trumbowyg( find_child( ".trumbowyg-editor", dlg ),
"foo <script>format_hdd();</script> bar"
)
find_child( "button.snippet", dlg ).click()
wait_for_clipboard( 2, "foo bar", contains=True )
click_dialog_button( "OK" )
# ---------------------------------------------------------------------
def _make_scenario_params( real_vo ):
"""Generate scenario parameters that contain unsafe HTML."""
# generate the scenario parameters
params = {
"PLAYER_1": "german",
"PLAYER_2": "russian",
"SCENARIO_NAME": "!scenario_name:<script>@@@</script>#",
"SCENARIO_ID": "!scenario_id:<x>@@@</x>#",
"SCENARIO_LOCATION": "!scenario_location:<div foo='bar' style='text-align:right;'>@@@</div>#",
"SCENARIO_WIDTH": "!scenario_width:<x>@@@</x>#",
"PLAYER_1_DESCRIPTION": "!player1_description:<iframe>@@@</iframe>#",
"PLAYER_2_DESCRIPTION": "!player2_description:<iframe>@@@</iframe>#",
"PLAYERS_WIDTH": "!players_width:<x>@@@</x>#",
"VICTORY_CONDITIONS": "!victory_conditions:<applet>@@@</applet>#",
"VICTORY_CONDITIONS_WIDTH": "!victory_conditions_width:<x>@@@</x>#",
"SCENARIO_NOTES": [
{ "caption": "!scenario_note:<iframe>@@@</iframe>#", "width": "!scenario_note_width:<x>@@@</x>#" }
],
"SSR": [ "!ssr:<iframe>@@@</iframe>#" ],
"SSR_WIDTH": "!ssr_width:<x>@@@</x>#",
}
# add in player-specific parameters
params.update( {
"OB_VEHICLES_1": [ {
"name": "PzKpfw IB" if real_vo else "a german vehicle",
"custom_capabilities": [ "!custom_cap:<x>@@@</x>#" ]
} ],
"OB_ORDNANCE_1": [ {
"name": "5cm leGrW" if real_vo else "a german ordnance",
"custom_capabilities": [ "!custom_cap:<x>@@@</x>#" ]
} ],
"OB_VEHICLES_2": [ {
"name": "T-37" if real_vo else "a russian vehicle",
"custom_capabilities": [ "!custom_cap:<x>@@@</x>#" ]
} ],
"OB_ORDNANCE_2": [ {
"name": "50mm RM obr. 40" if real_vo else "a russian ordnance",
"custom_capabilities": [ "!custom_cap:<x>@@@</x>#" ]
} ],
} )
for player_no in (1,2):
params.update( {
"OB_SETUPS_{}".format( player_no ): [ {
"caption": "!ob_setup:<script foo='bar' style='text-align:right;'>@@@</script>#",
"width": "!ob_setup_width:<x>@@@</x>#"
} ],
"OB_NOTES_{}".format( player_no ): [ {
"caption": "!ob_note:<iframe foo='bar' style='text-align:right;'>@@@</iframe>#",
"width": "!ob_note_width:<x>@@@</x>#"
} ],
"OB_VEHICLES_WIDTH_{}".format( player_no ): "!ob_vehicles_width:<x>@@@</x>#",
"OB_VEHICLES_MA_NOTES_WIDTH_{}".format( player_no ): "!ob_vehicles_ma_notes_width:<x>@@@</x>#",
"OB_ORDNANCE_WIDTH_{}".format( player_no ): "!ob_ordnance_width:<x>@@@</x>#",
"OB_ORDNANCE_MA_NOTES_WIDTH_{}".format( player_no ): "!ob_ordnance_ma_notes_width:<x>@@@</x>#",
} )
return params

@ -123,12 +123,12 @@ def test_scenario_snippets( webapp, webdriver ):
# generate a SCENARIO snippet with non-English content and HTML special characters
_test_snippet( btn, {
"SCENARIO_NAME": "<foo> & <bar>",
"SCENARIO_NAME": "& > <",
"SCENARIO_LOCATION": "japan (\u65e5\u672c)",
"SCENARIO_DATE": "01/02/1942",
"SCENARIO_WIDTH": "",
},
'name = [<foo> & <bar>] | loc = [japan (\u65e5\u672c)] | date = [01/02/1942] aka "2 January, 1942"',
'name = [&amp; &gt; &lt;] | loc = [japan (\u65e5\u672c)] | date = [01/02/1942] aka "2 January, 1942"',
None
)
@ -194,7 +194,7 @@ def test_scenario_notes_snippets( webapp, webdriver ):
# add a scenario note with non-English content and HTML special characters
sortable = find_child( "#scenario_notes-sortable" )
add_simple_note( sortable, "japan <\u65e5\u672c>", None )
assert generate_sortable_entry_snippet( sortable, 0 ) == "[japan <\u65e5\u672c>]"
assert generate_sortable_entry_snippet( sortable, 0 ) == "[japan &lt;\u65e5\u672c&gt;]"
# ---------------------------------------------------------------------

@ -29,7 +29,7 @@ def test_full_update( webapp, webdriver ):
.set_data_dir( "{REAL}" ) \
.set_vo_notes_dir( "{TEST}" if enable_vo_notes else None )
init_webapp( webapp, webdriver, vsav_persistence=1, no_app_config_snippet_params=1 )
_disable_snippet_images()
disable_snippet_images()
# load the scenario fields
SCENARIO_PARAMS = {
@ -139,7 +139,7 @@ def test_full_update( webapp, webdriver ):
temp_file.close( delete=False )
updated_vsav_dump = _dump_vsav( webapp, temp_file.name )
expected = {
"scenario": "Modified scenario name (<>{}\"'\\)",
"scenario": "Modified scenario name (&lt;&gt;{}\"'\\)",
"players": re.compile( r"American:.*Belgian:" ),
"victory_conditions": "Just do it!",
"ssr": re.compile( r"Modified SSR #1.*Modified SSR #2" ),
@ -218,7 +218,7 @@ def test_latw_autocreate( webapp, webdriver ):
# initialize
webapp.control_tests.set_data_dir( "{REAL}" )
init_webapp( webapp, webdriver, vsav_persistence=1 )
_disable_snippet_images()
disable_snippet_images()
# check the VASL scenario
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/empty.vsav" )
@ -227,7 +227,7 @@ def test_latw_autocreate( webapp, webdriver ):
# update the scenario (German/Russian, no date)
load_scenario_params( { "scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "" } } )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname, { "created": 5 } )
updated_vsav_dump = update_vsav_and_dump( webapp, fname, { "created": 5 } )
_check_vsav_dump( updated_vsav_dump, {
# nb: no LATW labels should have been created
}, ignore_labels )
@ -236,7 +236,7 @@ def test_latw_autocreate( webapp, webdriver ):
load_scenario_params( {
"scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "10/01/1943" }
} )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname, { "created": 6 } )
updated_vsav_dump = update_vsav_and_dump( webapp, fname, { "created": 6 } )
_check_vsav_dump( updated_vsav_dump, {
"german/pf": "Panzerfaust",
}, ignore_labels )
@ -245,14 +245,14 @@ def test_latw_autocreate( webapp, webdriver ):
load_scenario_params( {
"scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "01/01/1944" }
} )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname, { "created": 7 } )
updated_vsav_dump = update_vsav_and_dump( webapp, fname, { "created": 7 } )
_check_vsav_dump( updated_vsav_dump, {
"german/pf": "Panzerfaust", "german/atmm": "ATMM check:",
}, ignore_labels )
# update the scenario (British/American, no date)
load_scenario_params( { "scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "" } } )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname, { "created": 5 } )
updated_vsav_dump = update_vsav_and_dump( webapp, fname, { "created": 5 } )
_check_vsav_dump( updated_vsav_dump, {
# nb: no LATW labels should have been created
}, ignore_labels )
@ -261,7 +261,7 @@ def test_latw_autocreate( webapp, webdriver ):
load_scenario_params( {
"scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "12/31/1945" }
} )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname, { "created": 5 } )
updated_vsav_dump = update_vsav_and_dump( webapp, fname, { "created": 5 } )
_check_vsav_dump( updated_vsav_dump, {
# nb: no LATW labels should have been created
}, ignore_labels )
@ -287,7 +287,7 @@ def test_latw_update( webapp, webdriver ):
# initialize
webapp.control_tests.set_data_dir( "{REAL}" )
init_webapp( webapp, webdriver, vsav_persistence=1 )
_disable_snippet_images()
disable_snippet_images()
# check the VASL scenario
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/latw.vsav" )
@ -304,7 +304,7 @@ def test_latw_update( webapp, webdriver ):
# NOTE: We changed the MOL-P template (to add custom list bullets), so the snippet is different
# to when this test was originally written, and so #updated changed from 2 to 3.
# NOTE: Same thing happened when we factored out the common CSS into common.css :-/ Sigh...
updated_vsav_dump = _update_vsav_and_dump( webapp, fname,
updated_vsav_dump = update_vsav_and_dump( webapp, fname,
{ "created": 5, "updated": 5 }
)
_check_vsav_dump( updated_vsav_dump, {
@ -319,7 +319,7 @@ def test_latw_update( webapp, webdriver ):
load_scenario_params( {
"scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "12/31/1943" }
} )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname,
updated_vsav_dump = update_vsav_and_dump( webapp, fname,
{ "created": 5, "updated": 2 }
)
_check_vsav_dump( updated_vsav_dump, {
@ -371,15 +371,15 @@ def test_update_legacy_labels( webapp, webdriver ):
.set_data_dir( "{REAL}" ) \
.set_vo_notes_dir( "{TEST}" if enable_vo_notes else None )
init_webapp( webapp, webdriver, vsav_persistence=1, scenario_persistence=1 )
_disable_snippet_images()
disable_snippet_images()
# dump the VASL scenario
# NOTE: We implemented snippet ID's in v0.5, this scenario is the "Hill 621" example from v0.4.
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/hill621-legacy.vsav" )
vsav_dump = _dump_vsav( webapp, fname )
labels = _get_vsav_labels( vsav_dump )
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 20
assert len( [ lbl for lbl in labels if "vasl-templates:id" in lbl ] ) == 0 #pylint: disable=len-as-condition
ours, others = get_vsav_labels( vsav_dump )
assert len( ours ) == 0 #pylint: disable=len-as-condition
assert len( others ) == 20
# load the scenario into the UI and update the VSAV
fname2 = change_extn( fname, ".json" )
@ -387,15 +387,15 @@ def test_update_legacy_labels( webapp, webdriver ):
saved_scenario = json.load( fp )
load_scenario( saved_scenario )
expected = 7 if enable_vo_notes else 3
updated_vsav_dump = _update_vsav_and_dump( webapp, fname,
updated_vsav_dump = update_vsav_and_dump( webapp, fname,
{ "created": expected, "updated": 20 }
)
# check the results
# nb: the update process should create 1 new label (the "Download from MMP" scenario note)
labels = _get_vsav_labels( updated_vsav_dump )
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 0 #pylint: disable=len-as-condition
assert len( [ lbl for lbl in labels if "vasl-templates:id" in lbl ] ) == 27 if enable_vo_notes else 21
ours, others = get_vsav_labels( updated_vsav_dump )
assert len( ours ) == 27 if enable_vo_notes else 21
assert len( others ) == 0 #pylint: disable=len-as-condition
expected = {
"scenario": "Near Minsk",
"players": re.compile( r"Russian:.*German:" ),
@ -462,15 +462,15 @@ def test_update_legacy_latw_labels( webapp, webdriver ):
# initialize
webapp.control_tests.set_data_dir( "{REAL}" )
init_webapp( webapp, webdriver, vsav_persistence=1, scenario_persistence=1 )
_disable_snippet_images()
disable_snippet_images()
# dump the VASL scenario
# NOTE: This scenario contains LATW labels created using v0.4 i.e. they have no snippet ID's.
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/latw-legacy.vsav" )
vsav_dump = _dump_vsav( webapp, fname )
labels = _get_vsav_labels( vsav_dump )
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 8
assert len( [ lbl for lbl in labels if "vasl-templates:id" in lbl ] ) == 0 #pylint: disable=len-as-condition
ours, others = get_vsav_labels( vsav_dump )
assert len( ours ) == 0 #pylint: disable=len-as-condition
assert len( others ) == 8
# NOTE: We're only interested in what happens with the LATW labels, ignore everything else
ignore_labels = [ "scenario", "players", "victory_conditions",
@ -481,57 +481,57 @@ def test_update_legacy_latw_labels( webapp, webdriver ):
load_scenario_params( {
"scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "12/31/1945" }
} )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname,
updated_vsav_dump = update_vsav_and_dump( webapp, fname,
{ "created": 5, "updated": 5 }
)
_check_vsav_dump( updated_vsav_dump, {
"german/pf": "Panzerfaust", "german/psk": "Panzerschrek", "german/atmm": "ATMM check:",
"russian/mol": "Kindling Attempt:", "russian/mol-p": "TH#",
}, ignore_labels )
labels = _get_vsav_labels( updated_vsav_dump )
_, others = get_vsav_labels( updated_vsav_dump )
# nb: the legacy labels left in place: the scenario comment, and the PIAT/BAZ labels
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 3
assert len( others ) == 3
# update the VSAV (all LATW are active)
load_scenario_params( {
"scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "12/31/1945" }
} )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname,
updated_vsav_dump = update_vsav_and_dump( webapp, fname,
{ "created": 5, "updated": 2 }
)
_check_vsav_dump( updated_vsav_dump, {
"british/piat": "PIAT",
"american/baz": "Bazooka ('45)",
}, ignore_labels )
labels = _get_vsav_labels( updated_vsav_dump )
_, others = get_vsav_labels( updated_vsav_dump )
# nb: the legacy labels left in place: the scenario comment, the PF/PSK/ATMM and MOL/MOL-P labels
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 6
assert len( others ) == 6
# update the VSAV (some LATW are active)
load_scenario_params( { "scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "" } } )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname,
updated_vsav_dump = update_vsav_and_dump( webapp, fname,
{ "created": 5, "updated": 5 }
)
_check_vsav_dump( updated_vsav_dump, {
"german/pf": "Panzerfaust", "german/psk": "Panzerschrek", "german/atmm": "ATMM check:",
"russian/mol": "Kindling Attempt:", "russian/mol-p": "TH#",
}, ignore_labels )
labels = _get_vsav_labels( updated_vsav_dump )
_, others = get_vsav_labels( updated_vsav_dump )
# nb: the legacy labels left in place: the scenario comment, the PIAT/BAZ labels
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 3
assert len( others ) == 3
# update the VSAV (some LATW are active)
load_scenario_params( { "scenario": { "PLAYER_1": "british", "PLAYER_2": "american", "SCENARIO_DATE": "" } } )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname,
updated_vsav_dump = update_vsav_and_dump( webapp, fname,
{ "created": 5, "updated": 2 }
)
_check_vsav_dump( updated_vsav_dump, {
"british/piat": "PIAT",
"american/baz": "Bazooka",
}, ignore_labels )
labels = _get_vsav_labels( updated_vsav_dump )
_, others = get_vsav_labels( updated_vsav_dump )
# nb: the legacy labels left in place: the scenario comment, the PF/PSK/ATMM, MOL/MOL-P and BAZ labels
assert len( [ lbl for lbl in labels if "vasl-templates:id" not in lbl ] ) == 6
assert len( others ) == 6
# run the test against all versions of VASSAL+VASL
run_vassal_tests( webapp, do_test )
@ -546,7 +546,7 @@ def test_player_owned_labels( webapp, webdriver ):
# initialize
webapp.control_tests.set_data_dir( "{REAL}" )
init_webapp( webapp, webdriver, vsav_persistence=1 )
_disable_snippet_images()
disable_snippet_images()
load_scenario_params( {
"scenario": {
"SCENARIO_NAME": "Player-owned labels",
@ -564,7 +564,7 @@ def test_player_owned_labels( webapp, webdriver ):
# - scenario (timestamp)
# - players (new American player)
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/player-owned-labels-legacy.vsav" )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname,
updated_vsav_dump = update_vsav_and_dump( webapp, fname,
{ "created": 2, "updated": 4 }
)
_check_vsav_dump( updated_vsav_dump , {
@ -583,7 +583,7 @@ def test_player_owned_labels( webapp, webdriver ):
# - players (new American player)
# The existing Russian OB setup label should be ignored and left in-place.
fname = os.path.join( os.path.split(__file__)[0], "fixtures/update-vsav/player-owned-labels.vsav" )
updated_vsav_dump = _update_vsav_and_dump( webapp, fname,
updated_vsav_dump = update_vsav_and_dump( webapp, fname,
{ "created": 3, "updated": 2 }
)
_check_vsav_dump( updated_vsav_dump , {
@ -930,7 +930,7 @@ def _update_vsav( fname, expected ):
return updated_vsav_data
def _update_vsav_and_dump( webapp, fname, expected ):
def update_vsav_and_dump( webapp, fname, expected ):
"""Update a VASL scenario and dump the result."""
# update the VSAV
@ -953,11 +953,7 @@ def _check_vsav_dump( vsav_dump, expected, ignore=None ):
# extract the information of interest from the dump
labels = {}
for label in _get_vsav_labels(vsav_dump):
mo2 = re.search( r"<!-- vasl-templates:id (.*?) -->", label, re.DOTALL )
if not mo2:
continue # nb: this is not one of ours
snippet_id = mo2.group( 1 )
for snippet_id, label in get_vsav_labels( vsav_dump )[0].items():
if snippet_id.startswith( "extras/" ):
continue
for tag in [ "b", "em" ]:
@ -984,8 +980,9 @@ def _check_vsav_dump( vsav_dump, expected, ignore=None ):
print( "Extra label in the VASL scenario: {}".format( snippet_id ) )
assert False
def _get_vsav_labels( vsav_dump ):
def get_vsav_labels( vsav_dump ):
"""Extract the labels from a VSAV dump."""
# extract the labels from the VSAV dump
matches = re.finditer( r"AddPiece: DynamicProperty/User-Labeled.*?^\s*?(?=[^- ])",
vsav_dump,
re.MULTILINE+re.DOTALL
@ -993,7 +990,16 @@ def _get_vsav_labels( vsav_dump ):
labels = [ mo.group() for mo in matches ]
regex = re.compile( r"<html>.*?</html>" )
matches = [ regex.search(label) for label in labels ]
return [ mo.group() if mo else "<???>" for mo in matches ]
labels = [ mo.group() if mo else "<???>" for mo in matches ]
# identfy which labels belong to us
ours, others = {}, []
for label in labels:
mo = re.search( r"<!-- vasl-templates:id (.*?) -->", label )
if mo:
ours[ mo.group(1) ] = label
else:
others.append( label )
return ours, others
# ---------------------------------------------------------------------
@ -1038,7 +1044,7 @@ def analyze_vsav( fname, expected_ob1, expected_ob2, expected_report ):
msg = get_stored_msg( "_last-warning_" )
assert all( e in msg for e in expected_report )
def _disable_snippet_images():
def disable_snippet_images():
"""Disable images in snippets."""
# NOTE: These used to default to off, but we changed that to on (v1.9), and since this will cause many labels
# to update when we are not expecting them to (because they now have an image in them), we turn everything off

@ -161,7 +161,7 @@ def test_snippets( webapp, webdriver ):
btn.click()
caps = '"XYZ"'
if vo_type == "vehicles":
caps += ' "<span class=\'brewup\'>cs 4</span>"'
caps += ' "<span class="brewup">cs 4</span>"'
expected = [
'[German] ; width=',
'[*] another german {}: #=2'.format( vo_type0 ),

Loading…
Cancel
Save