diff --git a/src/common/endoflife.py b/src/common/endoflife.py index 86fed6ab..385396aa 100644 --- a/src/common/endoflife.py +++ b/src/common/endoflife.py @@ -133,4 +133,5 @@ def to_identifier(s: str) -> str: """Convert a string to a valid endoflife.date identifier.""" identifier = s.strip().lower() identifier = identifier.replace(" ", "-") + identifier = re.sub(r"-+", "-", identifier) return re.sub(r"[^a-z0-9.\-+_]", "", identifier) diff --git a/src/motorola-security.py b/src/motorola-security.py new file mode 100644 index 00000000..b437e71a --- /dev/null +++ b/src/motorola-security.py @@ -0,0 +1,59 @@ +import json +import logging +import re + +from common import dates, endoflife, http +from common.releasedata import ProductData, config_from_argv + + +def extract_data(url: str, key_to_search_for: str) -> dict | None: + """Extract JSON data from Motorola Mobility security updates page.""" + html = http.fetch_html(url) + + # find all script tags with no src attribute and containing a CDATA section. + all_scripts = [str(t.getText) for t in html.find_all("script", src=False)] + candidate_scripts = [s for s in all_scripts if key_to_search_for in s] + if len(candidate_scripts) != 1: + msg = f"Expected exactly one script containing {key_to_search_for}, found {len(candidate_scripts)}" + raise ValueError(msg) + + # Ideally, we would use an actual JavaScript parser to find our desired content. + lines = candidate_scripts[0].split("\n") + candidate_lines = [line for line in lines if key_to_search_for in line] + if len(candidate_lines) != 1: + msg = f"Expected exactly one containing {key_to_search_for} in JS, got {len(candidate_lines)}" + raise ValueError(msg) + + candidate_line = candidate_lines[0] + raw_json = re.search(r"\{.+}", candidate_line) + if not raw_json: + msg = f"Failed to find JSON in {candidate_line}" + raise ValueError(msg) + + logging.debug(f"Found : {raw_json}") + return json.loads(raw_json.group(0)) + +config = config_from_argv() +with ProductData(config.product) as product_data: + json_data = extract_data(config.url, "guidedAssistant") + + for question in json_data["j"]["guidedAssistant"]["questions"]: + if not question.get("responses") or question["responses"][0].get("text") != "Security patch details for all Motorola products": + logging.debug(f"Skipping {question}") + continue + + lines = question["taglessText"].replace("\xa0", " ").split("\n") + logging.debug(lines) + + product_id = question["agentText"] + label = lines[0].strip() + name = endoflife.to_identifier(label) + release = product_data.get_release(name) + release.set_field("link", f"https://en-us.support.motorola.com/app/software-security-update/g_id/7112/productid/{product_id}") + for line in lines: + if (m := re.search(r"^Device launched on (.*)$", line)): + date = dates.parse_date_or_month_year_date(m.group(1).strip()).replace(day=1) + release.set_release_date(date) + if (m := re.search(r"^Security updates will stop on (.*)$", line)): + eol = dates.parse_date_or_month_year_date(m.group(1).strip()) + release.set_eol(eol)