1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2  *
3  * Copyright (C) 2014 Richard Hughes <richard@hughsie.com>
4  *
5  * SPDX-License-Identifier: LGPL-2.1+
6  */
7 
8 #include "config.h"
9 
10 #include <gdk-pixbuf/gdk-pixbuf.h>
11 #include <libsoup/soup.h>
12 #include <libsoup/soup-status.h>
13 #include <string.h>
14 
15 #include "as-app-private.h"
16 #include "as-node-private.h"
17 #include "as-problem.h"
18 #include "as-utils.h"
19 
20 typedef struct {
21 	AsApp			*app;
22 	AsAppValidateFlags	 flags;
23 	GPtrArray		*screenshot_urls;
24 	GPtrArray		*probs;
25 	SoupSession		*session;
26 	gboolean		 previous_para_was_short;
27 	gchar			*previous_para_was_short_str;
28 	guint			 para_chars_before_list;
29 	guint			 number_paragraphs;
30 } AsAppValidateHelper;
31 
32 G_GNUC_PRINTF (3, 4) static void
ai_app_validate_add(AsAppValidateHelper * helper,AsProblemKind kind,const gchar * fmt,...)33 ai_app_validate_add (AsAppValidateHelper *helper,
34 		     AsProblemKind kind,
35 		     const gchar *fmt, ...)
36 {
37 	AsProblem *problem;
38 	guint i;
39 	va_list args;
40 	g_autofree gchar *str = NULL;
41 
42 	va_start (args, fmt);
43 	str = g_strdup_vprintf (fmt, args);
44 	va_end (args);
45 
46 	/* don't care about style when relaxed */
47 	if (helper->flags & AS_APP_VALIDATE_FLAG_RELAX &&
48 	    kind == AS_PROBLEM_KIND_STYLE_INCORRECT)
49 		return;
50 
51 	/* already added */
52 	for (i = 0; i < helper->probs->len; i++) {
53 		problem = g_ptr_array_index (helper->probs, i);
54 		if (g_strcmp0 (as_problem_get_message (problem), str) == 0)
55 			return;
56 	}
57 
58 	/* add new problem to list */
59 	problem = as_problem_new ();
60 	as_problem_set_kind (problem, kind);
61 	as_problem_set_message (problem, str);
62 	g_debug ("Adding %s '%s'", as_problem_kind_to_string (kind), str);
63 	g_ptr_array_add (helper->probs, problem);
64 }
65 
66 /**
67  * ai_app_validate_fullstop_ending:
68  *
69  * Returns %TRUE if the string ends in a full stop, unless the string contains
70  * multiple dots. This allows names such as "0 A.D." and summaries to end
71  * with "..."
72  */
73 static gboolean
ai_app_validate_fullstop_ending(const gchar * tmp)74 ai_app_validate_fullstop_ending (const gchar *tmp)
75 {
76 	guint cnt = 0;
77 	guint i;
78 	guint str_len;
79 
80 	for (i = 0; tmp[i] != '\0'; i++)
81 		if (tmp[i] == '.')
82 			cnt++;
83 	if (cnt++ > 1)
84 		return FALSE;
85 	str_len = (guint) strlen (tmp);
86 	if (str_len == 0)
87 		return FALSE;
88 	return tmp[str_len - 1] == '.';
89 }
90 
91 static gboolean
as_app_validate_has_hyperlink(const gchar * text)92 as_app_validate_has_hyperlink (const gchar *text)
93 {
94 	if (g_strstr_len (text, -1, "http://") != NULL)
95 		return TRUE;
96 	if (g_strstr_len (text, -1, "https://") != NULL)
97 		return TRUE;
98 	if (g_strstr_len (text, -1, "ftp://") != NULL)
99 		return TRUE;
100 	return FALSE;
101 }
102 
103 static gboolean
as_app_validate_has_email(const gchar * text)104 as_app_validate_has_email (const gchar *text)
105 {
106 	if (g_strstr_len (text, -1, "@") != NULL)
107 		return TRUE;
108 	if (g_strstr_len (text, -1, "_at_") != NULL)
109 		return TRUE;
110 	return FALSE;
111 }
112 
113 static gboolean
as_app_validate_has_first_word_capital(AsAppValidateHelper * helper,const gchar * text)114 as_app_validate_has_first_word_capital (AsAppValidateHelper *helper, const gchar *text)
115 {
116 	g_autofree gchar *first_word = NULL;
117 	gchar *tmp;
118 	guint i;
119 
120 	if (text == NULL || text[0] == '\0')
121 		return TRUE;
122 
123 	/* text starts with a number */
124 	if (g_ascii_isdigit (text[0]))
125 		return TRUE;
126 
127 	/* get the first word */
128 	first_word = g_strdup (text);
129 	tmp = g_strstr_len (first_word, -1, " ");
130 	if (tmp != NULL)
131 		*tmp = '\0';
132 
133 	/* does the word have caps anywhere? */
134 	for (i = 0; first_word[i] != '\0'; i++) {
135 		if (first_word[i] >= 'A' && first_word[i] <= 'Z')
136 			return TRUE;
137 	}
138 
139 	/* is the first word the project name */
140 	if (g_strcmp0 (first_word, as_app_get_name (helper->app, NULL)) == 0)
141 		return TRUE;
142 
143 	return FALSE;
144 }
145 
146 static void
as_app_validate_description_li(const gchar * text,AsAppValidateHelper * helper)147 as_app_validate_description_li (const gchar *text, AsAppValidateHelper *helper)
148 {
149 	gboolean require_sentence_case = FALSE;
150 	guint str_len;
151 	guint length_li_max = 500;
152 	guint length_li_min = 3;
153 
154 	/* make the requirements more strict */
155 	if ((helper->flags & AS_APP_VALIDATE_FLAG_STRICT) > 0) {
156 		require_sentence_case = TRUE;
157 		length_li_max = 250;
158 	}
159 
160 	/* relax the requirements a bit */
161 	if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
162 		length_li_max = 1000;
163 		length_li_min = 3;
164 		require_sentence_case = FALSE;
165 	}
166 
167 	/* empty */
168 	if (text == NULL) {
169 		ai_app_validate_add (helper,
170 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
171 				     "<li> is empty");
172 		return;
173 	}
174 
175 	str_len = (guint) strlen (text);
176 	if (str_len < length_li_min) {
177 		ai_app_validate_add (helper,
178 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
179 				     "<li> is too short [%s] minimum is %u chars",
180 				     text, length_li_min);
181 	}
182 	if (str_len > length_li_max) {
183 		ai_app_validate_add (helper,
184 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
185 				     "<li> is too long [%s] maximum is %u chars",
186 				     text, length_li_max);
187 	}
188 	if (require_sentence_case && ai_app_validate_fullstop_ending (text)) {
189 		ai_app_validate_add (helper,
190 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
191 				     "<li> cannot end in '.' [%s]", text);
192 	}
193 	if (as_app_validate_has_hyperlink (text)) {
194 		ai_app_validate_add (helper,
195 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
196 				     "<li> cannot contain a hyperlink [%s]",
197 				     text);
198 	}
199 	if (require_sentence_case &&
200 	    !as_app_validate_has_first_word_capital (helper, text)) {
201 		ai_app_validate_add (helper,
202 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
203 				     "<li> requires sentence case [%s]", text);
204 	}
205 }
206 
207 static void
as_app_validate_description_para(const gchar * text,AsAppValidateHelper * helper)208 as_app_validate_description_para (const gchar *text, AsAppValidateHelper *helper)
209 {
210 	gboolean require_sentence_case = FALSE;
211 	guint length_para_max = 1000;
212 	guint length_para_min = 10;
213 	guint str_len;
214 
215 	/* empty */
216 	if (text == NULL) {
217 		ai_app_validate_add (helper,
218 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
219 				     "<p> was empty");
220 		return;
221 	}
222 
223 	/* make the requirements more strict */
224 	if ((helper->flags & AS_APP_VALIDATE_FLAG_STRICT) > 0) {
225 		require_sentence_case = TRUE;
226 	}
227 
228 	/* relax the requirements a bit */
229 	if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
230 		length_para_max = 2000;
231 		length_para_min = 5;
232 	}
233 
234 	/* previous was short */
235 	if (helper->previous_para_was_short) {
236 		ai_app_validate_add (helper,
237 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
238 				     "<p> is too short [%s]", text);
239 	}
240 	helper->previous_para_was_short = FALSE;
241 
242 	str_len = (guint) strlen (text);
243 	if (str_len < length_para_min) {
244 		/* we don't add the problem now, as we allow a short
245 		 * paragraph as an introduction to a list */
246 		helper->previous_para_was_short = TRUE;
247 		g_free (helper->previous_para_was_short_str);
248 		helper->previous_para_was_short_str = g_strdup (text);
249 	}
250 	if (str_len > length_para_max) {
251 		ai_app_validate_add (helper,
252 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
253 				     "<p> is too long [%s], maximum is %u chars",
254 				     text, length_para_max);
255 	}
256 	if (g_str_has_prefix (text, "This application")) {
257 		ai_app_validate_add (helper,
258 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
259 				     "<p> should not start with 'This application'");
260 	}
261 	if (as_app_validate_has_hyperlink (text)) {
262 		ai_app_validate_add (helper,
263 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
264 				     "<p> cannot contain a hyperlink [%s]",
265 				     text);
266 	}
267 	if (require_sentence_case &&
268 	    !as_app_validate_has_first_word_capital (helper, text)) {
269 		ai_app_validate_add (helper,
270 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
271 				     "<p> requires sentence case [%s]", text);
272 	}
273 	if (require_sentence_case &&
274 	    text[str_len - 1] != '.' &&
275 	    text[str_len - 1] != '!' &&
276 	    text[str_len - 1] != ':') {
277 		ai_app_validate_add (helper,
278 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
279 				     "<p> does not end in '.|:|!' [%s]", text);
280 	}
281 	helper->number_paragraphs++;
282 	helper->para_chars_before_list += str_len;
283 }
284 
285 static void
as_app_validate_description_list(const gchar * text,gboolean allow_short_para,AsAppValidateHelper * helper)286 as_app_validate_description_list (const gchar *text,
287 				  gboolean allow_short_para,
288 				  AsAppValidateHelper *helper)
289 {
290 	guint length_para_before_list = 20;
291 
292 	/* relax the requirements a bit */
293 	if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
294 		length_para_before_list = 100;
295 	}
296 
297 	/* ul without a leading para */
298 	if (helper->number_paragraphs < 1) {
299 		ai_app_validate_add (helper,
300 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
301 				     "<ul> cannot start a description [%s]",
302 				     text);
303 	}
304 	if (!allow_short_para &&
305 	    helper->para_chars_before_list != 0 &&
306 	    helper->para_chars_before_list < (guint) length_para_before_list) {
307 		ai_app_validate_add (helper,
308 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
309 				     "Content before <ul> is too short [%u], at least %u characters required",
310 				     helper->para_chars_before_list,
311 				     length_para_before_list);
312 	}
313 
314 	/* we allow the previous paragraph to be short to
315 	 * introduce the list */
316 	helper->previous_para_was_short = FALSE;
317 	helper->para_chars_before_list = 0;
318 }
319 
320 static gboolean
as_app_validate_description(const gchar * xml,AsAppValidateHelper * helper,guint number_para_min,guint number_para_max,gboolean allow_short_para,GError ** error)321 as_app_validate_description (const gchar *xml,
322 			     AsAppValidateHelper *helper,
323 			     guint number_para_min,
324 			     guint number_para_max,
325 			     gboolean allow_short_para,
326 			     GError **error)
327 {
328 	GNode *l;
329 	GNode *l2;
330 	g_autoptr(AsNode) node = NULL;
331 
332 	/* parse xml */
333 	node = as_node_from_xml (xml, AS_NODE_FROM_XML_FLAG_NONE, error);
334 	if (node == NULL)
335 		return FALSE;
336 	helper->number_paragraphs = 0;
337 	helper->previous_para_was_short = FALSE;
338 	for (l = node->children; l != NULL; l = l->next) {
339 		if (g_strcmp0 (as_node_get_name (l), "p") == 0) {
340 			if (as_node_get_attribute (l, "xml:lang") != NULL)
341 				continue;
342 			as_app_validate_description_para (as_node_get_data (l),
343 							  helper);
344 		} else if (g_strcmp0 (as_node_get_name (l), "ul") == 0 ||
345 			   g_strcmp0 (as_node_get_name (l), "ol") == 0) {
346 			as_app_validate_description_list (as_node_get_data (l),
347 							  allow_short_para,
348 							  helper);
349 			for (l2 = l->children; l2 != NULL; l2 = l2->next) {
350 				if (g_strcmp0 (as_node_get_name (l2), "li") == 0) {
351 					if (as_node_get_attribute (l2, "xml:lang") != NULL)
352 						continue;
353 					as_app_validate_description_li (as_node_get_data (l2),
354 									helper);
355 				} else {
356 					/* only <li> supported */
357 					g_set_error (error,
358 						     AS_APP_ERROR,
359 						     AS_APP_ERROR_FAILED,
360 						     "invalid markup: <%s> follows <%s>",
361 						     as_node_get_name (l2),
362 						     as_node_get_name (l));
363 					return FALSE;
364 				}
365 			}
366 		} else {
367 			/* only <p>, <ol> and <ul> supported */
368 			g_set_error (error,
369 				     AS_APP_ERROR,
370 				     AS_APP_ERROR_FAILED,
371 				     "invalid markup: tag <%s> invalid here",
372 				     as_node_get_name (l));
373 			return FALSE;
374 		}
375 	}
376 
377 	/* previous paragraph wasn't long enough */
378 	if (helper->previous_para_was_short) {
379 		ai_app_validate_add (helper,
380 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
381 				     "<p> is too short [%s]",
382 				     helper->previous_para_was_short_str);
383 	}
384 	if (helper->number_paragraphs < number_para_min) {
385 		ai_app_validate_add (helper,
386 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
387 				     "Not enough <p> tags for a good description [%u/%u]",
388 				     helper->number_paragraphs,
389 				     number_para_min);
390 	}
391 	if (helper->number_paragraphs > number_para_max) {
392 		ai_app_validate_add (helper,
393 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
394 				     "Too many <p> tags for a good description [%u/%u]",
395 				     helper->number_paragraphs,
396 				     number_para_max);
397 	}
398 	return TRUE;
399 }
400 
401 static gboolean
as_app_validate_image_url_already_exists(AsAppValidateHelper * helper,const gchar * search)402 as_app_validate_image_url_already_exists (AsAppValidateHelper *helper,
403 					  const gchar *search)
404 {
405 	const gchar *tmp;
406 	guint i;
407 
408 	for (i = 0; i < helper->screenshot_urls->len; i++) {
409 		tmp = g_ptr_array_index (helper->screenshot_urls, i);
410 		if (g_strcmp0 (tmp, search) == 0)
411 			return TRUE;
412 	}
413 	return FALSE;
414 }
415 
416 static gboolean
ai_app_validate_image_check(AsImage * im,AsAppValidateHelper * helper)417 ai_app_validate_image_check (AsImage *im, AsAppValidateHelper *helper)
418 {
419 	AsImageAlphaFlags alpha_flags;
420 	const gchar *url;
421 	gboolean require_correct_aspect_ratio = FALSE;
422 	gdouble desired_aspect = 1.777777778;
423 	gdouble screenshot_aspect;
424 	guint status_code;
425 	guint screenshot_height;
426 	guint screenshot_width;
427 	guint ss_size_height_max = 900;
428 	guint ss_size_height_min = 351;
429 	guint ss_size_width_max = 1600;
430 	guint ss_size_width_min = 624;
431 	g_autoptr(GdkPixbuf) pixbuf = NULL;
432 	g_autoptr(GInputStream) stream = NULL;
433 	g_autoptr(SoupMessage) msg = NULL;
434 	g_autoptr(SoupURI) base_uri = NULL;
435 
436 	/* make the requirements more strict */
437 	if ((helper->flags & AS_APP_VALIDATE_FLAG_STRICT) > 0) {
438 		require_correct_aspect_ratio = TRUE;
439 	}
440 
441 	/* relax the requirements a bit */
442 	if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
443 		ss_size_height_max = 1800;
444 		ss_size_height_min = 150;
445 		ss_size_width_max = 3200;
446 		ss_size_width_min = 300;
447 	}
448 
449 	/* have we got network access */
450 	if ((helper->flags & AS_APP_VALIDATE_FLAG_NO_NETWORK) > 0)
451 		return TRUE;
452 
453 	/* GET file */
454 	url = as_image_get_url (im);
455 	g_debug ("checking %s", url);
456 	base_uri = soup_uri_new (url);
457 	if (!SOUP_URI_VALID_FOR_HTTP (base_uri)) {
458 		ai_app_validate_add (helper,
459 				     AS_PROBLEM_KIND_URL_NOT_FOUND,
460 				     "<screenshot> url not valid [%s]", url);
461 		return FALSE;
462 	}
463 	msg = soup_message_new_from_uri (SOUP_METHOD_GET, base_uri);
464 	if (msg == NULL) {
465 		g_warning ("Failed to setup message");
466 		return FALSE;
467 	}
468 
469 	/* send sync */
470 	status_code = soup_session_send_message (helper->session, msg);
471 	if (SOUP_STATUS_IS_TRANSPORT_ERROR(status_code)) {
472 		ai_app_validate_add (helper,
473 			AS_PROBLEM_KIND_URL_NOT_FOUND,
474 			"<screenshot> failed to connect: %s [%s]",
475 			soup_status_get_phrase(status_code), url);
476 		return FALSE;
477 	} else if (status_code != SOUP_STATUS_OK) {
478 		ai_app_validate_add (helper,
479 			AS_PROBLEM_KIND_URL_NOT_FOUND,
480 			"<screenshot> failed to download (HTTP %d: %s) [%s]",
481 			status_code, soup_status_get_phrase(status_code), url);
482 		return FALSE;
483 	}
484 
485 	/* check if it's a zero sized file */
486 	if (msg->response_body->length == 0) {
487 		ai_app_validate_add (helper,
488 				     AS_PROBLEM_KIND_FILE_INVALID,
489 				     "<screenshot> url is a zero length file [%s]",
490 				     url);
491 		return FALSE;
492 	}
493 
494 	/* create a buffer with the data */
495 	stream = g_memory_input_stream_new_from_data (msg->response_body->data,
496 						      (gssize) msg->response_body->length,
497 						      NULL);
498 	if (stream == NULL) {
499 		ai_app_validate_add (helper,
500 				     AS_PROBLEM_KIND_URL_NOT_FOUND,
501 				     "<screenshot> failed to load data [%s]",
502 				     url);
503 		return FALSE;
504 	}
505 
506 	/* load the image */
507 	pixbuf = gdk_pixbuf_new_from_stream (stream, NULL, NULL);
508 	if (pixbuf == NULL) {
509 		ai_app_validate_add (helper,
510 				     AS_PROBLEM_KIND_FILE_INVALID,
511 				     "<screenshot> failed to load [%s]",
512 				     url);
513 		return FALSE;
514 	}
515 
516 	/* check width matches */
517 	screenshot_width = (guint) gdk_pixbuf_get_width (pixbuf);
518 	screenshot_height = (guint) gdk_pixbuf_get_height (pixbuf);
519 	if (as_image_get_width (im) != 0 &&
520 	    as_image_get_width (im) != screenshot_width) {
521 		ai_app_validate_add (helper,
522 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
523 				     "<screenshot> width (%u) did not match specified (%u) [%s]",
524 				     as_image_get_width (im), screenshot_width, url);
525 	}
526 
527 	/* check height matches */
528 	if (as_image_get_height (im) != 0 &&
529 	    as_image_get_height (im) != screenshot_height) {
530 		ai_app_validate_add (helper,
531 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
532 				     "<screenshot> height (%u) did not match specified (%u) [%s]",
533 				     as_image_get_height (im), screenshot_height, url);
534 	}
535 
536 	/* check size is reasonable */
537 	if (screenshot_width < ss_size_width_min) {
538 		ai_app_validate_add (helper,
539 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
540 				     "<screenshot> width (%u) too small [%s] minimum is %upx",
541 				     screenshot_width, url, ss_size_width_min);
542 	}
543 	if (screenshot_height < ss_size_height_min) {
544 		ai_app_validate_add (helper,
545 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
546 				     "<screenshot> height too small [%s] minimum is %upx",
547 				     url, ss_size_height_min);
548 	}
549 	if (screenshot_width > ss_size_width_max) {
550 		ai_app_validate_add (helper,
551 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
552 				     "<screenshot> width too large [%s] maximum is %upx",
553 				     url, ss_size_width_max);
554 	}
555 	if (screenshot_height > ss_size_height_max) {
556 		ai_app_validate_add (helper,
557 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
558 				     "<screenshot> height too large [%s] maximum is %upx",
559 				     url, ss_size_height_max);
560 	}
561 
562 	/* check padding */
563 	as_image_set_pixbuf (im, pixbuf);
564 	alpha_flags = as_image_get_alpha_flags (im);
565 	if ((alpha_flags & AS_IMAGE_ALPHA_FLAG_TOP) > 0||
566 	    (alpha_flags & AS_IMAGE_ALPHA_FLAG_BOTTOM) > 0) {
567 		ai_app_validate_add (helper,
568 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
569 				     "<image> has vertical padding [%s]",
570 				     url);
571 	}
572 	if ((alpha_flags & AS_IMAGE_ALPHA_FLAG_LEFT) > 0||
573 	    (alpha_flags & AS_IMAGE_ALPHA_FLAG_RIGHT) > 0) {
574 		ai_app_validate_add (helper,
575 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
576 				     "<image> has horizontal padding [%s]",
577 				     url);
578 	}
579 
580 	/* check aspect ratio */
581 	if (require_correct_aspect_ratio) {
582 		screenshot_aspect = (gdouble) screenshot_width / (gdouble) screenshot_height;
583 		if (ABS (screenshot_aspect - 1.777777777) > 0.1) {
584 			g_debug ("got aspect %.2f, wanted %.2f",
585 				 screenshot_aspect, desired_aspect);
586 			ai_app_validate_add (helper,
587 					     AS_PROBLEM_KIND_ASPECT_RATIO_INCORRECT,
588 					     "<screenshot> aspect ratio not 16:9 [%s]",
589 					     url);
590 		}
591 	}
592 	return TRUE;
593 }
594 
595 static void
as_app_validate_image(AsImage * im,AsAppValidateHelper * helper)596 as_app_validate_image (AsImage *im, AsAppValidateHelper *helper)
597 {
598 	const gchar *url;
599 	gboolean ret;
600 
601 	/* blank */
602 	url = as_image_get_url (im);
603 	if (url == NULL || (guint) strlen (url) == 0) {
604 		ai_app_validate_add (helper,
605 				     AS_PROBLEM_KIND_VALUE_MISSING,
606 				     "<screenshot> has no content");
607 		return;
608 	}
609 
610 	/* check for duplicates */
611 	ret = as_app_validate_image_url_already_exists (helper, url);
612 	if (ret) {
613 		ai_app_validate_add (helper,
614 				     AS_PROBLEM_KIND_DUPLICATE_DATA,
615 				     "<screenshot> has duplicated data");
616 		return;
617 	}
618 
619 	/* validate the URL */
620 	ret = ai_app_validate_image_check (im, helper);
621 	if (ret)
622 		g_ptr_array_add (helper->screenshot_urls, g_strdup (url));
623 }
624 
625 static void
as_app_validate_screenshot(AsScreenshot * ss,AsAppValidateHelper * helper)626 as_app_validate_screenshot (AsScreenshot *ss, AsAppValidateHelper *helper)
627 {
628 	AsImage *im;
629 	GPtrArray *images;
630 	const gchar *tmp;
631 	gboolean require_sentence_case = TRUE;
632 	guint i;
633 	guint length_caption_max = 50;
634 	guint length_caption_min = 10;
635 	guint str_len;
636 
637 	/* relax the requirements a bit */
638 	if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
639 		length_caption_max = 100;
640 		length_caption_min = 5;
641 		require_sentence_case = FALSE;
642 	}
643 
644 	if (as_screenshot_get_kind (ss) == AS_SCREENSHOT_KIND_UNKNOWN) {
645 		ai_app_validate_add (helper,
646 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
647 				     "<screenshot> has unknown type");
648 	}
649 	images = as_screenshot_get_images (ss);
650 	for (i = 0; i < images->len; i++) {
651 		im = g_ptr_array_index (images, i);
652 		as_app_validate_image (im, helper);
653 	}
654 	tmp = as_screenshot_get_caption (ss, NULL);
655 	if (tmp != NULL) {
656 		str_len = (guint) strlen (tmp);
657 		if (str_len < length_caption_min) {
658 			ai_app_validate_add (helper,
659 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
660 					     "<caption> is too short [%s];"
661 					     "shortest allowed is %u chars",
662 					     tmp, length_caption_min);
663 		}
664 		if (str_len > length_caption_max) {
665 			ai_app_validate_add (helper,
666 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
667 					     "<caption> is too long [%s];"
668 					     "longest allowed is %u chars",
669 					     tmp, length_caption_max);
670 		}
671 		if (ai_app_validate_fullstop_ending (tmp)) {
672 			ai_app_validate_add (helper,
673 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
674 					     "<caption> cannot end in '.' [%s]",
675 					     tmp);
676 		}
677 		if (as_app_validate_has_hyperlink (tmp)) {
678 			ai_app_validate_add (helper,
679 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
680 					     "<caption> cannot contain a hyperlink [%s]",
681 					     tmp);
682 		}
683 		if (require_sentence_case &&
684 		    !as_app_validate_has_first_word_capital (helper, tmp)) {
685 			ai_app_validate_add (helper,
686 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
687 					     "<caption> requires sentence case [%s]",
688 					     tmp);
689 		}
690 	}
691 }
692 
693 static void
as_app_validate_icons(AsApp * app,AsAppValidateHelper * helper)694 as_app_validate_icons (AsApp *app, AsAppValidateHelper *helper)
695 {
696 	AsIcon *icon;
697 	AsIconKind icon_kind;
698 	const gchar *icon_name;
699 
700 	/* just check the default icon */
701 	icon = as_app_get_icon_default (app);
702 	if (icon == NULL) {
703 		AsFormat *fmt = as_app_get_format_default (app);
704 		if (fmt != NULL &&
705 		    as_format_get_kind (fmt) == AS_FORMAT_KIND_APPSTREAM &&
706 		    as_app_get_kind (app) == AS_APP_KIND_DESKTOP) {
707 			ai_app_validate_add (helper,
708 					     AS_PROBLEM_KIND_TAG_MISSING,
709 					     "desktop application has no icon");
710 		}
711 		return;
712 	}
713 
714 	/* check the content is correct */
715 	icon_kind = as_icon_get_kind (icon);
716 	switch (icon_kind) {
717 	case AS_ICON_KIND_STOCK:
718 		icon_name = as_icon_get_name (icon);
719 		if (!as_utils_is_stock_icon_name (icon_name)) {
720 			ai_app_validate_add (helper,
721 					     AS_PROBLEM_KIND_TAG_INVALID,
722 					     "stock icon is not valid [%s]",
723 					     icon_name);
724 		}
725 		break;
726 	case AS_ICON_KIND_LOCAL:
727 		icon_name = as_icon_get_filename (icon);
728 		if (icon_name == NULL ||
729 		    !g_str_has_prefix (icon_name, "/")) {
730 			ai_app_validate_add (helper,
731 					     AS_PROBLEM_KIND_TAG_INVALID,
732 					     "local icon is not a filename [%s]",
733 					     icon_name);
734 		}
735 		break;
736 	case AS_ICON_KIND_CACHED:
737 		icon_name = as_icon_get_name (icon);
738 		if (icon_name == NULL ||
739 		    g_str_has_prefix (icon_name, "/")) {
740 			ai_app_validate_add (helper,
741 					     AS_PROBLEM_KIND_TAG_INVALID,
742 					     "cached icon is a filename [%s]",
743 					     icon_name);
744 		}
745 		break;
746 	case AS_ICON_KIND_REMOTE:
747 		icon_name = as_icon_get_url (icon);
748 		if (!g_str_has_prefix (icon_name, "http://") &&
749 		    !g_str_has_prefix (icon_name, "https://")) {
750 			ai_app_validate_add (helper,
751 					     AS_PROBLEM_KIND_TAG_INVALID,
752 					     "remote icon is not a url [%s]",
753 					     icon_name);
754 		}
755 		break;
756 	default:
757 		break;
758 	}
759 }
760 
761 static void
as_app_validate_screenshots(AsApp * app,AsAppValidateHelper * helper)762 as_app_validate_screenshots (AsApp *app, AsAppValidateHelper *helper)
763 {
764 	AsFormat *format;
765 	AsScreenshot *ss;
766 	GPtrArray *screenshots;
767 	gboolean screenshot_has_default = FALSE;
768 	guint number_screenshots_max = 25;
769 	guint number_screenshots_min = 1;
770 	guint i;
771 
772 	/* relax the requirements a bit */
773 	if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
774 		number_screenshots_max = 10;
775 		number_screenshots_min = 0;
776 	}
777 
778 	/* firmware does not need screenshots */
779 	if (as_app_get_kind (app) == AS_APP_KIND_FIRMWARE ||
780 	    as_app_get_kind (app) == AS_APP_KIND_DRIVER ||
781 	    as_app_get_kind (app) == AS_APP_KIND_RUNTIME ||
782 	    as_app_get_kind (app) == AS_APP_KIND_ADDON ||
783 	    as_app_get_kind (app) == AS_APP_KIND_LOCALIZATION)
784 		number_screenshots_min = 0;
785 
786 	/* metainfo and inf do not require any screenshots */
787 	format = as_app_get_format_default (app);
788 	if (as_format_get_kind (format) == AS_FORMAT_KIND_METAINFO)
789 		number_screenshots_min = 0;
790 
791 	/* only for AppData and AppStream */
792 	if (as_format_get_kind (format) == AS_FORMAT_KIND_DESKTOP)
793 		return;
794 
795 	screenshots = as_app_get_screenshots (app);
796 	if (screenshots->len < number_screenshots_min) {
797 		ai_app_validate_add (helper,
798 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
799 				     "Not enough <screenshot> tags, minimum is %u",
800 				     number_screenshots_min);
801 	}
802 	if (screenshots->len > number_screenshots_max) {
803 		ai_app_validate_add (helper,
804 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
805 				     "Too many <screenshot> tags, maximum is %u",
806 				     number_screenshots_max);
807 	}
808 	for (i = 0; i < screenshots->len; i++) {
809 		ss = g_ptr_array_index (screenshots, i);
810 		as_app_validate_screenshot (ss, helper);
811 		if (as_screenshot_get_kind (ss) == AS_SCREENSHOT_KIND_DEFAULT) {
812 			if (screenshot_has_default) {
813 				ai_app_validate_add (helper,
814 						     AS_PROBLEM_KIND_MARKUP_INVALID,
815 						     "<screenshot> has more than one default");
816 			}
817 			screenshot_has_default = TRUE;
818 			continue;
819 		}
820 	}
821 	if (screenshots->len > 0 && !screenshot_has_default) {
822 		ai_app_validate_add (helper,
823 				     AS_PROBLEM_KIND_MARKUP_INVALID,
824 				     "<screenshots> has no default <screenshot>");
825 	}
826 }
827 
828 static gboolean
as_app_validate_release(AsApp * app,AsRelease * release,AsAppValidateHelper * helper,GError ** error)829 as_app_validate_release (AsApp *app,
830 			 AsRelease *release,
831 			 AsAppValidateHelper *helper,
832 			 GError **error)
833 {
834 	const gchar *tmp;
835 	guint64 timestamp;
836 	guint number_para_max = 10;
837 	guint number_para_min = 1;
838 	gboolean required_timestamp = TRUE;
839 	gboolean required_past_timestamp = TRUE;
840 	const guint64 MAX_TZ_OFFSET = 14 * 60 * 60; /* UTC+14 is the biggest offset */
841 
842 	/* relax the requirements a bit */
843 	if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
844 		number_para_max = 20;
845 		required_timestamp = FALSE;
846 		required_past_timestamp = FALSE;
847 	}
848 
849 	/* make the requirements more strict */
850 	if ((helper->flags & AS_APP_VALIDATE_FLAG_STRICT) > 0) {
851 		number_para_max = 4;
852 	}
853 
854 	/* check version */
855 	tmp = as_release_get_version (release);
856 	if (tmp == NULL) {
857 		ai_app_validate_add (helper,
858 				     AS_PROBLEM_KIND_ATTRIBUTE_MISSING,
859 				     "<release> has no version");
860 	}
861 
862 	/* check timestamp */
863 	timestamp = as_release_get_timestamp (release);
864 	if (required_timestamp && timestamp == 0) {
865 		ai_app_validate_add (helper,
866 				     AS_PROBLEM_KIND_ATTRIBUTE_MISSING,
867 				     "<release> has no timestamp");
868 	}
869 	if (timestamp > 20120101 && timestamp < 20351231) {
870 		ai_app_validate_add (helper,
871 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
872 				     "<release> timestamp should be a UNIX time");
873 	}
874 
875 	/* check the timestamp is not in the future */
876 	if (required_past_timestamp && timestamp > (guint64) g_get_real_time () / G_USEC_PER_SEC + MAX_TZ_OFFSET) {
877 		ai_app_validate_add (helper,
878 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
879 				     "<release> timestamp is in the future");
880 	}
881 
882 	/* for firmware, check urgency */
883 	if (as_app_get_kind (app) == AS_APP_KIND_FIRMWARE &&
884 	    as_release_get_urgency (release) == AS_URGENCY_KIND_UNKNOWN) {
885 		ai_app_validate_add (helper,
886 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
887 				     "<release> urgency is required for firmware");
888 	}
889 
890 	/* check description */
891 	tmp = as_release_get_description (release, "C");
892 	if (tmp != NULL) {
893 		if (as_app_validate_has_hyperlink (tmp)) {
894 			ai_app_validate_add (helper,
895 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
896 					     "<release> description should be "
897 					     "prose and not contain hyperlinks [%s]",
898 					     tmp);
899 		}
900 		if (!as_app_validate_description (tmp,
901 						  helper,
902 						  number_para_min,
903 						  number_para_max,
904 						  TRUE,
905 						  error))
906 			return FALSE;
907 	}
908 	return TRUE;
909 }
910 
911 static gboolean
as_app_validate_kudos(AsApp * app,AsAppValidateHelper * helper,GError ** error)912 as_app_validate_kudos (AsApp *app, AsAppValidateHelper *helper, GError **error)
913 {
914 	GPtrArray *kudos = as_app_get_kudos (app);
915 	for (guint i = 0; i < kudos->len; i++) {
916 		const gchar *kudo = g_ptr_array_index (kudos, i);
917 		const gchar *valid[] = { "AppMenu",
918 					 "HiDpiIcon",
919 					 "HighContrast",
920 					 "ModernToolkit",
921 					 "Notifications",
922 					 "SearchProvider",
923 					 "UserDocs",
924 					 NULL };
925 		if (!g_strv_contains (valid, kudo)) {
926 			ai_app_validate_add (helper,
927 					     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
928 					     "<kudo> is invalid [%s]", kudo);
929 		}
930 	}
931 	return TRUE;
932 }
933 
934 static gboolean
as_app_validate_releases(AsApp * app,AsAppValidateHelper * helper,GError ** error)935 as_app_validate_releases (AsApp *app, AsAppValidateHelper *helper, GError **error)
936 {
937 	GPtrArray *releases;
938 	AsFormat *format;
939 	gboolean require_release = FALSE;
940 
941 	/* only for AppData */
942 	format = as_app_get_format_default (app);
943 	if (as_format_get_kind (format) != AS_FORMAT_KIND_APPDATA &&
944 	    as_format_get_kind (format) != AS_FORMAT_KIND_METAINFO)
945 		return TRUE;
946 
947 	/* make the requirements more strict */
948 	if ((helper->flags & AS_APP_VALIDATE_FLAG_RELAX) == 0) {
949 		/* only for desktop and console apps */
950 		if (as_app_get_kind (app) == AS_APP_KIND_DESKTOP ||
951 		    as_app_get_kind (app) == AS_APP_KIND_CONSOLE) {
952 			require_release = TRUE;
953 		}
954 	}
955 
956 	/* require releases */
957 	releases = as_app_get_releases (app);
958 	if (require_release && releases->len == 0) {
959 		ai_app_validate_add (helper,
960 				     AS_PROBLEM_KIND_TAG_MISSING,
961 				     "<release> required");
962 		return TRUE;
963 	}
964 	for (guint i = 0; i < releases->len; i++) {
965 		AsRelease *release = g_ptr_array_index (releases, i);
966 		if (!as_app_validate_release (app, release, helper, error))
967 			return FALSE;
968 	}
969 
970 	/* check the version numbers go down each time */
971 	if (releases->len > 1) {
972 		AsRelease *release_old = g_ptr_array_index (releases, 0);
973 		for (guint i = 1; i < releases->len; i++) {
974 			AsRelease *release = g_ptr_array_index (releases, i);
975 			const gchar *version = as_release_get_version (release);
976 			const gchar *version_old = as_release_get_version (release_old);
977 			if (version == NULL || version_old == NULL)
978 				continue;
979 			if (as_utils_vercmp_full (version, version_old, AS_VERSION_COMPARE_FLAG_NONE) > 0) {
980 				ai_app_validate_add (helper,
981 						     AS_PROBLEM_KIND_TAG_INVALID,
982 						     "<release> versions are not in order "
983 						     "[%s before %s]",
984 						     version_old, version);
985 			}
986 			release_old = release;
987 		}
988 	}
989 
990 	return TRUE;
991 }
992 
993 static gboolean
as_app_validate_setup_networking(AsAppValidateHelper * helper,GError ** error)994 as_app_validate_setup_networking (AsAppValidateHelper *helper, GError **error)
995 {
996 	helper->session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT,
997 							 "libappstream-glib",
998 							 SOUP_SESSION_TIMEOUT,
999 							 5000,
1000 							 NULL);
1001 	if (helper->session == NULL) {
1002 		g_set_error_literal (error,
1003 				     AS_APP_ERROR,
1004 				     AS_APP_ERROR_FAILED,
1005 				     "Failed to set up networking");
1006 		return FALSE;
1007 	}
1008 	soup_session_add_feature_by_type (helper->session,
1009 					  SOUP_TYPE_PROXY_RESOLVER_DEFAULT);
1010 	return TRUE;
1011 }
1012 
1013 static gboolean
as_app_validate_license(const gchar * license_text,GError ** error)1014 as_app_validate_license (const gchar *license_text, GError **error)
1015 {
1016 	guint i;
1017 	g_auto(GStrv) licenses = NULL;
1018 
1019 	licenses = as_utils_spdx_license_tokenize (license_text);
1020 	if (licenses == NULL) {
1021 		g_set_error (error,
1022 			     AS_APP_ERROR,
1023 			     AS_APP_ERROR_FAILED,
1024 			     "SPDX license text '%s' could not be parsed",
1025 			     license_text);
1026 		return FALSE;
1027 	}
1028 	for (i = 0; licenses[i] != NULL; i++) {
1029 		if (g_strcmp0 (licenses[i], "&") == 0 ||
1030 		    g_strcmp0 (licenses[i], "|") == 0 ||
1031 		    g_strcmp0 (licenses[i], "+") == 0 ||
1032 		    g_strcmp0 (licenses[i], "(") == 0 ||
1033 		    g_strcmp0 (licenses[i], ")") == 0)
1034 			continue;
1035 		if (licenses[i][0] != '@' ||
1036 		    !as_utils_is_spdx_license_id (licenses[i] + 1)) {
1037 			g_set_error (error,
1038 				     AS_APP_ERROR,
1039 				     AS_APP_ERROR_FAILED,
1040 				     "SPDX ID '%s' unknown",
1041 				     licenses[i]);
1042 			return FALSE;
1043 		}
1044 	}
1045 	return TRUE;
1046 }
1047 
1048 static gboolean
as_app_validate_is_content_license_id(const gchar * license_id)1049 as_app_validate_is_content_license_id (const gchar *license_id)
1050 {
1051 	if (g_strcmp0 (license_id, "@FSFAP") == 0)
1052 		return TRUE;
1053 	if (g_strcmp0 (license_id, "@MIT") == 0)
1054 		return TRUE;
1055 	if (g_strcmp0 (license_id, "@0BSD") == 0)
1056 		return TRUE;
1057 	if (g_strcmp0 (license_id, "@CC0-1.0") == 0)
1058 		return TRUE;
1059 	if (g_strcmp0 (license_id, "@CC-BY-3.0") == 0)
1060 		return TRUE;
1061 	if (g_strcmp0 (license_id, "@CC-BY-4.0") == 0)
1062 		return TRUE;
1063 	if (g_strcmp0 (license_id, "@CC-BY-SA-3.0") == 0)
1064 		return TRUE;
1065 	if (g_strcmp0 (license_id, "@CC-BY-SA-4.0") == 0)
1066 		return TRUE;
1067 	if (g_strcmp0 (license_id, "@GFDL-1.1") == 0)
1068 		return TRUE;
1069 	if (g_strcmp0 (license_id, "@GFDL-1.2") == 0)
1070 		return TRUE;
1071 	if (g_strcmp0 (license_id, "@GFDL-1.3") == 0)
1072 		return TRUE;
1073 	if (g_strcmp0 (license_id, "@BSL-1.0") == 0)
1074 		return TRUE;
1075 	if (g_strcmp0 (license_id, "@FTL") == 0)
1076 		return TRUE;
1077 	if (g_strcmp0 (license_id, "@FSFUL") == 0)
1078 		return TRUE;
1079 	return FALSE;
1080 }
1081 
1082 static gboolean
as_app_validate_is_content_license(const gchar * license)1083 as_app_validate_is_content_license (const gchar *license)
1084 {
1085 	gboolean requires_all_tokens = TRUE;
1086 	guint content_license_bad_cnt = 0;
1087 	guint content_license_good_cnt = 0;
1088 	g_auto(GStrv) tokens = NULL;
1089 	tokens = as_utils_spdx_license_tokenize (license);
1090 	if (tokens == NULL)
1091 		return FALSE;
1092 
1093 	/* this is too complicated to process */
1094 	for (guint i = 0; tokens[i] != NULL; i++) {
1095 		if (g_strcmp0 (tokens[i], "(") == 0 ||
1096 		    g_strcmp0 (tokens[i], ")") == 0)
1097 			return FALSE;
1098 	}
1099 
1100 	/* this is a simple expression parser and can be easily tricked */
1101 	for (guint i = 0; tokens[i] != NULL; i++) {
1102 		if (g_strcmp0 (tokens[i], "+") == 0)
1103 			continue;
1104 		if (g_strcmp0 (tokens[i], "|") == 0) {
1105 			requires_all_tokens = FALSE;
1106 			continue;
1107 		}
1108 		if (g_strcmp0 (tokens[i], "&") == 0) {
1109 			requires_all_tokens = TRUE;
1110 			continue;
1111 		}
1112 		if (as_app_validate_is_content_license_id (tokens[i])) {
1113 			content_license_good_cnt++;
1114 		} else {
1115 			content_license_bad_cnt++;
1116 		}
1117 	}
1118 
1119 	/* any valid token makes this valid */
1120 	if (!requires_all_tokens && content_license_good_cnt > 0)
1121 		return TRUE;
1122 
1123 	/* all tokens are required to be valid */
1124 	if (requires_all_tokens && content_license_bad_cnt == 0)
1125 		return TRUE;
1126 
1127 	/* either the license was bad, or it was too complex to process */
1128 	return FALSE;
1129 }
1130 
1131 static void
as_app_validate_helper_free(AsAppValidateHelper * helper)1132 as_app_validate_helper_free (AsAppValidateHelper *helper)
1133 {
1134 	g_ptr_array_unref (helper->screenshot_urls);
1135 	g_free (helper->previous_para_was_short_str);
1136 	if (helper->session != NULL)
1137 		g_object_unref (helper->session);
1138 	g_free (helper);
1139 }
1140 
1141 G_DEFINE_AUTOPTR_CLEANUP_FUNC(AsAppValidateHelper, as_app_validate_helper_free);
1142 
1143 static gboolean
as_app_validate_check_id_char(const gchar c)1144 as_app_validate_check_id_char (const gchar c)
1145 {
1146 	const gchar valid[] = { '-', '_', '.', '\0' };
1147 	const gchar invalid[] = { '/', '\\', '\0' };
1148 	for (guint i = 0; invalid[i] != '\0'; i++) {
1149 		if (invalid[i] == c)
1150 			return FALSE;
1151 	}
1152 	for (guint i = 0; valid[i] != '\0'; i++) {
1153 		if (valid[i] == c)
1154 			return TRUE;
1155 	}
1156 	return g_ascii_isalnum (c);
1157 }
1158 
1159 static void
as_app_validate_check_id(AsAppValidateHelper * helper,const gchar * id)1160 as_app_validate_check_id (AsAppValidateHelper *helper, const gchar *id)
1161 {
1162 	/* check valid */
1163 	if (id == NULL) {
1164 		ai_app_validate_add (helper,
1165 				     AS_PROBLEM_KIND_MARKUP_INVALID,
1166 				     "<id> is not set");
1167 		return;
1168 	}
1169 
1170 	/* check contains permitted chars */
1171 	for (guint i = 0; id[i] != '\0'; i++) {
1172 		if (!as_app_validate_check_id_char (id[i])) {
1173 			ai_app_validate_add (helper,
1174 					     AS_PROBLEM_KIND_MARKUP_INVALID,
1175 					     "<id> has invalid character [%c]",
1176 					     id[i]);
1177 			break;
1178 		}
1179 	}
1180 }
1181 
1182 static void
as_app_validate_launchables(AsApp * app,AsAppValidateHelper * helper)1183 as_app_validate_launchables (AsApp *app, AsAppValidateHelper *helper)
1184 {
1185 	GPtrArray *launchables = as_app_get_launchables (app);
1186 
1187 	/* launchable isn't required */
1188 	if (launchables == NULL)
1189 		return;
1190 
1191 	/* check each launchable in the file */
1192 	for (guint j = 0; j < launchables->len; j++) {
1193 		AsLaunchable *tmp = g_ptr_array_index (launchables, j);
1194 
1195 		if (as_launchable_get_kind (tmp) == AS_LAUNCHABLE_KIND_UNKNOWN) {
1196 			ai_app_validate_add (helper,
1197 			     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
1198 			     "<launchable> has invalid type attribute");
1199 			continue;
1200 		}
1201 
1202 		if (as_launchable_get_value (tmp) == NULL) {
1203 			ai_app_validate_add (helper,
1204 			     AS_PROBLEM_KIND_VALUE_MISSING,
1205 			     "<launchable> missing value");
1206 			continue;
1207 		}
1208 	}
1209 }
1210 
1211 /**
1212  * as_app_validate:
1213  * @app: a #AsApp instance.
1214  * @flags: the #AsAppValidateFlags to use, e.g. %AS_APP_VALIDATE_FLAG_NONE
1215  * @error: A #GError or %NULL.
1216  *
1217  * Validates data in the instance for style and consistency.
1218  *
1219  * Returns: (transfer container) (element-type AsProblem): A list of problems, or %NULL
1220  *
1221  * Since: 0.1.4
1222  **/
1223 GPtrArray *
as_app_validate(AsApp * app,guint32 flags,GError ** error)1224 as_app_validate (AsApp *app, guint32 flags, GError **error)
1225 {
1226 	AsAppProblems problems;
1227 	AsFormat *format;
1228 	GError *error_local = NULL;
1229 	GHashTable *urls;
1230 	GList *l;
1231 	const gchar *description;
1232 	const gchar *key;
1233 	const gchar *license;
1234 	const gchar *name;
1235 	const gchar *summary;
1236 	const gchar *tmp;
1237 	const gchar *update_contact;
1238 	gboolean deprecated_failure = FALSE;
1239 	gboolean require_appstream_spec_only = FALSE;
1240 	gboolean require_contactdetails = FALSE;
1241 	gboolean require_copyright = FALSE;
1242 	gboolean require_description = FALSE;
1243 	gboolean require_project_license = FALSE;
1244 	gboolean require_sentence_case = FALSE;
1245 	gboolean require_translations = FALSE;
1246 	gboolean require_url = TRUE;
1247 	gboolean require_content_license = TRUE;
1248 	gboolean require_name = TRUE;
1249 	gboolean require_translation = FALSE;
1250 	gboolean require_content_rating = FALSE;
1251 	gboolean require_name_shorter_than_summary = FALSE;
1252 	gboolean validate_license = TRUE;
1253 	gboolean ret;
1254 	guint length_name_max = 60;
1255 	guint length_name_min = 3;
1256 	guint length_summary_max = 200;
1257 	guint length_summary_min = 8;
1258 	guint number_para_max = 10;
1259 	guint number_para_min = 1;
1260 	guint str_len;
1261 	g_autoptr(GList) keys = NULL;
1262 	g_autoptr(AsAppValidateHelper) helper = g_new0 (AsAppValidateHelper, 1);
1263 
1264 	/* has to be set */
1265 	format = as_app_get_format_default (app);
1266 	if (format == NULL) {
1267 		g_set_error_literal (error,
1268 				     AS_APP_ERROR,
1269 				     AS_APP_ERROR_FAILED,
1270 				     "cannot validate without at least one format");
1271 		return NULL;
1272 	}
1273 
1274 	/* only for desktop and console apps */
1275 	if (as_app_get_kind (app) == AS_APP_KIND_DESKTOP ||
1276 	    as_app_get_kind (app) == AS_APP_KIND_CONSOLE) {
1277 		require_content_rating = TRUE;
1278 		require_description = TRUE;
1279 	}
1280 
1281 	/* relax the requirements a bit */
1282 	if ((flags & AS_APP_VALIDATE_FLAG_RELAX) > 0) {
1283 		length_name_max = 100;
1284 		length_summary_max = 200;
1285 		require_content_license = FALSE;
1286 		validate_license = FALSE;
1287 		require_url = FALSE;
1288 		number_para_max = 20;
1289 		number_para_min = 1;
1290 		require_sentence_case = FALSE;
1291 		require_content_rating = FALSE;
1292 		switch (as_format_get_kind (format)) {
1293 		case AS_FORMAT_KIND_METAINFO:
1294 		case AS_FORMAT_KIND_APPDATA:
1295 			require_name = FALSE;
1296 			break;
1297 		default:
1298 			break;
1299 		}
1300 	}
1301 
1302 	/* make the requirements more strict */
1303 	if ((flags & AS_APP_VALIDATE_FLAG_STRICT) > 0) {
1304 		deprecated_failure = TRUE;
1305 		require_copyright = TRUE;
1306 		require_translations = TRUE;
1307 		require_project_license = TRUE;
1308 		require_content_license = TRUE;
1309 		require_appstream_spec_only = TRUE;
1310 		require_sentence_case = TRUE;
1311 		require_name_shorter_than_summary = TRUE;
1312 		require_contactdetails = TRUE;
1313 		require_translation = TRUE;
1314 		number_para_min = 2;
1315 		number_para_max = 4;
1316 	}
1317 
1318 	/* addons don't need such a long description */
1319 	switch (as_format_get_kind (format)) {
1320 	case AS_FORMAT_KIND_METAINFO:
1321 	case AS_FORMAT_KIND_APPDATA:
1322 		number_para_min = 1;
1323 		break;
1324 	default:
1325 		break;
1326 	}
1327 
1328 	/* set up networking */
1329 	helper->app = app;
1330 	helper->probs = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
1331 	helper->screenshot_urls = g_ptr_array_new_with_free_func (g_free);
1332 	helper->flags = flags;
1333 	if (!as_app_validate_setup_networking (helper, error))
1334 		return NULL;
1335 
1336 	/* invalid component type */
1337 	if (as_app_get_kind (app) == AS_APP_KIND_UNKNOWN) {
1338 		ai_app_validate_add (helper,
1339 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
1340 				     "<component> has invalid type attribute");
1341 
1342 	}
1343 	as_app_validate_check_id (helper, as_app_get_id (app));
1344 
1345 	/* metadata_license */
1346 	license = as_app_get_metadata_license (app);
1347 	if (license != NULL) {
1348 		if (require_content_license &&
1349 		    !as_app_validate_is_content_license (license)) {
1350 			ai_app_validate_add (helper,
1351 					     AS_PROBLEM_KIND_TAG_INVALID,
1352 					     "<metadata_license> is not valid [%s]",
1353 					     license);
1354 		} else if (validate_license) {
1355 			if (!as_app_validate_license (license, &error_local)) {
1356 				g_prefix_error (&error_local,
1357 						"<metadata_license> is not valid [%s]",
1358 						license);
1359 				ai_app_validate_add (helper,
1360 						     AS_PROBLEM_KIND_TAG_INVALID,
1361 						     "%s", error_local->message);
1362 				g_clear_error (&error_local);
1363 			}
1364 		}
1365 	}
1366 	if (license == NULL) {
1367 		switch (as_format_get_kind (format)) {
1368 		case AS_FORMAT_KIND_APPDATA:
1369 		case AS_FORMAT_KIND_METAINFO:
1370 			ai_app_validate_add (helper,
1371 					     AS_PROBLEM_KIND_TAG_MISSING,
1372 					     "<metadata_license> is not present");
1373 			break;
1374 		default:
1375 			break;
1376 		}
1377 	}
1378 
1379 	/* project_license */
1380 	license = as_app_get_project_license (app);
1381 	if (license != NULL && validate_license) {
1382 		if (!as_app_validate_license (license, &error_local)) {
1383 			g_prefix_error (&error_local,
1384 					"<project_license> is not valid [%s]",
1385 					license);
1386 			ai_app_validate_add (helper,
1387 					     AS_PROBLEM_KIND_TAG_INVALID,
1388 					     "%s", error_local->message);
1389 			g_clear_error (&error_local);
1390 		}
1391 	}
1392 	if (require_project_license && license == NULL) {
1393 		switch (as_format_get_kind (format)) {
1394 		case AS_FORMAT_KIND_APPDATA:
1395 		case AS_FORMAT_KIND_METAINFO:
1396 			ai_app_validate_add (helper,
1397 					     AS_PROBLEM_KIND_TAG_MISSING,
1398 					     "<project_license> is not present");
1399 			break;
1400 		default:
1401 			break;
1402 		}
1403 	}
1404 
1405 	/* categories */
1406 	if (as_format_get_kind (format) == AS_FORMAT_KIND_APPSTREAM &&
1407 	    as_app_get_kind (app) == AS_APP_KIND_DESKTOP) {
1408 		GPtrArray *categories = as_app_get_categories (app);
1409 		guint nr_toplevel_cats = 0;
1410 		const gchar *cats[] = { "AudioVideo",
1411 					"Development",
1412 					"Education",
1413 					"Game",
1414 					"Graphics",
1415 					"Network",
1416 					"Office",
1417 					"Science",
1418 					"Settings",
1419 					"System",
1420 					"Utility",
1421 					NULL };
1422 		for (guint i = 0; i < categories->len; i++) {
1423 			const gchar *cat = g_ptr_array_index (categories, i);
1424 			for (guint j = 0; cats[j] != NULL; j++) {
1425 				if (g_strcmp0 (cats[j], cat) == 0)
1426 					nr_toplevel_cats++;
1427 			}
1428 		}
1429 		if (nr_toplevel_cats == 0) {
1430 			ai_app_validate_add (helper,
1431 					     AS_PROBLEM_KIND_TAG_MISSING,
1432 					     "<category> must include main categories "
1433 					     "from the desktop entry spec");
1434 		} else if (nr_toplevel_cats > 3) {
1435 			ai_app_validate_add (helper,
1436 					     AS_PROBLEM_KIND_TAG_MISSING,
1437 					     "too many main <category> types: %u",
1438 					     nr_toplevel_cats);
1439 		}
1440 	}
1441 
1442 	/* translation */
1443 	if (require_translation &&
1444 	    as_format_get_kind (format) == AS_FORMAT_KIND_APPDATA &&
1445 	    as_app_get_translations (app)->len == 0) {
1446 		ai_app_validate_add (helper,
1447 				     AS_PROBLEM_KIND_TAG_MISSING,
1448 				     "<translation> not specified");
1449 	}
1450 
1451 	/* pkgname */
1452 	if (as_app_get_pkgname_default (app) != NULL &&
1453 	    as_format_get_kind (format) == AS_FORMAT_KIND_METAINFO) {
1454 		ai_app_validate_add (helper,
1455 				     AS_PROBLEM_KIND_TAG_INVALID,
1456 				     "<pkgname> not allowed in metainfo");
1457 	}
1458 
1459 	/* appdata */
1460 	if (as_format_get_kind (format) == AS_FORMAT_KIND_APPDATA &&
1461 	    as_app_get_kind (app) == AS_APP_KIND_DESKTOP) {
1462 		AsIcon *icon = as_app_get_icon_default (app);
1463 		if (icon != NULL &&
1464 		    as_icon_get_kind (icon) != AS_ICON_KIND_REMOTE) {
1465 			ai_app_validate_add (helper,
1466 					     AS_PROBLEM_KIND_TAG_INVALID,
1467 					     "<icon> not allowed in desktop appdata");
1468 		}
1469 	}
1470 
1471 	/* extends */
1472 	if (as_app_get_extends(app)->len == 0 &&
1473 	    as_app_get_kind (app) == AS_APP_KIND_ADDON &&
1474 	    as_format_get_kind (format) == AS_FORMAT_KIND_METAINFO) {
1475 		ai_app_validate_add (helper,
1476 				     AS_PROBLEM_KIND_TAG_MISSING,
1477 				     "<extends> is not present");
1478 	}
1479 
1480 	/* update_contact */
1481 	update_contact = as_app_get_update_contact (app);
1482 	if (g_strcmp0 (update_contact,
1483 		       "someone_who_cares@upstream_project.org") == 0) {
1484 		ai_app_validate_add (helper,
1485 				     AS_PROBLEM_KIND_TAG_INVALID,
1486 				     "<update_contact> is still set to a dummy value");
1487 	}
1488 	if (update_contact != NULL && strlen (update_contact) < 6) {
1489 		ai_app_validate_add (helper,
1490 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
1491 				     "<update_contact> is too short [%s]",
1492 				     update_contact);
1493 	}
1494 	if (require_contactdetails && update_contact == NULL) {
1495 		switch (as_format_get_kind (format)) {
1496 		case AS_FORMAT_KIND_APPDATA:
1497 		case AS_FORMAT_KIND_METAINFO:
1498 			ai_app_validate_add (helper,
1499 					     AS_PROBLEM_KIND_TAG_MISSING,
1500 					     "<update_contact> is not present");
1501 			break;
1502 		default:
1503 			break;
1504 		}
1505 	}
1506 
1507 	/* only found for files */
1508 	problems = as_app_get_problems (app);
1509 	if (as_format_get_kind (format) == AS_FORMAT_KIND_APPDATA ||
1510 	    as_format_get_kind (format) == AS_FORMAT_KIND_METAINFO) {
1511 		if ((problems & AS_APP_PROBLEM_NO_XML_HEADER) > 0) {
1512 			ai_app_validate_add (helper,
1513 					     AS_PROBLEM_KIND_MARKUP_INVALID,
1514 					     "<?xml> header not found");
1515 		}
1516 		if (require_copyright &&
1517 		    (problems & AS_APP_PROBLEM_NO_COPYRIGHT_INFO) > 0) {
1518 			ai_app_validate_add (helper,
1519 					     AS_PROBLEM_KIND_VALUE_MISSING,
1520 					     "<!-- Copyright [year] [name] --> is not present");
1521 		}
1522 		if (deprecated_failure &&
1523 		    (problems & AS_APP_PROBLEM_UPDATECONTACT_FALLBACK) > 0) {
1524 			ai_app_validate_add (helper,
1525 					     AS_PROBLEM_KIND_TAG_INVALID,
1526 					     "<updatecontact> should be <update_contact>");
1527 		}
1528 	}
1529 
1530 	/* check invalid values */
1531 	if ((problems & AS_APP_PROBLEM_INVALID_PROJECT_GROUP) > 0) {
1532 		ai_app_validate_add (helper,
1533 				     AS_PROBLEM_KIND_TAG_INVALID,
1534 				     "<project_group> is not valid");
1535 	}
1536 
1537 	/* only allow XML in the specification */
1538 	if (require_appstream_spec_only &&
1539 	    (problems & AS_APP_PROBLEM_INVALID_XML_TAG) > 0) {
1540 		ai_app_validate_add (helper,
1541 				     AS_PROBLEM_KIND_TAG_INVALID,
1542 				     "XML data contains unknown tag");
1543 	}
1544 
1545 	/* only allow XML in the specification */
1546 	if (problems & AS_APP_PROBLEM_EXPECTED_CHILDREN) {
1547 		ai_app_validate_add (helper,
1548 				     AS_PROBLEM_KIND_TAG_INVALID,
1549 				     "Expected children for tag");
1550 	}
1551 
1552 	/* only allow XML in the specification */
1553 	if (problems & AS_APP_PROBLEM_INVALID_KEYWORDS) {
1554 		ai_app_validate_add (helper,
1555 				     AS_PROBLEM_KIND_TAG_INVALID,
1556 				     "<keyword> invalid contents");
1557 	}
1558 
1559 	/* releases all have to have unique versions */
1560 	if (problems & AS_APP_PROBLEM_DUPLICATE_RELEASE) {
1561 		ai_app_validate_add (helper,
1562 				     AS_PROBLEM_KIND_TAG_INVALID,
1563 				     "<release> version was duplicated");
1564 	}
1565 	if (problems & AS_APP_PROBLEM_DUPLICATE_SCREENSHOT) {
1566 		ai_app_validate_add (helper,
1567 				     AS_PROBLEM_KIND_TAG_INVALID,
1568 				     "<screenshot> content was duplicated");
1569 	}
1570 	if (problems & AS_APP_PROBLEM_DUPLICATE_CONTENT_RATING) {
1571 		ai_app_validate_add (helper,
1572 				     AS_PROBLEM_KIND_TAG_INVALID,
1573 				     "<content_rating> was duplicated");
1574 	}
1575 
1576 	/* check for things that have to exist */
1577 	if (as_app_get_id (app) == NULL) {
1578 		ai_app_validate_add (helper,
1579 				     AS_PROBLEM_KIND_TAG_MISSING,
1580 				     "<id> is not present");
1581 	}
1582 
1583 	/* games require a content rating */
1584 	if (require_content_rating) {
1585 		GPtrArray *ratings = as_app_get_content_ratings (app);
1586 		if (ratings->len == 0) {
1587 			ai_app_validate_add (helper,
1588 					     AS_PROBLEM_KIND_TAG_MISSING,
1589 					     "<content_rating> required "
1590 					     "[use https://odrs.gnome.org/oars]");
1591 		}
1592 	}
1593 
1594 	/* url */
1595 	urls = as_app_get_urls (app);
1596 	keys = g_hash_table_get_keys (urls);
1597 	for (l = keys; l != NULL; l = l->next) {
1598 		key = l->data;
1599 		if (g_strcmp0 (key, "unknown") == 0) {
1600 			ai_app_validate_add (helper,
1601 					     AS_PROBLEM_KIND_TAG_INVALID,
1602 					     "<url> type invalid [%s]", key);
1603 		}
1604 		tmp = g_hash_table_lookup (urls, key);
1605 		if (tmp == NULL || tmp[0] == '\0')
1606 			continue;
1607 		if (!g_str_has_prefix (tmp, "http://") &&
1608 		    !g_str_has_prefix (tmp, "https://")) {
1609 			ai_app_validate_add (helper,
1610 					     AS_PROBLEM_KIND_TAG_INVALID,
1611 					     "<url> does not start with 'http://' [%s]",
1612 					     tmp);
1613 		}
1614 	}
1615 
1616 	/* screenshots */
1617 	as_app_validate_screenshots (app, helper);
1618 
1619 	/* icons */
1620 	as_app_validate_icons (app, helper);
1621 
1622 	/* launchables */
1623 	as_app_validate_launchables (app, helper);
1624 
1625 	/* releases */
1626 	if (!as_app_validate_releases (app, helper, error))
1627 		return NULL;
1628 
1629 	/* kudos */
1630 	if (!as_app_validate_kudos (app, helper, error))
1631 		return NULL;
1632 
1633 	/* name */
1634 	name = as_app_get_name (app, "C");
1635 	if (name != NULL) {
1636 		str_len = (guint) strlen (name);
1637 		if (str_len < length_name_min) {
1638 			ai_app_validate_add (helper,
1639 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1640 					     "<name> is too short [%s] minimum is %u chars",
1641 					     name, length_name_min);
1642 		}
1643 		if (str_len > length_name_max) {
1644 			ai_app_validate_add (helper,
1645 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1646 					     "<name> is too long [%s] maximum is %u chars",
1647 					     name, length_name_max);
1648 		}
1649 		if (ai_app_validate_fullstop_ending (name)) {
1650 			ai_app_validate_add (helper,
1651 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1652 					     "<name> cannot end in '.' [%s]",
1653 					     name);
1654 		}
1655 		if (as_app_validate_has_hyperlink (name)) {
1656 			ai_app_validate_add (helper,
1657 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1658 					     "<name> cannot contain a hyperlink [%s]",
1659 					     name);
1660 		}
1661 		if (require_sentence_case &&
1662 		    !as_app_validate_has_first_word_capital (helper, name)) {
1663 			ai_app_validate_add (helper,
1664 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1665 					     "<name> requires sentence case [%s]",
1666 					     name);
1667 		}
1668 	} else if (require_name) {
1669 		ai_app_validate_add (helper,
1670 				     AS_PROBLEM_KIND_TAG_MISSING,
1671 				     "<name> is not present");
1672 	}
1673 
1674 	/* comment */
1675 	summary = as_app_get_comment (app, "C");
1676 	if (summary != NULL) {
1677 		str_len = (guint) strlen (summary);
1678 		if (str_len < length_summary_min) {
1679 			ai_app_validate_add (helper,
1680 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1681 					     "<summary> is too short [%s] minimum is %u chars",
1682 					     summary, length_summary_min);
1683 		}
1684 		if (str_len > length_summary_max) {
1685 			ai_app_validate_add (helper,
1686 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1687 					     "<summary> is too long [%s] maximum is %u chars",
1688 					     summary, length_summary_max);
1689 		}
1690 		if (require_sentence_case &&
1691 		    ai_app_validate_fullstop_ending (summary)) {
1692 			ai_app_validate_add (helper,
1693 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1694 					     "<summary> cannot end in '.' [%s]",
1695 					     summary);
1696 		}
1697 		if (as_app_validate_has_hyperlink (summary)) {
1698 			ai_app_validate_add (helper,
1699 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1700 					     "<summary> cannot contain a hyperlink [%s]",
1701 					     summary);
1702 		}
1703 		if (require_sentence_case &&
1704 		    !as_app_validate_has_first_word_capital (helper, summary)) {
1705 			ai_app_validate_add (helper,
1706 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1707 					     "<summary> requires sentence case [%s]",
1708 					     summary);
1709 		}
1710 	} else if (require_name) {
1711 		ai_app_validate_add (helper,
1712 				     AS_PROBLEM_KIND_TAG_MISSING,
1713 				     "<summary> is not present");
1714 	}
1715 	if (require_name_shorter_than_summary &&
1716 	    summary != NULL && name != NULL &&
1717 	    strlen (summary) < strlen (name)) {
1718 		ai_app_validate_add (helper,
1719 				     AS_PROBLEM_KIND_STYLE_INCORRECT,
1720 				     "<summary> is shorter than <name>");
1721 	}
1722 	description = as_app_get_description (app, "C");
1723 	if (description == NULL) {
1724 		if (require_description) {
1725 			ai_app_validate_add (helper,
1726 					     AS_PROBLEM_KIND_TAG_MISSING,
1727 					     "<description> required");
1728 		}
1729 	} else {
1730 		ret = as_app_validate_description (description,
1731 						   helper,
1732 						   number_para_min,
1733 						   number_para_max,
1734 						   FALSE,
1735 						   &error_local);
1736 		if (!ret) {
1737 			ai_app_validate_add (helper,
1738 					     AS_PROBLEM_KIND_MARKUP_INVALID,
1739 					     "%s", error_local->message);
1740 			g_error_free (error_local);
1741 		}
1742 	}
1743 	if (require_translations) {
1744 		if (name != NULL &&
1745 		    as_app_get_name_size (app) == 1 &&
1746 		    (problems & AS_APP_PROBLEM_INTLTOOL_NAME) == 0) {
1747 			ai_app_validate_add (helper,
1748 					     AS_PROBLEM_KIND_TRANSLATIONS_REQUIRED,
1749 					     "<name> has no translations");
1750 		}
1751 		if (summary != NULL &&
1752 		    as_app_get_comment_size (app) == 1 &&
1753 		    (problems & AS_APP_PROBLEM_INTLTOOL_SUMMARY) == 0) {
1754 			ai_app_validate_add (helper,
1755 					     AS_PROBLEM_KIND_TRANSLATIONS_REQUIRED,
1756 					     "<summary> has no translations");
1757 		}
1758 		if (description != NULL &&
1759 		    as_app_get_description_size (app) == 1 &&
1760 		    (problems & AS_APP_PROBLEM_INTLTOOL_DESCRIPTION) == 0) {
1761 			ai_app_validate_add (helper,
1762 					     AS_PROBLEM_KIND_TRANSLATIONS_REQUIRED,
1763 					     "<description> has no translations");
1764 		}
1765 	}
1766 
1767 	/* developer_name */
1768 	name = as_app_get_developer_name (app, NULL);
1769 	if (name != NULL) {
1770 		str_len = (guint) strlen (name);
1771 		if (str_len < length_name_min) {
1772 			ai_app_validate_add (helper,
1773 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1774 					     "<developer_name> is too short [%s] minimum is %u chars",
1775 					     name, length_name_min);
1776 		}
1777 		if (str_len > length_name_max) {
1778 			ai_app_validate_add (helper,
1779 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1780 					     "<developer_name> is too long [%s] maximum is %u chars",
1781 					     name, length_name_max);
1782 		}
1783 		if (as_app_validate_has_hyperlink (name)) {
1784 			ai_app_validate_add (helper,
1785 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1786 					     "<developer_name> cannot contain a hyperlink [%s]",
1787 					     name);
1788 		}
1789 		if (as_app_validate_has_email (name)) {
1790 			ai_app_validate_add (helper,
1791 					     AS_PROBLEM_KIND_STYLE_INCORRECT,
1792 					     "<developer_name> cannot contain an email address [%s]",
1793 					     name);
1794 		}
1795 	}
1796 
1797 	/* using deprecated names */
1798 	if (deprecated_failure && (problems & AS_APP_PROBLEM_DEPRECATED_LICENCE) > 0) {
1799 		ai_app_validate_add (helper,
1800 				     AS_PROBLEM_KIND_ATTRIBUTE_INVALID,
1801 				     "<licence> is deprecated, use "
1802 				     "<metadata_license> instead");
1803 	}
1804 	if ((problems & AS_APP_PROBLEM_MULTIPLE_ENTRIES) > 0) {
1805 		ai_app_validate_add (helper,
1806 				     AS_PROBLEM_KIND_MARKUP_INVALID,
1807 				     "<application> used more than once");
1808 	}
1809 
1810 	/* require homepage */
1811 	if (require_url && as_app_get_url_item (app, AS_URL_KIND_HOMEPAGE) == NULL) {
1812 		switch (as_format_get_kind (format)) {
1813 		case AS_FORMAT_KIND_APPDATA:
1814 		case AS_FORMAT_KIND_METAINFO:
1815 			ai_app_validate_add (helper,
1816 					     AS_PROBLEM_KIND_TAG_MISSING,
1817 					     "<url> is not present");
1818 			break;
1819 		default:
1820 			break;
1821 		}
1822 	}
1823 	return helper->probs;
1824 }
1825