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