zotero-addons-scraper/moz_addons.py
2024-03-07 20:37:51 +08:00

186 lines
7.5 KiB
Python

import zipfile
import commentjson as json
import os
from xml.dom import minidom
import re
# modify from `https://github.com/mozilla/gecko-dev/blob/ac19e2c0d7c09a2deaedbe6afc4cdcf1a4561456/testing/mozbase/mozprofile/mozprofile/addons.py#L213`
def addon_details(addon_path):
"""
Returns a dictionary of details about the addon.
:param addon_path: path to the add-on directory or XPI
Returns::
{'id': u'rainbow@colors.org', # id of the addon
'version': u'1.4', # version of the addon
'name': u'Rainbow', # name of the addon
'unpack': False } # whether to unpack the addon
"""
details = {"id": None, "unpack": False, "name": None, "version": None, "description": None}
def get_namespace_id(doc, url):
attributes = doc.documentElement.attributes
namespace = ""
for i in range(attributes.length):
if attributes.item(i).value == url:
if ":" in attributes.item(i).name:
# If the namespace is not the default one remove 'xlmns:'
namespace = attributes.item(i).name.split(":")[1] + ":"
break
return namespace
def get_text(element):
"""Retrieve the text value of a given node"""
rc = []
for node in element.childNodes:
if node.nodeType == node.TEXT_NODE:
rc.append(node.data)
return "".join(rc).strip()
if not os.path.exists(addon_path):
raise IOError("Add-on path does not exist: %s" % addon_path)
is_webext = False
try:
if zipfile.is_zipfile(addon_path):
# Bug 944361 - We cannot use 'with' together with zipFile because
# it will cause an exception thrown in Python 2.6.
try:
compressed_file = zipfile.ZipFile(addon_path, "r")
filenames = [f.filename for f in (compressed_file).filelist]
if "manifest.json" in filenames:
is_webext = True
manifest = compressed_file.read("manifest.json").decode()
manifest = json.loads(manifest)
elif "install.rdf" in filenames:
manifest = compressed_file.read("install.rdf")
else:
raise KeyError("No manifest")
finally:
compressed_file.close()
elif os.path.isdir(addon_path):
try:
with open(os.path.join(addon_path, "manifest.json")) as f:
manifest = json.loads(f.read())
is_webext = True
except IOError:
with open(os.path.join(addon_path, "install.rdf")) as f:
manifest = f.read()
else:
raise IOError(
"Add-on path is neither an XPI nor a directory: %s" % addon_path
)
except (IOError, KeyError) as e:
# reraise(AddonFormatError, AddonFormatError(str(e)), sys.exc_info()[2])
raise e
if is_webext:
details["version"] = manifest.get("version")
details["name"] = manifest.get("name")
details["description"] = manifest.get("description")
# Bug 1572404 - we support two locations for gecko-specific
# metadata.
for location in ("applications", "browser_specific_settings"):
for app in ("zotero", "gecko"):
try:
details["id"] = manifest[location][app]["id"]
break
except KeyError:
pass
# if details["id"] is None:
# details["id"] = cls._gen_iid(addon_path)
details["unpack"] = False
# handler for __MSG_{}__ items
def extract_MSG_placeholder(text):
if match := re.search(r'__MSG_(.*?)__', text):
return match.group(1)
def load_locale_for_MSG():
default_locale = manifest.get('default_locale')
locale_filename = f"_locales/{default_locale}/messages.json"
try:
if zipfile.is_zipfile(addon_path):
# Bug 944361 - We cannot use 'with' together with zipFile because
# it will cause an exception thrown in Python 2.6.
try:
compressed_file = zipfile.ZipFile(addon_path, "r")
filenames = [f.filename for f in (compressed_file).filelist]
if locale_filename in filenames:
locale = compressed_file.read(locale_filename).decode()
return json.loads(locale)
else:
raise KeyError(f"No {locale_filename}")
finally:
compressed_file.close()
elif os.path.isdir(addon_path):
try:
with open(os.path.join(addon_path, locale_filename)) as f:
return json.loads(f.read())
except IOError:
raise KeyError(f"No {locale_filename}")
else:
raise IOError(
"Cannot found: %s" % locale_filename
)
except (IOError, KeyError) as e:
# reraise(AddonFormatError, AddonFormatError(str(e)), sys.exc_info()[2])
raise e
locale_for_MSG = None
for key in details:
if isinstance(details[key], str) and (placeholder := extract_MSG_placeholder(details[key])):
if not locale_for_MSG:
locale_for_MSG = load_locale_for_MSG()
if not locale_for_MSG:
break
if value := locale_for_MSG.get(placeholder, {}).get('message'):
details[key] = value
else:
try:
doc = minidom.parseString(manifest)
# Get the namespaces abbreviations
em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
rdf = get_namespace_id(
doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
)
description = doc.getElementsByTagName(rdf + "Description").item(0)
try:
descriptions = [e for e in doc.getElementsByTagName(rdf + "Description")
if len(e.getElementsByTagName(em + "targetApplication")) > 0]
if descriptions:
description = descriptions[0]
except Exception as e:
pass
for entry, value in description.attributes.items():
# Remove the namespace prefix from the tag for comparison
entry = entry.replace(em, "")
if entry in details.keys():
details.update({entry: value})
for node in description.childNodes:
# Remove the namespace prefix from the tag for comparison
entry = node.nodeName.replace(em, "")
if entry in details.keys():
details.update({entry: get_text(node)})
except Exception as e:
raise e
# turn unpack into a true/false value
if isinstance(details["unpack"], str):
details["unpack"] = details["unpack"].lower() == "true"
# If no ID is set, the add-on is invalid
# if details.get("id") is None and not is_webext:
# raise AddonFormatError("Add-on id could not be found.")
return details