Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion gramps_webapi/api/resources/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from __future__ import annotations

import copy
import gzip
import logging
import os
Expand Down Expand Up @@ -1273,19 +1274,26 @@ def validate_object_dict(obj_dict: dict[str, Any]) -> bool:
return False
schema = obj_cls.get_schema()

obj_dict_fixed = {k: v for k, v in obj_dict.items() if k != "complete"}

# Gramps 5.2 added Person.OTHER = 3, but the JSON schema still caps gender
# at 2. Patch the schema to allow the actual maximum value.
# This patch can be removed once https://github.com/gramps-project/gramps/pull/2213
# is merged and a new Gramps version is released.
other = getattr(obj_cls, "OTHER", None)
if (
other is not None
and obj_dict_fixed.get("gender") == other
and schema.get("properties", {}).get("gender", {}).get("maximum") is not None
and other > schema["properties"]["gender"]["maximum"]
):
# `get_schema()` may return an object shared across calls (e.g. a
# cached class-level schema); never mutate it in place, since that
# would leak this one-off patch into every other caller. Copy it
# first, since we only need a locally patched view for validation.
schema = copy.deepcopy(schema)
schema["properties"]["gender"]["maximum"] = other

obj_dict_fixed = {k: v for k, v in obj_dict.items() if k != "complete"}
try:
jsonschema.validate(obj_dict_fixed, schema)
except jsonschema.exceptions.ValidationError as exc:
Expand Down
37 changes: 37 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,40 @@ def test_recalc_date_sortvals_year_only():
date_dict["sortval"] = 0
recalc_date_sortvals({"_class": "Event", "date": date_dict})
assert date_dict["sortval"] == correct


def test_validate_object_dict_does_not_mutate_shared_schema():
"""validate_object_dict() must not mutate a shared/cached get_schema() result.

Gramps core may cache Person.get_schema() and return the same object to
every caller. The gender-max patch here (for Person.OTHER, added in
Gramps 5.2) previously mutated that returned schema in place, which
would corrupt it for every other caller of get_schema() once Gramps
starts sharing/caching it, or raise outright if the shared object is
made read-only.
"""
from gramps.gen.lib import Person

from gramps_webapi.api.resources.util import validate_object_dict

# Simulate a schema whose declared gender maximum hasn't caught up with
# Person.OTHER yet -- the exact case this patch exists to handle.
shared_schema = {
"type": "object",
"properties": {
"_class": {"enum": ["Person"]},
"gender": {"type": "integer", "maximum": Person.OTHER - 1},
},
}

with patch.object(Person, "get_schema", return_value=shared_schema):
obj_dict = {"_class": "Person", "gender": Person.OTHER}
assert validate_object_dict(obj_dict) is True

# The object returned by get_schema() is shared across every call;
# it must come back untouched.
assert shared_schema["properties"]["gender"]["maximum"] == Person.OTHER - 1

# And a second call must still succeed -- it can't rely on a
# mutation left behind by the first call.
assert validate_object_dict(obj_dict) is True