/** * gs_appstream_refine_app: */ gboolean gs_appstream_refine_app (GsPlugin *plugin, GsApp *app, AsApp *item, GError **error) { AsRelease *rel; GHashTable *urls; GPtrArray *pkgnames; GPtrArray *kudos; const gchar *tmp; guint i; /* set the kind to be more precise */ if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN || gs_app_get_kind (app) == AS_APP_KIND_GENERIC) { gs_app_set_kind (app, as_app_get_kind (item)); } /* is installed already */ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) { switch (as_app_get_source_kind (item)) { case AS_APP_SOURCE_KIND_APPDATA: case AS_APP_SOURCE_KIND_DESKTOP: gs_app_set_kind (app, AS_APP_KIND_DESKTOP); gs_app_set_state (app, AS_APP_STATE_INSTALLED); break; case AS_APP_SOURCE_KIND_METAINFO: gs_app_set_state (app, AS_APP_STATE_INSTALLED); break; case AS_APP_SOURCE_KIND_APPSTREAM: gs_app_set_state (app, as_app_get_state (item)); break; default: break; } } /* set management plugin automatically */ gs_refine_item_management_plugin (app, item); /* set id */ if (as_app_get_id (item) != NULL && gs_app_get_id (app) == NULL) gs_app_set_id (app, as_app_get_id (item)); /* set name */ tmp = as_app_get_name (item, NULL); if (tmp != NULL) { if (g_str_has_prefix (tmp, "(Nightly) ")) { tmp += 10; if (gs_app_get_metadata_item (app, "X-XdgApp-Tags") == NULL) gs_app_set_metadata (app, "X-XdgApp-Tags", "nightly"); } gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, tmp); } /* set summary */ tmp = as_app_get_comment (item, NULL); if (tmp != NULL) { gs_app_set_summary (app, GS_APP_QUALITY_HIGHEST, tmp); } /* add urls */ urls = as_app_get_urls (item); if (g_hash_table_size (urls) > 0 && gs_app_get_url (app, AS_URL_KIND_HOMEPAGE) == NULL) { GList *l; g_autoptr(GList) keys = NULL; keys = g_hash_table_get_keys (urls); for (l = keys; l != NULL; l = l->next) { gs_app_set_url (app, as_url_kind_from_string (l->data), g_hash_table_lookup (urls, l->data)); } } /* set licence */ if (as_app_get_project_license (item) != NULL && gs_app_get_license (app) == NULL) gs_app_set_license (app, GS_APP_QUALITY_HIGHEST, as_app_get_project_license (item)); /* set keywords */ if (as_app_get_keywords (item, NULL) != NULL && gs_app_get_keywords (app) == NULL) { gs_app_set_keywords (app, as_app_get_keywords (item, NULL)); gs_app_add_kudo (app, GS_APP_KUDO_HAS_KEYWORDS); } /* set origin */ if (as_app_get_origin (item) != NULL && gs_app_get_origin (app) == NULL) { gs_app_set_origin (app, as_app_get_origin (item)); } /* set description */ tmp = as_app_get_description (item, NULL); if (tmp != NULL) { g_autofree gchar *from_xml = NULL; from_xml = as_markup_convert_simple (tmp, error); if (from_xml == NULL) { g_prefix_error (error, "trying to parse '%s': ", tmp); return FALSE; } gs_app_set_description (app, GS_APP_QUALITY_HIGHEST, from_xml); } /* set icon */ if (as_app_get_icon_default (item) != NULL && gs_app_get_pixbuf (app) == NULL) gs_refine_item_pixbuf (plugin, app, item); /* set categories */ if (as_app_get_categories (item) != NULL && gs_app_get_categories (app)->len == 0) gs_app_set_categories (app, as_app_get_categories (item)); /* set project group */ if (as_app_get_project_group (item) != NULL && gs_app_get_project_group (app) == NULL) gs_app_set_project_group (app, as_app_get_project_group (item)); /* this is a core application for the desktop and cannot be removed */ if (_as_app_has_compulsory_for_desktop (item, "GNOME") && gs_app_get_kind (app) == AS_APP_KIND_DESKTOP) gs_app_add_quirk (app, AS_APP_QUIRK_COMPULSORY); /* set id kind */ if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN) gs_app_set_kind (app, as_app_get_kind (item)); /* copy all the metadata */ gs_appstream_copy_metadata (app, item); /* set package names */ pkgnames = as_app_get_pkgnames (item); if (pkgnames->len > 0 && gs_app_get_sources(app)->len == 0) gs_app_set_sources (app, pkgnames); /* set addons */ gs_appstream_refine_add_addons (plugin, app, item); /* set screenshots */ gs_appstream_refine_add_screenshots (app, item); /* are the screenshots perfect */ if (gs_appstream_are_screenshots_perfect (item)) gs_app_add_kudo (app, GS_APP_KUDO_PERFECT_SCREENSHOTS); /* was this application released recently */ if (gs_appstream_is_recent_release (item)) gs_app_add_kudo (app, GS_APP_KUDO_RECENT_RELEASE); /* add kudos */ if (as_app_get_language (item, plugin->locale) > 50) gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE); /* add new-style kudos */ kudos = as_app_get_kudos (item); for (i = 0; i < kudos->len; i++) { tmp = g_ptr_array_index (kudos, i); switch (as_kudo_kind_from_string (tmp)) { case AS_KUDO_KIND_SEARCH_PROVIDER: gs_app_add_kudo (app, GS_APP_KUDO_SEARCH_PROVIDER); break; case AS_KUDO_KIND_USER_DOCS: gs_app_add_kudo (app, GS_APP_KUDO_INSTALLS_USER_DOCS); break; case AS_KUDO_KIND_APP_MENU: gs_app_add_kudo (app, GS_APP_KUDO_USES_APP_MENU); break; case AS_KUDO_KIND_MODERN_TOOLKIT: gs_app_add_kudo (app, GS_APP_KUDO_MODERN_TOOLKIT); break; case AS_KUDO_KIND_NOTIFICATIONS: gs_app_add_kudo (app, GS_APP_KUDO_USES_NOTIFICATIONS); break; case AS_KUDO_KIND_HIGH_CONTRAST: gs_app_add_kudo (app, GS_APP_KUDO_HIGH_CONTRAST); break; case AS_KUDO_KIND_HI_DPI_ICON: gs_app_add_kudo (app, GS_APP_KUDO_HI_DPI_ICON); break; default: g_debug ("no idea how to handle kudo '%s'", tmp); break; } } /* is there any update information */ rel = as_app_get_release_default (item); if (rel != NULL) { tmp = as_release_get_description (rel, NULL); if (tmp != NULL) { g_autofree gchar *desc = NULL; desc = as_markup_convert (tmp, AS_MARKUP_CONVERT_FORMAT_MARKDOWN, error); if (desc == NULL) return FALSE; gs_app_set_update_details (app, desc); } gs_app_set_update_urgency (app, as_release_get_urgency (rel)); gs_app_set_update_version (app, as_release_get_version (rel)); } return TRUE; }
/** * as_validator_validate_component_node: **/ static AsComponent* as_validator_validate_component_node (AsValidator *validator, AsXMLData *xdt, xmlNode *root) { gchar *cpttype; xmlNode *iter; AsComponent *cpt; gchar *metadata_license = NULL; GHashTable *found_tags; const gchar *summary; AsParserMode mode; found_tags = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); mode = as_xmldata_get_parser_mode (xdt); /* validate the resulting AsComponent for sanity */ cpt = as_component_new (); as_xmldata_parse_component_node (xdt, root, cpt, NULL); as_validator_set_current_cpt (validator, cpt); /* check if component type is valid */ cpttype = (gchar*) xmlGetProp (root, (xmlChar*) "type"); if (cpttype != NULL) { if (as_component_kind_from_string (cpttype) == AS_COMPONENT_KIND_UNKNOWN) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Invalid component type found: %s", cpttype); } } g_free (cpttype); for (iter = root->children; iter != NULL; iter = iter->next) { const gchar *node_name; g_autofree gchar *node_content = NULL; gboolean tag_valid = TRUE; /* discard spaces */ if (iter->type != XML_ELEMENT_NODE) continue; node_name = (const gchar*) iter->name; node_content = (gchar*) xmlNodeGetContent (iter); if (g_strcmp0 (node_name, "id") == 0) { gchar *prop; prop = (gchar*) xmlGetProp (iter, (xmlChar*) "type"); if (prop != NULL) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_PROPERTY_INVALID, "The id tag for \"%s\" still contains a 'type' property, probably from an old conversion.", node_content); } g_free (prop); if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) { if (!g_str_has_suffix (node_content, ".desktop")) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_VALUE_WRONG, "Component id belongs to a desktop-application, but does not resemble the .desktop file name: \"%s\"", node_content); } } else if (g_strcmp0 (node_name, "metadata_license") == 0) { metadata_license = g_strdup (node_content); as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "pkgname") == 0) { if (g_hash_table_contains (found_tags, node_name)) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_PEDANTIC, AS_ISSUE_KIND_TAG_DUPLICATED, "The tag 'pkgname' appears multiple times. You should evaluate creating a metapackage containing the data in order to avoid defining multiple package names per component."); } } else if (g_strcmp0 (node_name, "source_pkgname") == 0) { if (g_hash_table_contains (found_tags, node_name)) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_DUPLICATED, "The tag 'source_pkgname' appears multiple times."); } } else if (g_strcmp0 (node_name, "name") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "summary") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "description") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_description_tag (validator, iter, cpt, mode); } else if (g_strcmp0 (node_name, "icon") == 0) { gchar *prop; prop = as_validator_check_type_property (validator, cpt, iter); if ((g_strcmp0 (prop, "cached") == 0) || (g_strcmp0 (prop, "stock") == 0)) { if (g_strrstr (node_content, "/") != NULL) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Icons of type 'stock' or 'cached' must not contain a full or relative path to the icon."); } g_free (prop); } else if (g_strcmp0 (node_name, "url") == 0) { gchar *prop; prop = as_validator_check_type_property (validator, cpt, iter); if (as_url_kind_from_string (prop) == AS_URL_KIND_UNKNOWN) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_PROPERTY_INVALID, "Invalid property for 'url' tag: \"%s\"", prop); } g_free (prop); } else if (g_strcmp0 (node_name, "categories") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "category", cpt); } else if (g_strcmp0 (node_name, "keywords") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "keyword", cpt); } else if (g_strcmp0 (node_name, "mimetypes") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "mimetype", cpt); } else if (g_strcmp0 (node_name, "provides") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "screenshots") == 0) { as_validator_check_children_quick (validator, iter, "screenshot", cpt); } else if (g_strcmp0 (node_name, "project_license") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "project_group") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "developer_name") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "compulsory_for_desktop") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "releases") == 0) { as_validator_check_children_quick (validator, iter, "release", cpt); } else if ((g_strcmp0 (node_name, "languages") == 0) && (mode == AS_PARSER_MODE_DISTRO)) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "lang", cpt); } else if ((g_strcmp0 (node_name, "translation") == 0) && (mode == AS_PARSER_MODE_UPSTREAM)) { g_autofree gchar *prop = NULL; AsTranslationKind trkind; prop = as_validator_check_type_property (validator, cpt, iter); trkind = as_translation_kind_from_string (prop); if (trkind == AS_TRANSLATION_KIND_UNKNOWN) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Unknown type '%s' for <translation/> tag.", prop); } } else if (g_strcmp0 (node_name, "extends") == 0) { } else if (g_strcmp0 (node_name, "bundle") == 0) { g_autofree gchar *prop = NULL; prop = as_validator_check_type_property (validator, cpt, iter); if ((g_strcmp0 (prop, "limba") != 0) && (g_strcmp0 (prop, "xdg-app") != 0)) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Unknown type '%s' for <bundle/> tag.", prop); } } else if (g_strcmp0 (node_name, "update_contact") == 0) { if (mode == AS_PARSER_MODE_DISTRO) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_NOT_ALLOWED, "The 'update_contact' tag should not be included in distro AppStream XML."); } else { as_validator_check_appear_once (validator, iter, found_tags, cpt); } } else if (g_strcmp0 (node_name, "metadata") == 0) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_PEDANTIC, AS_ISSUE_KIND_TAG_UNKNOWN, "Found custom metadata in <metadata/> tag. Use of this tag is common, but should be avoided if possible."); tag_valid = FALSE; } else if (!g_str_has_prefix (node_name, "x-")) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_UNKNOWN, "Found invalid tag: '%s'. Non-standard tags must be prefixed with \"x-\".", node_name); tag_valid = FALSE; } if (tag_valid) { as_validator_check_content_empty (validator, node_content, node_name, AS_ISSUE_IMPORTANCE_WARNING, cpt); } } g_hash_table_unref (found_tags); if (metadata_license == NULL) { if (mode == AS_PARSER_MODE_UPSTREAM) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "The essential tag 'metadata_license' is missing."); } else { g_free (metadata_license); } /* check if the summary is sane */ summary = as_component_get_summary (cpt); if ((summary != NULL) && ((strstr (summary, "\n") != NULL) || (strstr (summary, "\t") != NULL))) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "The summary tag must not contain tabs or linebreaks."); } /* check if we have a description */ if (as_str_empty (as_component_get_description (cpt))) { AsComponentKind cpt_kind; cpt_kind = as_component_get_kind (cpt); if ((cpt_kind == AS_COMPONENT_KIND_DESKTOP_APP) || (cpt_kind == AS_COMPONENT_KIND_FONT)) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "The component is missing a long description. Components of this type must have a long description."); } else if (cpt_kind != AS_COMPONENT_KIND_GENERIC) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_TAG_MISSING, "The component is missing a long description. It is recommended to add one."); } } /* validate font specific stuff */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_FONT) { if (!g_str_has_suffix (as_component_get_id (cpt), ".font")) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Components of type 'font' must have an AppStream ID with a '.font' suffix."); if (as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_FONT) == NULL) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_MISSING, "Type 'font' component, but no font information was provided via a provides/font tag."); } /* validate addon specific stuff */ if (as_component_get_extends (cpt)->len > 0) { if (as_component_get_kind (cpt) != AS_COMPONENT_KIND_ADDON) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_NOT_ALLOWED, "An 'extends' tag is specified, but the component is not an addon."); } else { if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_ADDON) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "The component is an addon, but no 'extends' tag was specified."); } as_validator_clear_current_cpt (validator); return cpt; }
/** * as_validator_validate_component_node: **/ static AsComponent* as_validator_validate_component_node (AsValidator *validator, AsXMLData *xdt, xmlNode *root) { xmlNode *iter; AsComponent *cpt; g_autofree gchar *cpttype = NULL; g_autoptr(GHashTable) found_tags = NULL; AsFormatStyle mode; gboolean has_metadata_license = FALSE; found_tags = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); mode = as_xmldata_get_format_style (xdt); /* validate the resulting AsComponent for sanity */ cpt = as_component_new (); as_xmldata_parse_component_node (xdt, root, cpt, NULL); as_validator_set_current_cpt (validator, cpt); /* check if component type is valid */ cpttype = (gchar*) xmlGetProp (root, (xmlChar*) "type"); if (cpttype != NULL) { if (as_component_kind_from_string (cpttype) == AS_COMPONENT_KIND_UNKNOWN) { as_validator_add_issue (validator, root, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Invalid component type found: %s", cpttype); } } if ((as_component_get_priority (cpt) != 0) && (mode == AS_FORMAT_STYLE_METAINFO)) { as_validator_add_issue (validator, root, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "The component has a priority value set. This is not allowed in metainfo files."); } if ((as_component_get_merge_kind (cpt) != AS_MERGE_KIND_NONE) && (mode == AS_FORMAT_STYLE_METAINFO)) { as_validator_add_issue (validator, root, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "The component has a 'merge' method defined. This is not allowed in metainfo files."); } for (iter = root->children; iter != NULL; iter = iter->next) { const gchar *node_name; g_autofree gchar *node_content = NULL; gboolean tag_valid = TRUE; /* discard spaces */ if (iter->type != XML_ELEMENT_NODE) continue; node_name = (const gchar*) iter->name; node_content = (gchar*) xmlNodeGetContent (iter); if (g_strcmp0 (node_name, "id") == 0) { gchar *prop; prop = (gchar*) xmlGetProp (iter, (xmlChar*) "type"); if (prop != NULL) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_PROPERTY_INVALID, "The id tag for \"%s\" still contains a 'type' property, probably from an old conversion.", node_content); } g_free (prop); if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) { if (!g_str_has_suffix (node_content, ".desktop")) as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_VALUE_WRONG, "Component id belongs to a desktop-application, but does not resemble the .desktop file name: \"%s\"", node_content); } /* validate the AppStream ID */ as_validator_validate_component_id (validator, iter, cpt); } else if (g_strcmp0 (node_name, "metadata_license") == 0) { has_metadata_license = TRUE; as_validator_check_appear_once (validator, iter, found_tags, cpt); /* the license must allow easy mixing of metadata in metainfo files */ if (mode == AS_FORMAT_STYLE_METAINFO) { if (!as_license_is_metadata_license (node_content)) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_VALUE_WRONG, "The metadata itself does not seem to be licensed under a permissive license. Please license the data under a permissive license, like FSFAP, CC-0-1.0 or MIT " "to allow distributors to include it in mixed data collections without the risk of license violations due to mutually incompatible licenses."); } } } else if (g_strcmp0 (node_name, "pkgname") == 0) { if (g_hash_table_contains (found_tags, node_name)) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_PEDANTIC, AS_ISSUE_KIND_TAG_DUPLICATED, "The tag 'pkgname' appears multiple times. You should evaluate creating a metapackage containing the data in order to avoid defining multiple package names per component."); } } else if (g_strcmp0 (node_name, "source_pkgname") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "name") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); if (g_str_has_suffix (node_content, ".")) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_VALUE_ISSUE, "The component name should not end with a \".\" [%s]", node_content); } } else if (g_strcmp0 (node_name, "summary") == 0) { const gchar *summary = node_content; as_validator_check_appear_once (validator, iter, found_tags, cpt); if (g_str_has_suffix (summary, ".")) as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_VALUE_ISSUE, "The component summary should not end with a \".\" [%s]", summary); if ((summary != NULL) && ((strstr (summary, "\n") != NULL) || (strstr (summary, "\t") != NULL))) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "The summary tag must not contain tabs or linebreaks."); } } else if (g_strcmp0 (node_name, "description") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_description_tag (validator, iter, cpt, mode); } else if (g_strcmp0 (node_name, "icon") == 0) { gchar *prop; prop = as_validator_check_type_property (validator, cpt, iter); if ((g_strcmp0 (prop, "cached") == 0) || (g_strcmp0 (prop, "stock") == 0)) { if (g_strrstr (node_content, "/") != NULL) as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Icons of type 'stock' or 'cached' must not contain a full or relative path to the icon."); } g_free (prop); } else if (g_strcmp0 (node_name, "url") == 0) { gchar *prop; prop = as_validator_check_type_property (validator, cpt, iter); if (as_url_kind_from_string (prop) == AS_URL_KIND_UNKNOWN) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_PROPERTY_INVALID, "Invalid property for 'url' tag: \"%s\"", prop); } g_free (prop); } else if (g_strcmp0 (node_name, "categories") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "category", cpt); } else if (g_strcmp0 (node_name, "keywords") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "keyword", cpt); } else if (g_strcmp0 (node_name, "mimetypes") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "mimetype", cpt); } else if (g_strcmp0 (node_name, "provides") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "screenshots") == 0) { as_validator_check_children_quick (validator, iter, "screenshot", cpt); } else if (g_strcmp0 (node_name, "project_license") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "project_group") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "developer_name") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "compulsory_for_desktop") == 0) { if (!as_utils_is_desktop_environment (node_content)) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Unknown desktop-id '%s'.", node_content); } } else if (g_strcmp0 (node_name, "releases") == 0) { as_validator_check_children_quick (validator, iter, "release", cpt); } else if (g_strcmp0 (node_name, "languages") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "lang", cpt); } else if ((g_strcmp0 (node_name, "translation") == 0) && (mode == AS_FORMAT_STYLE_METAINFO)) { g_autofree gchar *prop = NULL; AsTranslationKind trkind; prop = as_validator_check_type_property (validator, cpt, iter); trkind = as_translation_kind_from_string (prop); if (prop != NULL && trkind == AS_TRANSLATION_KIND_UNKNOWN) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Unknown type '%s' for <translation/> tag.", prop); } } else if (g_strcmp0 (node_name, "extends") == 0) { } else if (g_strcmp0 (node_name, "bundle") == 0) { g_autofree gchar *prop = NULL; prop = as_validator_check_type_property (validator, cpt, iter); if (prop != NULL && as_bundle_kind_from_string (prop) == AS_BUNDLE_KIND_UNKNOWN) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Unknown type '%s' for <bundle/> tag.", prop); } } else if (g_strcmp0 (node_name, "update_contact") == 0) { if (mode == AS_FORMAT_STYLE_COLLECTION) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_NOT_ALLOWED, "The 'update_contact' tag should not be included in collection AppStream XML."); } else { as_validator_check_appear_once (validator, iter, found_tags, cpt); } } else if (g_strcmp0 (node_name, "suggests") == 0) { as_validator_check_children_quick (validator, iter, "id", cpt); } else if ((g_strcmp0 (node_name, "metadata") == 0) || (g_strcmp0 (node_name, "kudos") == 0)) { /* these tags are GNOME / Fedora specific extensions and are therefore quite common. They shouldn't make the validation fail, * especially if we might standardize at leat the <kudos/> tag one day, but we should still complain about those tags to make * it obvious that they are not supported by all implementations */ as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_TAG_UNKNOWN, "Found invalid tag: '%s'. This tag is a GNOME extensions to AppStream and is not supported by all implementations.", node_name); tag_valid = FALSE; } else if (!g_str_has_prefix (node_name, "x-")) { as_validator_add_issue (validator, iter, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_UNKNOWN, "Found invalid tag: '%s'. Non-standard tags must be prefixed with \"x-\".", node_name); tag_valid = FALSE; } if (tag_valid) { as_validator_check_content_empty (validator, iter, node_name, AS_ISSUE_IMPORTANCE_WARNING, cpt); } } /* emit an error if we are missing the metadata license in metainfo files */ if ((!has_metadata_license) && (mode == AS_FORMAT_STYLE_METAINFO)) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "The essential tag 'metadata_license' is missing."); } /* check if we have a description */ if (as_str_empty (as_component_get_description (cpt))) { AsComponentKind cpt_kind; cpt_kind = as_component_get_kind (cpt); if ((cpt_kind == AS_COMPONENT_KIND_DESKTOP_APP) || (cpt_kind == AS_COMPONENT_KIND_CONSOLE_APP) || (cpt_kind == AS_COMPONENT_KIND_WEB_APP)) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "The component is missing a long description. Components of this type must have a long description."); } else if (cpt_kind == AS_COMPONENT_KIND_FONT) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_PEDANTIC, AS_ISSUE_KIND_TAG_MISSING, "It would be useful for add a long description to this font to present it better to users."); } else if (cpt_kind != AS_COMPONENT_KIND_GENERIC) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_TAG_MISSING, "The component is missing a long description. It is recommended to add one."); } } /* validate console-app specific stuff */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_CONSOLE_APP) { if (as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_BINARY) == NULL) as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_MISSING, "Type 'console-application' component, but no information about binaries in $PATH was provided via a provides/binary tag."); } /* validate font specific stuff */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_FONT) { if (as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_FONT) == NULL) as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_MISSING, "Type 'font' component, but no font information was provided via a provides/font tag."); } /* validate driver specific stuff */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DRIVER) { if (as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_MODALIAS) == NULL) as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_TAG_MISSING, "Type 'driver' component, but no modalias information was provided via a provides/modalias tag."); } /* validate addon specific stuff */ if (as_component_get_extends (cpt)->len > 0) { AsComponentKind kind = as_component_get_kind (cpt); if ((kind != AS_COMPONENT_KIND_ADDON) && (kind != AS_COMPONENT_KIND_LOCALIZATION)) as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_NOT_ALLOWED, "An 'extends' tag is specified, but the component is not of type 'addon' or 'localization'."); } else { if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_ADDON) as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "The component is an addon, but no 'extends' tag was specified."); } /* validate l10n specific stuff */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_LOCALIZATION) { if (as_component_get_extends (cpt)->len == 0) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_MISSING, "This 'localization' component is missing an An 'extends' tag, to specify the components it adds localization to."); } if (g_hash_table_size (as_component_get_languages_table (cpt)) == 0) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "This 'localization' component does not define any languages this localization is for."); } } /* validate suggestions */ if (as_component_get_suggested (cpt)->len > 0) { guint j; GPtrArray *sug_array; sug_array = as_component_get_suggested (cpt); for (j = 0; j < sug_array->len; j++) { AsSuggested *prov = AS_SUGGESTED (g_ptr_array_index (sug_array, j)); if (mode == AS_FORMAT_STYLE_METAINFO) { if (as_suggested_get_kind (prov) != AS_SUGGESTED_KIND_UPSTREAM) as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Suggestions of any type other than 'upstream' are not allowed in metainfo files (type was '%s')", as_suggested_kind_to_string (as_suggested_get_kind (prov))); } } } as_validator_clear_current_cpt (validator); return cpt; }
/** * as_validator_validate_component_node: **/ static AsComponent* as_validator_validate_component_node (AsValidator *validator, AsXMLData *xdt, xmlNode *root) { xmlNode *iter; AsComponent *cpt; guint i; g_autofree gchar *metadata_license = NULL; g_autofree gchar *cpttype = NULL; g_autoptr(GHashTable) found_tags = NULL; g_auto(GStrv) cid_parts = NULL; const gchar *summary; const gchar *cid; AsParserMode mode; found_tags = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); mode = as_xmldata_get_parser_mode (xdt); /* validate the resulting AsComponent for sanity */ cpt = as_component_new (); as_xmldata_parse_component_node (xdt, root, cpt, NULL); as_validator_set_current_cpt (validator, cpt); /* check if component type is valid */ cpttype = (gchar*) xmlGetProp (root, (xmlChar*) "type"); if (cpttype != NULL) { if (as_component_kind_from_string (cpttype) == AS_COMPONENT_KIND_UNKNOWN) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Invalid component type found: %s", cpttype); } } /* validate the AppStream ID */ cid = as_component_get_id (cpt); cid_parts = g_strsplit (cid, ".", 3); if (g_strv_length (cid_parts) != 3) { if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) { /* since the ID and .desktop-file-id are tied together, we can't make this an error for desktop apps */ as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_VALUE_WRONG, "The component ID is not a reverse domain-name. Please update the ID and that of the accompanying .desktop file to follow the latest version of the specifications and avoid future issues."); } else { /* anything which isn't a .desktop app should follow the schema though */ as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "The component ID is no reverse domain-name."); } } else { /* some people just add random dots to their ID - check if we have an actual known TLD as first part, to be more certain that this is a reverse domain name * (this issue happens quite often with old .desktop files) */ if (!as_utils_is_tld (cid_parts[0])) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_VALUE_WRONG, "The component ID might not follow the reverse domain-name schema (we do not know about the TLD '%s').", cid_parts[0]); } } /* validate characters in AppStream ID */ for (i = 0; cid[i] != '\0'; i++) { /* check if we have a printable, alphanumeric ASCII character or a dot, hyphen or underscore */ if ((!g_ascii_isalnum (cid[i])) && (cid[i] != '.') && (cid[i] != '-') && (cid[i] != '_')) { g_autofree gchar *c = NULL; c = g_utf8_substring (cid, i, i + 1); as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "The component ID contains an invalid character: '%s'", c); } } for (iter = root->children; iter != NULL; iter = iter->next) { const gchar *node_name; g_autofree gchar *node_content = NULL; gboolean tag_valid = TRUE; /* discard spaces */ if (iter->type != XML_ELEMENT_NODE) continue; node_name = (const gchar*) iter->name; node_content = (gchar*) xmlNodeGetContent (iter); if (g_strcmp0 (node_name, "id") == 0) { gchar *prop; prop = (gchar*) xmlGetProp (iter, (xmlChar*) "type"); if (prop != NULL) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_PROPERTY_INVALID, "The id tag for \"%s\" still contains a 'type' property, probably from an old conversion.", node_content); } g_free (prop); if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) { if (!g_str_has_suffix (node_content, ".desktop")) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_VALUE_WRONG, "Component id belongs to a desktop-application, but does not resemble the .desktop file name: \"%s\"", node_content); } } else if (g_strcmp0 (node_name, "metadata_license") == 0) { metadata_license = g_strdup (node_content); as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "pkgname") == 0) { if (g_hash_table_contains (found_tags, node_name)) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_PEDANTIC, AS_ISSUE_KIND_TAG_DUPLICATED, "The tag 'pkgname' appears multiple times. You should evaluate creating a metapackage containing the data in order to avoid defining multiple package names per component."); } } else if (g_strcmp0 (node_name, "source_pkgname") == 0) { if (g_hash_table_contains (found_tags, node_name)) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_DUPLICATED, "The tag 'source_pkgname' appears multiple times."); } } else if (g_strcmp0 (node_name, "name") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "summary") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "description") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_description_tag (validator, iter, cpt, mode); } else if (g_strcmp0 (node_name, "icon") == 0) { gchar *prop; prop = as_validator_check_type_property (validator, cpt, iter); if ((g_strcmp0 (prop, "cached") == 0) || (g_strcmp0 (prop, "stock") == 0)) { if (g_strrstr (node_content, "/") != NULL) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Icons of type 'stock' or 'cached' must not contain a full or relative path to the icon."); } g_free (prop); } else if (g_strcmp0 (node_name, "url") == 0) { gchar *prop; prop = as_validator_check_type_property (validator, cpt, iter); if (as_url_kind_from_string (prop) == AS_URL_KIND_UNKNOWN) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_PROPERTY_INVALID, "Invalid property for 'url' tag: \"%s\"", prop); } g_free (prop); } else if (g_strcmp0 (node_name, "categories") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "category", cpt); } else if (g_strcmp0 (node_name, "keywords") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "keyword", cpt); } else if (g_strcmp0 (node_name, "mimetypes") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "mimetype", cpt); } else if (g_strcmp0 (node_name, "provides") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "screenshots") == 0) { as_validator_check_children_quick (validator, iter, "screenshot", cpt); } else if (g_strcmp0 (node_name, "project_license") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "project_group") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "developer_name") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "compulsory_for_desktop") == 0) { as_validator_check_appear_once (validator, iter, found_tags, cpt); } else if (g_strcmp0 (node_name, "releases") == 0) { as_validator_check_children_quick (validator, iter, "release", cpt); } else if ((g_strcmp0 (node_name, "languages") == 0) && (mode == AS_PARSER_MODE_DISTRO)) { as_validator_check_appear_once (validator, iter, found_tags, cpt); as_validator_check_children_quick (validator, iter, "lang", cpt); } else if ((g_strcmp0 (node_name, "translation") == 0) && (mode == AS_PARSER_MODE_UPSTREAM)) { g_autofree gchar *prop = NULL; AsTranslationKind trkind; prop = as_validator_check_type_property (validator, cpt, iter); trkind = as_translation_kind_from_string (prop); if (trkind == AS_TRANSLATION_KIND_UNKNOWN) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Unknown type '%s' for <translation/> tag.", prop); } } else if (g_strcmp0 (node_name, "extends") == 0) { } else if (g_strcmp0 (node_name, "bundle") == 0) { g_autofree gchar *prop = NULL; prop = as_validator_check_type_property (validator, cpt, iter); if ((g_strcmp0 (prop, "limba") != 0) && (g_strcmp0 (prop, "flatpak") != 0)) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Unknown type '%s' for <bundle/> tag.", prop); } } else if (g_strcmp0 (node_name, "update_contact") == 0) { if (mode == AS_PARSER_MODE_DISTRO) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_NOT_ALLOWED, "The 'update_contact' tag should not be included in distro AppStream XML."); } else { as_validator_check_appear_once (validator, iter, found_tags, cpt); } } else if (g_strcmp0 (node_name, "metadata") == 0) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_PEDANTIC, AS_ISSUE_KIND_TAG_UNKNOWN, "Found custom metadata in <metadata/> tag. Use of this tag is common, but should be avoided if possible."); tag_valid = FALSE; } else if (!g_str_has_prefix (node_name, "x-")) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_UNKNOWN, "Found invalid tag: '%s'. Non-standard tags must be prefixed with \"x-\".", node_name); tag_valid = FALSE; } if (tag_valid) { as_validator_check_content_empty (validator, node_content, node_name, AS_ISSUE_IMPORTANCE_WARNING, cpt); } } if (metadata_license == NULL) { if (mode == AS_PARSER_MODE_UPSTREAM) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "The essential tag 'metadata_license' is missing."); } /* check if the summary is sane */ summary = as_component_get_summary (cpt); if ((summary != NULL) && ((strstr (summary, "\n") != NULL) || (strstr (summary, "\t") != NULL))) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "The summary tag must not contain tabs or linebreaks."); } /* check if we have a description */ if (as_str_empty (as_component_get_description (cpt))) { AsComponentKind cpt_kind; cpt_kind = as_component_get_kind (cpt); if ((cpt_kind == AS_COMPONENT_KIND_DESKTOP_APP) || (cpt_kind == AS_COMPONENT_KIND_FONT)) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "The component is missing a long description. Components of this type must have a long description."); } else if (cpt_kind != AS_COMPONENT_KIND_GENERIC) { as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_TAG_MISSING, "The component is missing a long description. It is recommended to add one."); } } /* validate font specific stuff */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_FONT) { if (!g_str_has_suffix (as_component_get_id (cpt), ".font")) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "Components of type 'font' must have an AppStream ID with a '.font' suffix."); if (as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_FONT) == NULL) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_TAG_MISSING, "Type 'font' component, but no font information was provided via a provides/font tag."); } /* validate addon specific stuff */ if (as_component_get_extends (cpt)->len > 0) { if (as_component_get_kind (cpt) != AS_COMPONENT_KIND_ADDON) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_NOT_ALLOWED, "An 'extends' tag is specified, but the component is not an addon."); } else { if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_ADDON) as_validator_add_issue (validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_MISSING, "The component is an addon, but no 'extends' tag was specified."); } as_validator_clear_current_cpt (validator); return cpt; }