Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| #!/usr/bin/env python3 | |
| """ | |
| Reads AppStream XML metadata and metadata from | |
| XDG .desktop files. | |
| """ | |
| # Copyright (c) 2014 Abhishek Bhattacharjee <abhishek.bhattacharjee11@gmail.com> | |
| # Copyright (c) 2014-2016 Matthias Klumpp <mak@debian.org> | |
| # | |
| # This program is free software; you can redistribute it and/or | |
| # modify it under the terms of the GNU Lesser General Public | |
| # License as published by the Free Software Foundation; either | |
| # version 3.0 of the License, or (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| # Lesser General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU Lesser General Public | |
| # License along with this program. | |
| import re | |
| from configparser import RawConfigParser | |
| import lxml.etree as et | |
| from xml.sax.saxutils import escape | |
| from io import StringIO | |
| from .component import Component, Screenshot, IconType, ProvidedItemType | |
| from .utils import str_enc_dec | |
| def read_desktop_data(cpt, dcontent, ignore_nodisplay=False): | |
| ''' | |
| Parses a .desktop file and sets ComponentData properties | |
| ''' | |
| df = RawConfigParser(allow_no_value=True) | |
| items = None | |
| try: | |
| df.readfp(StringIO(dcontent)) | |
| dtype = df.get("Desktop Entry", "Type") | |
| if dtype and dtype.lower() != "application": | |
| # ignore this file, isn't an application | |
| cpt.add_hint("not-an-application") | |
| return False | |
| try: | |
| nodisplay = df.get("Desktop Entry", "NoDisplay") | |
| if not ignore_nodisplay and nodisplay and nodisplay.lower() == "true": | |
| # we ignore this .desktop file, shouldn't be displayed | |
| cpt.add_hint("invisible-application") | |
| return False | |
| except: | |
| # we don't care if the NoDisplay variable doesn't exist | |
| # if it isn't there, the file should be processed | |
| pass | |
| try: | |
| asignore = df.get("Desktop Entry", "X-AppStream-Ignore") | |
| if asignore and asignore.lower() == "true": | |
| # this .desktop file should be excluded from AppStream metadata | |
| cpt.add_hint("invisible-application") | |
| return False | |
| except: | |
| # we don't care if the AppStream-Ignore variable doesn't exist | |
| # if it isn't there, the file should be processed | |
| pass | |
| items = df.items('Desktop Entry') | |
| except Exception as e: | |
| # this .desktop file is not interesting | |
| cpt.add_hint("desktop-file-read-error", str(e)) | |
| return True | |
| # if we reached this step, we are dealing with a GUI desktop app | |
| cpt.set_kind_from_string('desktop-app') | |
| for item in items: | |
| if len(item) != 2: | |
| continue | |
| key = item[0] | |
| value = str_enc_dec(item[1]) | |
| if not value: | |
| continue | |
| value = value.strip() | |
| if key.startswith("name"): | |
| if key == 'name': | |
| cpt.name['C'] = value | |
| else: | |
| cpt.name[key[5:-1]] = value | |
| elif key == 'categories': | |
| value = value.split(';') | |
| value.pop() | |
| cpt.categories = value | |
| elif key.startswith('comment'): | |
| if key == 'comment': | |
| cpt.summary['C'] = value | |
| else: | |
| cpt.summary[key[8:-1]] = value | |
| elif key.startswith('keywords'): | |
| value = re.split(';|,', value) | |
| if not value[-1]: | |
| value.pop() | |
| if key[8:] == '': | |
| if cpt.keywords: | |
| if set(value) not in \ | |
| [set(val) for val in | |
| cpt.keywords.values()]: | |
| cpt.keywords.update( | |
| {'C': list(map(str_enc_dec, value))} | |
| ) | |
| else: | |
| cpt.keywords = { | |
| 'C': list(map(str_enc_dec, value)) | |
| } | |
| else: | |
| if cpt.keywords: | |
| if set(value) not in \ | |
| [set(val) for val in | |
| cpt.keywords.values()]: | |
| cpt.keywords.update( | |
| {key[9:-1]: list(map(str_enc_dec, value))} | |
| ) | |
| else: | |
| cpt.keywords = { | |
| key[9:-1]: list(map(str_enc_dec, value)) | |
| } | |
| elif key == 'mimetype': | |
| value = value.split(';') | |
| if len(value) > 1: | |
| value.pop() | |
| for val in value: | |
| cpt.add_provided_item(ProvidedItemType.MIMETYPE, val) | |
| elif key == 'icon': | |
| cpt.set_icon(IconType.CACHED, value) | |
| return True | |
| def _get_tag_locale(subs): | |
| attr_dic = subs.attrib | |
| if attr_dic: | |
| locale = attr_dic.get('{http://www.w3.org/XML/1998/namespace}lang') | |
| if locale: | |
| return locale | |
| return "C" | |
| def _parse_description_tag(subs): | |
| ''' | |
| Handles the description tag | |
| ''' | |
| def prepare_desc_string(s): | |
| ''' | |
| Clears linebreaks and XML-escapes the resulting string | |
| ''' | |
| if not s: | |
| return "" | |
| s = s.strip() | |
| s = " ".join(s.split()) | |
| return escape(s) | |
| ddict = dict() | |
| # The description tag translation is combined per language, | |
| # for faster parsing on the client side. | |
| # In case no translation is found, the untranslated version is used instead. | |
| # the DEP-11 YAML stores the description as HTML | |
| for usubs in subs: | |
| locale = _get_tag_locale(usubs) | |
| if usubs.tag == 'p': | |
| if not locale in ddict: | |
| ddict[locale] = "" | |
| ddict[locale] += "<p>%s</p>" % str_enc_dec(prepare_desc_string(usubs.text)) | |
| elif usubs.tag == 'ul' or usubs.tag == 'ol': | |
| tmp_dict = dict() | |
| # find the right locale, or fallback to untranslated | |
| for u_usubs in usubs: | |
| locale = _get_tag_locale(u_usubs) | |
| if not locale in tmp_dict: | |
| tmp_dict[locale] = "" | |
| if u_usubs.tag == 'li': | |
| tmp_dict[locale] += "<li>%s</li>" % str_enc_dec(prepare_desc_string(u_usubs.text)) | |
| for locale, value in tmp_dict.items(): | |
| if not locale in ddict: | |
| # This should not happen (but better be prepared) | |
| ddict[locale] = "" | |
| ddict[locale] += "<%s>%s</%s>" % (usubs.tag, value, usubs.tag) | |
| return ddict | |
| def _parse_screenshots_tag(subs): | |
| ''' | |
| Handles screenshots, caption, source-image etc. | |
| ''' | |
| shots = [] | |
| for usubs in subs: | |
| # for one screeshot tag | |
| if usubs.tag == 'screenshot': | |
| shot = Screenshot() | |
| attr_dic = usubs.attrib | |
| if attr_dic.get('type'): | |
| if attr_dic['type'] == 'default': | |
| shot.default = True | |
| # handle pre-0.6 spec screenshot notations | |
| url = usubs.text.strip() if usubs.text else None | |
| if url: | |
| # we do not know width or height yet, that information will be added later | |
| shot.set_source_image(url, 0, 0) | |
| shots.append(shot) | |
| continue | |
| # else look for captions and image tag | |
| for tags in usubs: | |
| if tags.tag == 'caption': | |
| # for localisation | |
| attr_dic = tags.attrib | |
| if attr_dic: | |
| for v in attr_dic.values(): | |
| key = v | |
| else: | |
| key = 'C' | |
| caption_text = str_enc_dec(tags.text) | |
| if caption_text: | |
| shot.caption[key] = caption_text | |
| if tags.tag == 'image': | |
| shot.set_source_image(tags.text, 0, 0) | |
| # only add the screenshot if we have a source image | |
| if shot.has_source_image(): | |
| shots.append(shot) | |
| return shots | |
| def _parse_releases_tag(relstag): | |
| ''' | |
| Parses a releases tag and returns the last three releases | |
| ''' | |
| rels = list() | |
| for subs in relstag: | |
| # for one screeshot tag | |
| if subs.tag != 'release': | |
| continue | |
| release = dict() | |
| attr_dic = subs.attrib | |
| if attr_dic.get('version'): | |
| release['version'] = attr_dic['version'] | |
| if attr_dic.get('timestamp'): | |
| try: | |
| release['unix-timestamp'] = int(attr_dic['timestamp']) | |
| except: | |
| # the timestamp was wrong - we silently ignore the error | |
| # TODO: Emit warning hint | |
| continue | |
| else: | |
| # we can't use releases which don't have a timestamp | |
| # TODO: Emit a warning hint here | |
| continue | |
| # else look for captions and image tag | |
| for usubs in subs: | |
| if usubs.tag == 'description': | |
| release['description'] = _parse_description_tag(usubs) | |
| rels.append(release) | |
| # sort releases, newest first | |
| rels = sorted(rels, key=lambda k: k['unix-timestamp'], reverse=True) | |
| if len(rels) > 3: | |
| return rels[:3] | |
| return rels | |
| def read_appstream_upstream_xml(cpt, xml_content): | |
| ''' | |
| Reads the metadata from the xml file in usr/share/metainfo. | |
| Sets ComponentData properties | |
| ''' | |
| root = None | |
| try: | |
| # Drop default namespace - some add a bogus namespace to their metainfo files which breaks the parser. | |
| # When we actually start using namespaces in future, we need to handle them explicitly. | |
| xml_content = re.sub(r'\sxmlns="[^"]+"', '', xml_content, count=1) | |
| root = et.fromstring(bytes(xml_content, 'utf-8')) | |
| except Exception as e: | |
| cpt.add_hint("metainfo-parse-error", str(e)) | |
| return | |
| if root is None: | |
| cpt.add_hint("metainfo-parse-error", "Error is unknown, the root node was null.") | |
| if root.tag == 'application': | |
| # we parse ancient AppStream XML, but it is a good idea to update it to make use of newer features, remove some ancient | |
| # oddities and to simplify the parser in future. So we add a hint for that. | |
| cpt.add_hint("ancient-metadata") | |
| # set the type of our component | |
| cpt.set_kind_from_string(root.attrib.get('type')) | |
| for subs in root: | |
| locale = _get_tag_locale(subs) | |
| value = None | |
| if subs.text: | |
| value = subs.text.strip() | |
| if subs.tag == 'id': | |
| cpt.cid = value | |
| # INFO: legacy support, remove later | |
| tps = subs.attrib.get('type') | |
| if tps: | |
| cpt.set_kind_from_string(tps) | |
| elif subs.tag == "name": | |
| cpt.name[locale] = value | |
| elif subs.tag == "summary": | |
| cpt.summary[locale] = value | |
| elif subs.tag == "description": | |
| desc = _parse_description_tag(subs) | |
| cpt.description = desc | |
| elif subs.tag == "screenshots": | |
| screen = _parse_screenshots_tag(subs) | |
| cpt.screenshots = screen | |
| elif subs.tag == "provides": | |
| for ptag in subs: | |
| ptag_text = None | |
| if ptag.text: | |
| ptag_text = ptag.text.strip() | |
| if ptag.tag == "binary": | |
| cpt.add_provided_item( | |
| ProvidedItemType.BINARY, ptag_text | |
| ) | |
| if ptag.tag == 'library': | |
| cpt.add_provided_item( | |
| ProvidedItemType.LIBRARY, ptag_text | |
| ) | |
| if ptag.tag == 'dbus': | |
| bus_kind = ptag.attrib.get('type') | |
| if bus_kind == "session": | |
| bus_kind = "user" | |
| if bus_kind: | |
| cpt.add_provided_item(ProvidedItemType.DBUS, {'type': bus_kind, 'service': ptag_text}) | |
| if ptag.tag == 'firmware': | |
| fw_type = ptag.attrib.get('type') | |
| fw_data = {'type': fw_type} | |
| fw_valid = True | |
| if fw_type == "flashed": | |
| fw_data['guid'] = ptag_text | |
| elif fw_type == "runtime": | |
| fw_data['fname'] = ptag_text | |
| else: | |
| fw_valid = False | |
| if fw_valid: | |
| cpt.add_provided_item(ProvidedItemType.FIRMWARE, fw_data) | |
| if ptag.tag == 'python2': | |
| cpt.add_provided_item( | |
| ProvidedItemType.PYTHON_2, ptag_text | |
| ) | |
| if ptag.tag == 'python3': | |
| cpt.add_provided_item( | |
| ProvidedItemType.PYTHON_3, ptag_text | |
| ) | |
| if ptag.tag == 'modalias': | |
| cpt.add_provided_item( | |
| ProvidedItemType.MODALIAS, ptag_text | |
| ) | |
| if ptag.tag == 'mimetype': | |
| cpt.add_provided_item( | |
| ProvidedItemType.MIMETYPE, ptag_text | |
| ) | |
| if ptag.tag == 'font': | |
| font_file = ptag.attrib.get('file') | |
| if font_file: | |
| cpt.add_provided_item(ProvidedItemType.FONT, {'file': font_file, 'name': ptag_text}) | |
| elif subs.tag == "mimetypes": | |
| for mimetag in subs: | |
| if mimetag.tag == "mimetype": | |
| cpt.add_provided_item( | |
| ProvidedItemType.MIMETYPE, mimetag.text | |
| ) | |
| elif subs.tag == "url": | |
| if cpt.url: | |
| cpt.url.update({subs.attrib['type']: value}) | |
| else: | |
| cpt.url = {subs.attrib['type']: value} | |
| elif subs.tag == "project_license": | |
| cpt.project_license = value | |
| elif subs.tag == "project_group": | |
| cpt.project_group = value | |
| elif subs.tag == "developer_name": | |
| cpt.developer_name[locale] = value | |
| elif subs.tag == "extends": | |
| cpt.extends.append(value) | |
| elif subs.tag == "compulsory_for_desktop": | |
| cpt.compulsory_for_desktops.append(value) | |
| elif subs.tag == "releases": | |
| releases = _parse_releases_tag(subs) | |
| cpt.releases = releases |