diff --git a/optuna_dashboard/_custom_plot_data.py b/optuna_dashboard/_custom_plot_data.py index d669d4ad..a88fc0a4 100644 --- a/optuna_dashboard/_custom_plot_data.py +++ b/optuna_dashboard/_custom_plot_data.py @@ -48,10 +48,14 @@ def save_plotly_graph_object( The plotly's graph object to save. graph_object_id: Unique identifier of the graph object. If specified, the graph object is overwritten. + This must be a valid HTML id attribute value. Returns: The graph object ID. """ + if graph_object_id is not None and not is_valid_html_name(graph_object_id): + raise ValueError("graph_object_id must be a valid HTML id attribute value.") + storage = study._storage study_id = study._study_id @@ -113,3 +117,20 @@ def split_plot_data(plot_data_str: str, key_prefix: str) -> dict[str, str]: def concat_plot_data(plot_data_attrs: dict[str, str], key_prefix: str) -> str: return "".join(plot_data_attrs[f"{key_prefix}{i}"] for i in range(len(plot_data_attrs))) + + +def is_valid_html_name(graph_object_id: str) -> bool: + if len(graph_object_id) == 0: + return False + + # Must begin with a letter [A-Za-z] + if not ("a" <= graph_object_id[0] <= "z" or "A" <= graph_object_id[0] <= "Z"): + return False + + # Can only contain letters [A-Za-z], numbers [0-9], hyphens ("-"), underscores ("_"), colons, and periods. + if not all( + "a" <= c <= "z" or "A" <= c <= "Z" or "0" <= c <= "9" or c in ("-", "_", ":", ".") + for c in graph_object_id[1:] + ): + return False + return True diff --git a/python_tests/test_custom_plot_data.py b/python_tests/test_custom_plot_data.py index 4d01af96..f73f7097 100644 --- a/python_tests/test_custom_plot_data.py +++ b/python_tests/test_custom_plot_data.py @@ -3,6 +3,7 @@ from __future__ import annotations import optuna from optuna_dashboard import _custom_plot_data as custom_plot_data from optuna_dashboard import save_plotly_graph_object +import pytest def get_dummy_study() -> optuna.Study: @@ -59,3 +60,27 @@ def test_update_plotly_graph_object() -> None: plot_data_dict = custom_plot_data.get_plotly_graph_objects(study_system_attrs) assert len(plot_data_dict) == 1 assert plot_data_dict[graph_object_id] == plot_data.to_json() + + +@pytest.mark.parametrize( + "name", + [ + "a", + "a1-:_.", + ], +) +def test_is_valid_html_name(name): + assert custom_plot_data.is_valid_html_name(name) + + +@pytest.mark.parametrize( + "name", + [ + "0", + "a,", + "a b", + "aあいうえお", + ], +) +def test_is_invalid_html_name(name): + assert not custom_plot_data.is_valid_html_name(name)