summaryrefslogtreecommitdiff
path: root/libappstream-glib/as-app-validate.c
diff options
context:
space:
mode:
authorRichard Hughes <richard@hughsie.com>2014-04-21 12:53:50 +0100
committerRichard Hughes <richard@hughsie.com>2014-04-21 12:53:50 +0100
commit6a8d7f2b3a817c0b69cf58432a1985bd9df8a221 (patch)
treefc5c2401a205d88b715afcde05467af682324229 /libappstream-glib/as-app-validate.c
parent7469f75841f1c335d93a11ec1192515022ad1961 (diff)
downloadappstream-glib-6a8d7f2b3a817c0b69cf58432a1985bd9df8a221.tar.gz
Add as_app_validate() to validate AppStream, AppData and desktop files
Diffstat (limited to 'libappstream-glib/as-app-validate.c')
-rw-r--r--libappstream-glib/as-app-validate.c918
1 files changed, 918 insertions, 0 deletions
diff --git a/libappstream-glib/as-app-validate.c b/libappstream-glib/as-app-validate.c
new file mode 100644
index 0000000..86c2f96
--- /dev/null
+++ b/libappstream-glib/as-app-validate.c
@@ -0,0 +1,918 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2014 Richard Hughes <richard@hughsie.com>
+ *
+ * Licensed under the GNU Lesser General Public License Version 2.1
+ *
+ * This library 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 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "config.h"
+
+#include <fnmatch.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <libsoup/soup.h>
+#include <string.h>
+
+#include "as-app-private.h"
+#include "as-node-private.h"
+#include "as-problem.h"
+
+typedef struct {
+ AsAppValidateFlags flags;
+ GPtrArray *screenshot_urls;
+ GPtrArray *probs;
+ SoupSession *session;
+ gboolean previous_para_was_short;
+ guint para_chars_before_list;
+ guint number_paragraphs;
+} AsAppValidateHelper;
+
+/**
+ * ai_app_validate_add:
+ */
+static void
+ai_app_validate_add (GPtrArray *problems,
+ AsProblemKind kind,
+ const gchar *str)
+{
+ AsProblem *problem;
+ guint i;
+
+ /* already added */
+ for (i = 0; i < problems->len; i++) {
+ problem = g_ptr_array_index (problems, i);
+ if (g_strcmp0 (as_problem_get_message (problem), str) == 0)
+ return;
+ }
+
+ /* add new problem to list */
+ problem = as_problem_new ();
+ as_problem_set_kind (problem, kind);
+ as_problem_set_message (problem, str);
+ g_debug ("Adding %s '%s'", as_problem_kind_to_string (kind), str);
+ g_ptr_array_add (problems, problem);
+}
+
+/**
+ * ai_app_validate_fullstop_ending:
+ *
+ * Returns %TRUE if the string ends in a full stop, unless the string contains
+ * multiple dots. This allows names such as "0 A.D." and summaries to end
+ * with "..."
+ */
+static gboolean
+ai_app_validate_fullstop_ending (const gchar *tmp)
+{
+ guint cnt = 0;
+ guint i;
+ guint str_len;
+
+ for (i = 0; tmp[i] != '\0'; i++)
+ if (tmp[i] == '.')
+ cnt++;
+ if (cnt++ > 1)
+ return FALSE;
+ str_len = strlen (tmp);
+ if (str_len == 0)
+ return FALSE;
+ return tmp[str_len - 1] == '.';
+}
+
+/**
+ * as_app_validate_description_li:
+ **/
+static void
+as_app_validate_description_li (const gchar *text, AsAppValidateHelper *helper)
+{
+ guint str_len;
+ guint length_li_max = 100;
+ guint length_li_min = 20;
+
+ /* relax the requirements a bit */
+ if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
+ length_li_max = 1000;
+ length_li_min = 4;
+ }
+
+ str_len = strlen (text);
+ if (str_len < length_li_min) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<li> is too short");
+ }
+ if (str_len > length_li_max) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<li> is too long");
+ }
+ if (ai_app_validate_fullstop_ending (text)) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<li> cannot end in '.'");
+ }
+}
+
+/**
+ * as_app_validate_description_para:
+ **/
+static void
+as_app_validate_description_para (const gchar *text, AsAppValidateHelper *helper)
+{
+ guint length_para_max = 600;
+ guint length_para_min = 50;
+ guint str_len;
+
+ /* relax the requirements a bit */
+ if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
+ length_para_max = 1000;
+ length_para_min = 10;
+ }
+
+ /* previous was short */
+ if (helper->previous_para_was_short) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<p> is too short [p]");
+ }
+ helper->previous_para_was_short = FALSE;
+
+ str_len = strlen (text);
+ if (str_len < length_para_min) {
+ /* we don't add the problem now, as we allow a short
+ * paragraph as an introduction to a list */
+ helper->previous_para_was_short = TRUE;
+ }
+ if (str_len > length_para_max) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<p> is too long");
+ }
+ if (g_str_has_prefix (text, "This application")) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<p> should not start with 'This application'");
+ }
+ if (text[str_len - 1] != '.' &&
+ text[str_len - 1] != '!' &&
+ text[str_len - 1] != ':') {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<p> does not end in '.|:|!'");
+ }
+ helper->number_paragraphs++;
+ helper->para_chars_before_list += str_len;
+}
+
+/**
+ * as_app_validate_description_list:
+ **/
+static void
+as_app_validate_description_list (const gchar *text, AsAppValidateHelper *helper)
+{
+ guint length_para_before_list = 300;
+
+ /* relax the requirements a bit */
+ if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
+ length_para_before_list = 100;
+ }
+
+ /* ul without a leading para */
+ if (helper->number_paragraphs < 1) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<ul> cannot start a description");
+ }
+ if (helper->para_chars_before_list != 0 &&
+ helper->para_chars_before_list < (guint) length_para_before_list) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "Not enough <p> content before <ul>");
+ }
+
+ /* we allow the previous paragraph to be short to
+ * introduce the list */
+ helper->previous_para_was_short = FALSE;
+ helper->para_chars_before_list = 0;
+}
+
+/**
+ * as_app_validate_description:
+ **/
+static gboolean
+as_app_validate_description (const gchar *xml,
+ AsAppValidateHelper *helper,
+ GError **error)
+{
+ GNode *l;
+ GNode *l2;
+ GNode *node;
+ gboolean ret = TRUE;
+ guint number_para_max = 4;
+ guint number_para_min = 2;
+
+ /* relax the requirements a bit */
+ if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
+ number_para_max = 10;
+ number_para_min = 1;
+ }
+
+ /* parse xml */
+ node = as_node_from_xml (xml, -1,
+ AS_NODE_FROM_XML_FLAG_NONE,
+ error);
+ if (node == NULL) {
+ ret = FALSE;
+ goto out;
+ }
+ for (l = node->children; l != NULL; l = l->next) {
+ if (g_strcmp0 (as_node_get_name (l), "p") == 0) {
+ if (as_node_get_attribute (l, "xml:lang") != NULL)
+ continue;
+ as_app_validate_description_para (as_node_get_data (l),
+ helper);
+ } else if (g_strcmp0 (as_node_get_name (l), "ul") == 0 ||
+ g_strcmp0 (as_node_get_name (l), "ol") == 0) {
+ as_app_validate_description_list (as_node_get_data (l),
+ helper);
+ for (l2 = l->children; l2 != NULL; l2 = l2->next) {
+ if (g_strcmp0 (as_node_get_name (l2), "li") == 0) {
+ if (as_node_get_attribute (l2, "xml:lang") != NULL)
+ continue;
+ as_app_validate_description_li (as_node_get_data (l2),
+ helper);
+ } else {
+ /* only <li> supported */
+ ret = FALSE;
+ g_set_error (error,
+ AS_APP_ERROR,
+ AS_APP_ERROR_FAILED,
+ "invalid markup: <%s> follows <%s>",
+ as_node_get_name (l2),
+ as_node_get_name (l));
+ goto out;
+ }
+ }
+ } else {
+ /* only <p>, <ol> and <ul> supported */
+ ret = FALSE;
+ g_set_error (error,
+ AS_APP_ERROR,
+ AS_APP_ERROR_FAILED,
+ "invalid markup: tag <%s> invalid here",
+ as_node_get_name (l));
+ goto out;
+ }
+ }
+
+ /* previous paragraph wasn't long enough */
+ if (helper->previous_para_was_short) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<p> is too short");
+ }
+ if (helper->number_paragraphs < number_para_min) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "Not enough <p> tags for a good description");
+ }
+ if (helper->number_paragraphs > number_para_max) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "Too many <p> tags for a good description");
+ }
+out:
+ if (node != NULL)
+ as_node_unref (node);
+ return ret;
+}
+
+/**
+ * as_app_validate_image_url_already_exists:
+ */
+static gboolean
+as_app_validate_image_url_already_exists (AsAppValidateHelper *helper,
+ const gchar *search)
+{
+ const gchar *tmp;
+ guint i;
+
+ for (i = 0; i < helper->screenshot_urls->len; i++) {
+ tmp = g_ptr_array_index (helper->screenshot_urls, i);
+ if (g_strcmp0 (tmp, search) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * ai_app_validate_image_check:
+ */
+static gboolean
+ai_app_validate_image_check (AsImage *im, AsAppValidateHelper *helper)
+{
+ GdkPixbuf *pixbuf = NULL;
+ GError *error = NULL;
+ GInputStream *stream = NULL;
+ SoupMessage *msg = NULL;
+ SoupURI *base_uri = NULL;
+ const gchar *url;
+ gboolean require_correct_aspect_ratio = FALSE;
+ gboolean ret = TRUE;
+ gdouble desired_aspect = 1.777777778;
+ gdouble screenshot_aspect;
+ gint status_code;
+ guint screenshot_height;
+ guint screenshot_width;
+ guint ss_size_height_max = 900;
+ guint ss_size_height_min = 351;
+ guint ss_size_width_max = 1600;
+ guint ss_size_width_min = 624;
+
+ /* make the requirements more strict */
+ if ((helper->flags & AS_APP_VALIDATE_FLAG_STRICT) > 0) {
+ require_correct_aspect_ratio = TRUE;
+ }
+
+ /* relax the requirements a bit */
+ if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
+ ss_size_height_max = 1800;
+ ss_size_height_min = 150;
+ ss_size_width_max = 3200;
+ ss_size_width_min = 300;
+ }
+
+ /* have we got network access */
+ if ((helper->flags & AS_APP_VALIDATE_FLAG_NO_NETWORK) > 0)
+ goto out;
+
+ /* GET file */
+ url = as_image_get_url (im);
+ g_debug ("checking %s", url);
+ base_uri = soup_uri_new (url);
+ if (base_uri == NULL) {
+ ret = FALSE;
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_URL_NOT_FOUND,
+ "<screenshot> url not valid");
+ goto out;
+ }
+ msg = soup_message_new_from_uri (SOUP_METHOD_GET, base_uri);
+ if (msg == NULL) {
+ g_warning ("Failed to setup message");
+ goto out;
+ }
+
+ /* send sync */
+ status_code = soup_session_send_message (helper->session, msg);
+ if (status_code != SOUP_STATUS_OK) {
+ ret = FALSE;
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_URL_NOT_FOUND,
+ "<screenshot> url not found");
+ goto out;
+ }
+
+ /* check if it's a zero sized file */
+ if (msg->response_body->length == 0) {
+ ret = FALSE;
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_FILE_INVALID,
+ "<screenshot> url is a zero length file");
+ goto out;
+ }
+
+ /* create a buffer with the data */
+ stream = g_memory_input_stream_new_from_data (msg->response_body->data,
+ msg->response_body->length,
+ NULL);
+ if (stream == NULL) {
+ ret = FALSE;
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_URL_NOT_FOUND,
+ "<screenshot> failed to load data");
+ goto out;
+ }
+
+ /* load the image */
+ pixbuf = gdk_pixbuf_new_from_stream (stream, NULL, &error);
+ if (pixbuf == NULL) {
+ ret = FALSE;
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_FILE_INVALID,
+ "<screenshot> failed to load image");
+ goto out;
+ }
+
+ /* check width matches */
+ screenshot_width = gdk_pixbuf_get_width (pixbuf);
+ screenshot_height = gdk_pixbuf_get_height (pixbuf);
+ if (as_image_get_width (im) != 0 &&
+ as_image_get_width (im) != screenshot_width) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
+ "<screenshot> width did not match specified");
+ }
+
+ /* check height matches */
+ if (as_image_get_height (im) != 0 &&
+ as_image_get_height (im) != screenshot_height) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
+ "<screenshot> height did not match specified");
+ }
+
+ /* check size is reasonable */
+ if (screenshot_width < ss_size_width_min) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
+ "<screenshot> width was too small");
+ }
+ if (screenshot_height < ss_size_height_min) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
+ "<screenshot> height was too small");
+ }
+ if (screenshot_width > ss_size_width_max) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
+ "<screenshot> width was too large");
+ }
+ if (screenshot_height > ss_size_height_max) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
+ "<screenshot> height was too large");
+ }
+
+ /* check aspect ratio */
+ if (require_correct_aspect_ratio) {
+ screenshot_aspect = (gdouble) screenshot_width / (gdouble) screenshot_height;
+ if (ABS (screenshot_aspect - 1.777777777) > 0.1) {
+ g_debug ("got aspect %.2f, wanted %.2f",
+ screenshot_aspect, desired_aspect);
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_ASPECT_RATIO_INCORRECT,
+ "<screenshot> aspect ratio was not 16:9");
+ }
+ }
+out:
+ if (base_uri != NULL)
+ soup_uri_free (base_uri);
+ if (msg != NULL)
+ g_object_unref (msg);
+ if (stream != NULL)
+ g_object_unref (stream);
+ if (pixbuf != NULL)
+ g_object_unref (pixbuf);
+ return ret;
+}
+
+/**
+ * as_app_validate_image:
+ **/
+static void
+as_app_validate_image (AsImage *im, AsAppValidateHelper *helper)
+{
+ const gchar *url;
+ gboolean ret;
+
+ /* blank */
+ url = as_image_get_url (im);
+ if (strlen (url) == 0) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_VALUE_MISSING,
+ "<screenshot> has no content");
+ return;
+ }
+
+ /* check for duplicates */
+ ret = as_app_validate_image_url_already_exists (helper, url);
+ if (ret) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_DUPLICATE_DATA,
+ "<screenshot> has duplicated data");
+ return;
+ }
+
+ /* validate the URL */
+ ret = ai_app_validate_image_check (im, helper);
+ if (ret) {
+ g_ptr_array_add (helper->screenshot_urls, g_strdup (url));
+ }
+}
+
+/**
+ * as_app_validate_screenshot:
+ **/
+static void
+as_app_validate_screenshot (AsScreenshot *ss, AsAppValidateHelper *helper)
+{
+ AsImage *im;
+ GPtrArray *images;
+ guint i;
+
+ if (as_screenshot_get_kind (ss) == AS_SCREENSHOT_KIND_UNKNOWN) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
+ "<screenshot> has unknown type");
+ }
+ images = as_screenshot_get_images (ss);
+ for (i = 0; i < images->len; i++) {
+ im = g_ptr_array_index (images, i);
+ as_app_validate_image (im, helper);
+ }
+}
+
+/**
+ * as_app_validate_screenshots:
+ **/
+static void
+as_app_validate_screenshots (AsApp *app, AsAppValidateHelper *helper)
+{
+ AsScreenshot *ss;
+ GPtrArray *screenshots;
+ gboolean screenshot_has_default = FALSE;
+ guint number_screenshots_max = 5;
+ guint number_screenshots_min = 1;
+ guint i;
+
+ /* relax the requirements a bit */
+ if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
+ number_screenshots_max = 10;
+ number_screenshots_min = 0;
+ }
+
+ /* only for AppData and AppStream */
+ if (as_app_get_source_kind (app) == AS_APP_SOURCE_KIND_DESKTOP)
+ return;
+
+ screenshots = as_app_get_screenshots (app);
+ if (screenshots->len < number_screenshots_min) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "Not enough <screenshot> tags");
+ }
+ if (screenshots->len > number_screenshots_max) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "Too many <screenshot> tags");
+ }
+ for (i = 0; i < screenshots->len; i++) {
+ ss = g_ptr_array_index (screenshots, i);
+ as_app_validate_screenshot (ss, helper);
+ if (as_screenshot_get_kind (ss) == AS_SCREENSHOT_KIND_DEFAULT) {
+ if (screenshot_has_default) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_MARKUP_INVALID,
+ "<screenshot> has more than one default");
+ }
+ screenshot_has_default = TRUE;
+ continue;
+ }
+ }
+ if (screenshots->len > 0 && !screenshot_has_default) {
+ ai_app_validate_add (helper->probs,
+ AS_PROBLEM_KIND_MARKUP_INVALID,
+ "<screenshots> has no default <screenshot>");
+ }
+}
+
+/**
+ * as_app_validate_setup_networking:
+ **/
+static gboolean
+as_app_validate_setup_networking (AsAppValidateHelper *helper, GError **error)
+{
+ gboolean ret = TRUE;
+
+ helper->session = soup_session_sync_new_with_options (SOUP_SESSION_USER_AGENT,
+ "libappstream-glib",
+ SOUP_SESSION_TIMEOUT,
+ 5000,
+ NULL);
+ if (helper->session == NULL) {
+ ret = FALSE;
+ g_set_error_literal (error,
+ AS_APP_ERROR,
+ AS_APP_ERROR_FAILED,
+ "Failed to set up networking");
+ goto out;
+ }
+ soup_session_add_feature_by_type (helper->session,
+ SOUP_TYPE_PROXY_RESOLVER_DEFAULT);
+out:
+ return ret;
+}
+
+/**
+ * as_app_validate:
+ * @app: a #AsApp instance.
+ * @flags: the #AsAppValidateFlags to use, e.g. %AS_APP_VALIDATE_FLAG_NONE
+ * @error: A #GError or %NULL.
+ *
+ * Validates data in the instance for style and consitency.
+ *
+ * Returns: (transfer container) (element-type AsProblem): A list of problems, or %NULL
+ *
+ * Since: 0.1.4
+ **/
+GPtrArray *
+as_app_validate (AsApp *app, AsAppValidateFlags flags, GError **error)
+{
+ AsAppProblems problems;
+ AsAppValidateHelper helper;
+ GError *error_local = NULL;
+ GHashTable *urls;
+ GList *keys;
+ GList *l;
+ GPtrArray *probs = NULL;
+ const gchar *description;
+ const gchar *id_full;
+ const gchar *key;
+ const gchar *metadata_license;
+ const gchar *name;
+ const gchar *summary;
+ const gchar *tmp;
+ const gchar *update_contact;
+ gboolean deprectated_failure = FALSE;
+ gboolean require_contactdetails = TRUE;
+ gboolean require_copyright = FALSE;
+ gboolean require_translations = FALSE;
+ gboolean require_url = TRUE;
+ gboolean ret;
+ guint length_name_max = 30;
+ guint length_name_min = 3;
+ guint length_summary_max = 100;
+ guint length_summary_min = 8;
+ guint str_len;
+
+ /* relax the requirements a bit */
+ if ((flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
+ length_name_max = 100;
+ length_summary_max = 200;
+ require_contactdetails = FALSE;
+ require_url = FALSE;
+ }
+
+ /* make the requirements more strict */
+ if ((flags & AS_APP_VALIDATE_FLAG_STRICT) > 0) {
+ deprectated_failure = TRUE;
+ require_copyright = TRUE;
+ require_translations = TRUE;
+ }
+
+ /* set up networking */
+ helper.probs = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ helper.screenshot_urls = g_ptr_array_new_with_free_func (g_free);
+ helper.flags = flags;
+ helper.previous_para_was_short = FALSE;
+ helper.para_chars_before_list = 0;
+ helper.number_paragraphs = 0;
+ ret = as_app_validate_setup_networking (&helper, error);
+ if (!ret)
+ goto out;
+
+ /* success, enough */
+ probs = helper.probs;
+
+ /* id */
+ ret = FALSE;
+ id_full = as_app_get_id_full (app);
+ switch (as_app_get_id_kind (app)) {
+ case AS_ID_KIND_DESKTOP:
+ if (g_str_has_suffix (id_full, ".desktop"))
+ ret = TRUE;
+ break;
+ case AS_ID_KIND_FONT:
+ if (g_str_has_suffix (id_full, ".ttf"))
+ ret = TRUE;
+ else if (g_str_has_suffix (id_full, ".otf"))
+ ret = TRUE;
+ break;
+ case AS_ID_KIND_INPUT_METHOD:
+ if (g_str_has_suffix (id_full, ".xml"))
+ ret = TRUE;
+ else if (g_str_has_suffix (id_full, ".db"))
+ ret = TRUE;
+ break;
+ case AS_ID_KIND_CODEC:
+ if (g_str_has_prefix (id_full, "gstreamer"))
+ ret = TRUE;
+ break;
+ case AS_ID_KIND_UNKNOWN:
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
+ "<id> has invalid type attribute");
+
+ break;
+ default:
+ break;
+ }
+ if (!ret) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_MARKUP_INVALID,
+ "<id> does not have correct extension for kind");
+ }
+
+ /* metadata_license */
+ metadata_license = as_app_get_metadata_license (app);
+ if (metadata_license != NULL) {
+ if (g_strcmp0 (metadata_license, "CC0") != 0 &&
+ g_strcmp0 (metadata_license, "CC-BY") != 0 &&
+ g_strcmp0 (metadata_license, "CC-BY-SA") != 0 &&
+ g_strcmp0 (metadata_license, "GFDL") != 0) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TAG_INVALID,
+ "<metadata_license> is not valid");
+ }
+ }
+ if (as_app_get_source_kind (app) == AS_APP_SOURCE_KIND_APPDATA &&
+ metadata_license == NULL) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TAG_MISSING,
+ "<metadata_license> is not present");
+ }
+
+ /* updatecontact */
+ update_contact = as_app_get_update_contact (app);
+ if (g_strcmp0 (update_contact,
+ "someone_who_cares@upstream_project.org") == 0) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TAG_INVALID,
+ "<update_contact> is still set to a dummy value");
+ }
+ if (update_contact != NULL && strlen (update_contact) < 6) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<update_contact> is too short");
+ }
+ if (as_app_get_source_kind (app) == AS_APP_SOURCE_KIND_APPDATA &&
+ require_contactdetails &&
+ update_contact == NULL) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TAG_MISSING,
+ "<updatecontact> is not present");
+ }
+
+ /* only found for files */
+ problems = as_app_get_problems (app);
+ if (as_app_get_source_kind (app) == AS_APP_SOURCE_KIND_APPDATA) {
+ if ((problems & AS_APP_PROBLEM_NO_XML_HEADER) > 0) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_MARKUP_INVALID,
+ "<?xml> header not found");
+ }
+ if (require_copyright &&
+ (problems & AS_APP_PROBLEM_NO_COPYRIGHT_INFO) > 0) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_VALUE_MISSING,
+ "<!-- Copyright [year] [name] --> is not present");
+ }
+ }
+
+ /* check for things that have to exist */
+ if (as_app_get_id_full (app) == NULL) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TAG_MISSING,
+ "<id> is not present");
+ }
+
+ /* url */
+ urls = as_app_get_urls (app);
+ keys = g_hash_table_get_keys (urls);
+ for (l = keys; l != NULL; l = l->next) {
+ key = l->data;
+ if (g_strcmp0 (key, "unknown") == 0) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TAG_INVALID,
+ "<url> type invalid");
+ }
+ tmp = g_hash_table_lookup (urls, key);
+ if (!g_str_has_prefix (tmp, "http://") &&
+ !g_str_has_prefix (tmp, "https://")) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TAG_INVALID,
+ "<url> does not start with 'http://'");
+ }
+ }
+ g_list_free (keys);
+
+ /* screenshots */
+ as_app_validate_screenshots (app, &helper);
+
+ /* name */
+ name = as_app_get_name (app, "C");
+ if (name != NULL) {
+ str_len = strlen (name);
+ if (str_len < length_name_min) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<name> is too short");
+ }
+ if (str_len > length_name_max) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<name> is too long");
+ }
+ if (ai_app_validate_fullstop_ending (name)) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<name> cannot end in '.'");
+ }
+ }
+
+ /* comment */
+ summary = as_app_get_comment (app, "C");
+ if (summary != NULL) {
+ str_len = strlen (summary);
+ if (str_len < length_summary_min) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<summary> is too short");
+ }
+ if (str_len > length_summary_max) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<summary> is too long");
+ }
+ if (ai_app_validate_fullstop_ending (summary)) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<summary> cannot end in '.'");
+ }
+ }
+ if (summary != NULL && name != NULL &&
+ strlen (summary) < strlen (name)) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_STYLE_INCORRECT,
+ "<summary> is shorter than <name>");
+ }
+ description = as_app_get_description (app, "C");
+ if (description != NULL) {
+ ret = as_app_validate_description (description,
+ &helper,
+ &error_local);
+ if (!ret) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_MARKUP_INVALID,
+ error_local->message);
+ g_error_free (error_local);
+ }
+ }
+ if (require_translations) {
+ if (name != NULL && as_app_get_name_size (app) == 1) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TRANSLATIONS_REQUIRED,
+ "<name> has no translations");
+ }
+ if (summary != NULL && as_app_get_comment_size (app) == 1) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TRANSLATIONS_REQUIRED,
+ "<summary> has no translations");
+ }
+ if (description != NULL && as_app_get_description_size (app) == 1) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TRANSLATIONS_REQUIRED,
+ "<description> has no translations");
+ }
+ }
+
+ /* using deprecated names */
+ if (deprectated_failure && (problems & AS_APP_PROBLEM_DEPRECATED_LICENCE) > 0) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
+ "<licence> is deprecated, use "
+ "<metadata_license> instead");
+ }
+ if ((problems & AS_APP_PROBLEM_MULTIPLE_ENTRIES) > 0) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_MARKUP_INVALID,
+ "<application> used more than once");
+ }
+
+ /* require homepage */
+ if (as_app_get_source_kind (app) == AS_APP_SOURCE_KIND_APPDATA &&
+ require_url &&
+ as_app_get_url_item (app, AS_URL_KIND_HOMEPAGE) == NULL) {
+ ai_app_validate_add (probs,
+ AS_PROBLEM_KIND_TAG_MISSING,
+ "<url> is not present");
+ }
+out:
+ g_ptr_array_unref (helper.screenshot_urls);
+ if (helper.session != NULL)
+ g_object_unref (helper.session);
+ return probs;
+}