1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2  *
3  * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
4  *
5  * SPDX-License-Identifier: LGPL-2.1+
6  */
7 
8 /**
9  * SECTION:as-content-rating
10  * @short_description: Object representing a content rating
11  * @include: appstream-glib.h
12  * @stability: Unstable
13  *
14  * Content ratings are age-specific guidelines for applications.
15  *
16  * See also: #AsApp
17  */
18 
19 #include "config.h"
20 
21 #include <glib/gi18n-lib.h>
22 
23 #include "as-node-private.h"
24 #include "as-content-rating-private.h"
25 #include "as-ref-string.h"
26 #include "as-tag.h"
27 
28 typedef struct {
29 	AsRefString		*id;
30 	AsContentRatingValue	 value;
31 } AsContentRatingKey;
32 
33 typedef struct
34 {
35 	AsRefString		*kind;
36 	GPtrArray		*keys; /* of AsContentRatingKey */
37 } AsContentRatingPrivate;
38 
39 G_DEFINE_TYPE_WITH_PRIVATE (AsContentRating, as_content_rating, G_TYPE_OBJECT)
40 
41 #define GET_PRIVATE(o) (as_content_rating_get_instance_private (o))
42 
43 typedef enum
44 {
45 	OARS_1_0,
46 	OARS_1_1,
47 } OarsVersion;
48 
49 static gboolean is_oars_key (const gchar *id, OarsVersion version);
50 
51 static void
as_content_rating_finalize(GObject * object)52 as_content_rating_finalize (GObject *object)
53 {
54 	AsContentRating *content_rating = AS_CONTENT_RATING (object);
55 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
56 
57 	if (priv->kind != NULL)
58 		as_ref_string_unref (priv->kind);
59 	g_ptr_array_unref (priv->keys);
60 
61 	G_OBJECT_CLASS (as_content_rating_parent_class)->finalize (object);
62 }
63 
64 static void
as_content_rating_key_free(AsContentRatingKey * key)65 as_content_rating_key_free (AsContentRatingKey *key)
66 {
67 	if (key->id != NULL)
68 		as_ref_string_unref (key->id);
69 	g_slice_free (AsContentRatingKey, key);
70 }
71 
72 static void
as_content_rating_init(AsContentRating * content_rating)73 as_content_rating_init (AsContentRating *content_rating)
74 {
75 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
76 	priv->keys = g_ptr_array_new_with_free_func ((GDestroyNotify) as_content_rating_key_free);
77 }
78 
79 static void
as_content_rating_class_init(AsContentRatingClass * klass)80 as_content_rating_class_init (AsContentRatingClass *klass)
81 {
82 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
83 	object_class->finalize = as_content_rating_finalize;
84 }
85 
86 static gint
ids_sort_cb(gconstpointer id_ptr_a,gconstpointer id_ptr_b)87 ids_sort_cb (gconstpointer id_ptr_a, gconstpointer id_ptr_b)
88 {
89 	const gchar *id_a = *((const gchar **) id_ptr_a);
90 	const gchar *id_b = *((const gchar **) id_ptr_b);
91 
92 	return g_strcmp0 (id_a, id_b);
93 }
94 
95 /**
96  * as_content_rating_get_rating_ids:
97  * @content_rating: a #AsContentRating
98  *
99  * Gets the set of ratings IDs which are present in this @content_rating. An
100  * example of a ratings ID is `violence-bloodshed`.
101  *
102  * The IDs are returned in lexicographical order.
103  *
104  * Returns: (array zero-terminated=1) (transfer container): %NULL-terminated
105  *    array of ratings IDs; each ratings ID is owned by the #AsContentRating and
106  *    must not be freed, but the container must be freed with g_free()
107  *
108  * Since: 0.7.15
109  **/
110 const gchar **
as_content_rating_get_rating_ids(AsContentRating * content_rating)111 as_content_rating_get_rating_ids (AsContentRating *content_rating)
112 {
113 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
114 	GPtrArray *ids = g_ptr_array_new_with_free_func (NULL);
115 	guint i;
116 
117 	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), NULL);
118 
119 	for (i = 0; i < priv->keys->len; i++) {
120 		AsContentRatingKey *key = g_ptr_array_index (priv->keys, i);
121 		g_ptr_array_add (ids, key->id);
122 	}
123 
124 	g_ptr_array_sort (ids, ids_sort_cb);
125 	g_ptr_array_add (ids, NULL);  /* NULL terminator */
126 
127 	return (const gchar **) g_ptr_array_free (g_steal_pointer (&ids), FALSE);
128 }
129 
130 /**
131  * as_content_rating_get_value:
132  * @content_rating: a #AsContentRating
133  * @id: A ratings ID, e.g. `violence-bloodshed`.
134  *
135  * Gets the set value of a content rating key.
136  *
137  * Returns: the #AsContentRatingValue, or %AS_CONTENT_RATING_VALUE_UNKNOWN
138  *
139  * Since: 0.6.4
140  **/
141 AsContentRatingValue
as_content_rating_get_value(AsContentRating * content_rating,const gchar * id)142 as_content_rating_get_value (AsContentRating *content_rating, const gchar *id)
143 {
144 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
145 	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), AS_CONTENT_RATING_VALUE_UNKNOWN);
146 	guint i;
147 	for (i = 0; i < priv->keys->len; i++) {
148 		AsContentRatingKey *key = g_ptr_array_index (priv->keys, i);
149 		if (g_strcmp0 (key->id, id) == 0)
150 			return key->value;
151 	}
152 
153 	/* According to the
154 	 * [OARS specification](https://github.com/hughsie/oars/blob/master/specification/oars-1.1.md),
155 	 * return %AS_CONTENT_RATING_VALUE_NONE if the #AsContentRating exists
156 	 * overall. Only return %AS_CONTENT_RATING_VALUE_UNKNOWN if the
157 	 * #AsContentRating doesn’t exist at all (or for other types of content
158 	 * rating). */
159 	if ((g_strcmp0 (priv->kind, "oars-1.0") == 0 && is_oars_key (id, OARS_1_0)) ||
160 	    (g_strcmp0 (priv->kind, "oars-1.1") == 0 && is_oars_key (id, OARS_1_1)))
161 		return AS_CONTENT_RATING_VALUE_NONE;
162 	else
163 		return AS_CONTENT_RATING_VALUE_UNKNOWN;
164 }
165 
166 /**
167  * as_content_rating_value_to_string:
168  * @value: the #AsContentRatingValue.
169  *
170  * Converts the enumerated value to an text representation.
171  *
172  * Returns: string version of @value
173  *
174  * Since: 0.5.12
175  **/
176 const gchar *
as_content_rating_value_to_string(AsContentRatingValue value)177 as_content_rating_value_to_string (AsContentRatingValue value)
178 {
179 	if (value == AS_CONTENT_RATING_VALUE_NONE)
180 		return "none";
181 	if (value == AS_CONTENT_RATING_VALUE_MILD)
182 		return "mild";
183 	if (value == AS_CONTENT_RATING_VALUE_MODERATE)
184 		return "moderate";
185 	if (value == AS_CONTENT_RATING_VALUE_INTENSE)
186 		return "intense";
187 	return "unknown";
188 }
189 
190 /**
191  * as_content_rating_value_from_string:
192  * @value: the string.
193  *
194  * Converts the text representation to an enumerated value.
195  *
196  * Returns: a #AsContentRatingValue or %AS_CONTENT_RATING_VALUE_UNKNOWN for unknown
197  *
198  * Since: 0.5.12
199  **/
200 AsContentRatingValue
as_content_rating_value_from_string(const gchar * value)201 as_content_rating_value_from_string (const gchar *value)
202 {
203 	if (g_strcmp0 (value, "none") == 0)
204 		return AS_CONTENT_RATING_VALUE_NONE;
205 	if (g_strcmp0 (value, "mild") == 0)
206 		return AS_CONTENT_RATING_VALUE_MILD;
207 	if (g_strcmp0 (value, "moderate") == 0)
208 		return AS_CONTENT_RATING_VALUE_MODERATE;
209 	if (g_strcmp0 (value, "intense") == 0)
210 		return AS_CONTENT_RATING_VALUE_INTENSE;
211 	return AS_CONTENT_RATING_VALUE_UNKNOWN;
212 }
213 
214 static const gchar *rating_system_names[] = {
215 	[AS_CONTENT_RATING_SYSTEM_UNKNOWN] = NULL,
216 	[AS_CONTENT_RATING_SYSTEM_INCAA] = "INCAA",
217 	[AS_CONTENT_RATING_SYSTEM_ACB] = "ACB",
218 	[AS_CONTENT_RATING_SYSTEM_DJCTQ] = "DJCTQ",
219 	[AS_CONTENT_RATING_SYSTEM_GSRR] = "GSRR",
220 	[AS_CONTENT_RATING_SYSTEM_PEGI] = "PEGI",
221 	[AS_CONTENT_RATING_SYSTEM_KAVI] = "KAVI",
222 	[AS_CONTENT_RATING_SYSTEM_USK] = "USK",
223 	[AS_CONTENT_RATING_SYSTEM_ESRA] = "ESRA",
224 	[AS_CONTENT_RATING_SYSTEM_CERO] = "CERO",
225 	[AS_CONTENT_RATING_SYSTEM_OFLCNZ] = "OFLCNZ",
226 	[AS_CONTENT_RATING_SYSTEM_RUSSIA] = "RUSSIA",
227 	[AS_CONTENT_RATING_SYSTEM_MDA] = "MDA",
228 	[AS_CONTENT_RATING_SYSTEM_GRAC] = "GRAC",
229 	[AS_CONTENT_RATING_SYSTEM_ESRB] = "ESRB",
230 	[AS_CONTENT_RATING_SYSTEM_IARC] = "IARC",
231 };
232 G_STATIC_ASSERT (G_N_ELEMENTS (rating_system_names) == AS_CONTENT_RATING_SYSTEM_LAST);
233 
234 /**
235  * as_content_rating_system_to_string:
236  * @system: an #AsContentRatingSystem
237  *
238  * Get a human-readable string to identify @system. %NULL will be returned for
239  * %AS_CONTENT_RATING_SYSTEM_UNKNOWN.
240  *
241  * Returns: (nullable): a human-readable string for @system, or %NULL if unknown
242  * Since: 0.7.18
243  */
244 const gchar *
as_content_rating_system_to_string(AsContentRatingSystem system)245 as_content_rating_system_to_string (AsContentRatingSystem system)
246 {
247 	if ((gint) system < AS_CONTENT_RATING_SYSTEM_UNKNOWN ||
248 	    (gint) system >= AS_CONTENT_RATING_SYSTEM_LAST)
249 		return NULL;
250 
251 	return rating_system_names[system];
252 }
253 
254 static char *
get_esrb_string(const gchar * source,const gchar * translate)255 get_esrb_string (const gchar *source, const gchar *translate)
256 {
257 	if (g_strcmp0 (source, translate) == 0)
258 		return g_strdup (source);
259 	/* TRANSLATORS: This is the formatting of English and localized name
260 	 * of the rating e.g. "Adults Only (solo adultos)" */
261 	return g_strdup_printf (_("%s (%s)"), source, translate);
262 }
263 
264 /**
265  * as_content_rating_system_format_age:
266  * @system: an #AsContentRatingSystem
267  * @age: a CSM age to format
268  *
269  * Format @age as a human-readable string in the given rating @system. This is
270  * the way to present system-specific strings in a UI.
271  *
272  * Returns: (transfer full) (nullable): a newly allocated formatted version of
273  *    @age, or %NULL if the given @system has no representation for @age
274  * Since: 0.7.18
275  */
276 /* data obtained from https://en.wikipedia.org/wiki/Video_game_rating_system */
277 gchar *
as_content_rating_system_format_age(AsContentRatingSystem system,guint age)278 as_content_rating_system_format_age (AsContentRatingSystem system, guint age)
279 {
280 	if (system == AS_CONTENT_RATING_SYSTEM_INCAA) {
281 		if (age >= 18)
282 			return g_strdup ("+18");
283 		if (age >= 13)
284 			return g_strdup ("+13");
285 		return g_strdup ("ATP");
286 	}
287 	if (system == AS_CONTENT_RATING_SYSTEM_ACB) {
288 		if (age >= 18)
289 			return g_strdup ("R18+");
290 		if (age >= 15)
291 			return g_strdup ("MA15+");
292 		return g_strdup ("PG");
293 	}
294 	if (system == AS_CONTENT_RATING_SYSTEM_DJCTQ) {
295 		if (age >= 18)
296 			return g_strdup ("18");
297 		if (age >= 16)
298 			return g_strdup ("16");
299 		if (age >= 14)
300 			return g_strdup ("14");
301 		if (age >= 12)
302 			return g_strdup ("12");
303 		if (age >= 10)
304 			return g_strdup ("10");
305 		return g_strdup ("L");
306 	}
307 	if (system == AS_CONTENT_RATING_SYSTEM_GSRR) {
308 		if (age >= 18)
309 			return g_strdup ("限制");
310 		if (age >= 15)
311 			return g_strdup ("輔15");
312 		if (age >= 12)
313 			return g_strdup ("輔12");
314 		if (age >= 6)
315 			return g_strdup ("保護");
316 		return g_strdup ("普通");
317 	}
318 	if (system == AS_CONTENT_RATING_SYSTEM_PEGI) {
319 		if (age >= 18)
320 			return g_strdup ("18");
321 		if (age >= 16)
322 			return g_strdup ("16");
323 		if (age >= 12)
324 			return g_strdup ("12");
325 		if (age >= 7)
326 			return g_strdup ("7");
327 		if (age >= 3)
328 			return g_strdup ("3");
329 		return NULL;
330 	}
331 	if (system == AS_CONTENT_RATING_SYSTEM_KAVI) {
332 		if (age >= 18)
333 			return g_strdup ("18+");
334 		if (age >= 16)
335 			return g_strdup ("16+");
336 		if (age >= 12)
337 			return g_strdup ("12+");
338 		if (age >= 7)
339 			return g_strdup ("7+");
340 		if (age >= 3)
341 			return g_strdup ("3+");
342 		return NULL;
343 	}
344 	if (system == AS_CONTENT_RATING_SYSTEM_USK) {
345 		if (age >= 18)
346 			return g_strdup ("18");
347 		if (age >= 16)
348 			return g_strdup ("16");
349 		if (age >= 12)
350 			return g_strdup ("12");
351 		if (age >= 6)
352 			return g_strdup ("6");
353 		return g_strdup ("0");
354 	}
355 	/* Reference: http://www.esra.org.ir/ */
356 	if (system == AS_CONTENT_RATING_SYSTEM_ESRA) {
357 		if (age >= 18)
358 			return g_strdup ("+18");
359 		if (age >= 15)
360 			return g_strdup ("+15");
361 		if (age >= 12)
362 			return g_strdup ("+12");
363 		if (age >= 7)
364 			return g_strdup ("+7");
365 		if (age >= 3)
366 			return g_strdup ("+3");
367 		return NULL;
368 	}
369 	if (system == AS_CONTENT_RATING_SYSTEM_CERO) {
370 		if (age >= 18)
371 			return g_strdup ("Z");
372 		if (age >= 17)
373 			return g_strdup ("D");
374 		if (age >= 15)
375 			return g_strdup ("C");
376 		if (age >= 12)
377 			return g_strdup ("B");
378 		return g_strdup ("A");
379 	}
380 	if (system == AS_CONTENT_RATING_SYSTEM_OFLCNZ) {
381 		if (age >= 18)
382 			return g_strdup ("R18");
383 		if (age >= 16)
384 			return g_strdup ("R16");
385 		if (age >= 15)
386 			return g_strdup ("R15");
387 		if (age >= 13)
388 			return g_strdup ("R13");
389 		return g_strdup ("G");
390 	}
391 	if (system == AS_CONTENT_RATING_SYSTEM_RUSSIA) {
392 		if (age >= 18)
393 			return g_strdup ("18+");
394 		if (age >= 16)
395 			return g_strdup ("16+");
396 		if (age >= 12)
397 			return g_strdup ("12+");
398 		if (age >= 6)
399 			return g_strdup ("6+");
400 		return g_strdup ("0+");
401 	}
402 	if (system == AS_CONTENT_RATING_SYSTEM_MDA) {
403 		if (age >= 18)
404 			return g_strdup ("M18");
405 		if (age >= 16)
406 			return g_strdup ("ADV");
407 		return get_esrb_string ("General", _("General"));
408 	}
409 	if (system == AS_CONTENT_RATING_SYSTEM_GRAC) {
410 		if (age >= 18)
411 			return g_strdup ("18");
412 		if (age >= 15)
413 			return g_strdup ("15");
414 		if (age >= 12)
415 			return g_strdup ("12");
416 		return get_esrb_string ("ALL", _("ALL"));
417 	}
418 	if (system == AS_CONTENT_RATING_SYSTEM_ESRB) {
419 		if (age >= 18)
420 			return get_esrb_string ("Adults Only", _("Adults Only"));
421 		if (age >= 17)
422 			return get_esrb_string ("Mature", _("Mature"));
423 		if (age >= 13)
424 			return get_esrb_string ("Teen", _("Teen"));
425 		if (age >= 10)
426 			return get_esrb_string ("Everyone 10+", _("Everyone 10+"));
427 		if (age >= 6)
428 			return get_esrb_string ("Everyone", _("Everyone"));
429 
430 		return get_esrb_string ("Early Childhood", _("Early Childhood"));
431 	}
432 	/* IARC = everything else */
433 	if (age >= 18)
434 		return g_strdup ("18+");
435 	if (age >= 16)
436 		return g_strdup ("16+");
437 	if (age >= 12)
438 		return g_strdup ("12+");
439 	if (age >= 7)
440 		return g_strdup ("7+");
441 	if (age >= 3)
442 		return g_strdup ("3+");
443 	return NULL;
444 }
445 
446 static const gchar *content_rating_strings[AS_CONTENT_RATING_SYSTEM_LAST][7] = {
447 	/* AS_CONTENT_RATING_SYSTEM_UNKNOWN is handled in code */
448 	[AS_CONTENT_RATING_SYSTEM_INCAA] = { "ATP", "+13", "+18", NULL },
449 	[AS_CONTENT_RATING_SYSTEM_ACB] = { "PG", "MA15+", "R18+", NULL },
450 	[AS_CONTENT_RATING_SYSTEM_DJCTQ] = { "L", "10", "12", "14", "16", "18", NULL },
451 	[AS_CONTENT_RATING_SYSTEM_GSRR] = { "普通", "保護", "輔12", "輔15", "限制", NULL },
452 	[AS_CONTENT_RATING_SYSTEM_PEGI] = { "3", "7", "12", "16", "18", NULL },
453 	[AS_CONTENT_RATING_SYSTEM_KAVI] = { "3+", "7+", "12+", "16+", "18+", NULL },
454 	[AS_CONTENT_RATING_SYSTEM_USK] = { "0", "6", "12", "16", "18", NULL },
455 	[AS_CONTENT_RATING_SYSTEM_ESRA] = { "+3", "+7", "+12", "+15", "+18", NULL },
456 	[AS_CONTENT_RATING_SYSTEM_CERO] = { "A", "B", "C", "D", "Z", NULL },
457 	[AS_CONTENT_RATING_SYSTEM_OFLCNZ] = { "G", "R13", "R15", "R16", "R18", NULL },
458 	[AS_CONTENT_RATING_SYSTEM_RUSSIA] = { "0+", "6+", "12+", "16+", "18+", NULL },
459 	[AS_CONTENT_RATING_SYSTEM_MDA] = { "General", "ADV", "M18", NULL },
460 	[AS_CONTENT_RATING_SYSTEM_GRAC] = { "ALL", "12", "15", "18", NULL },
461 	/* Note: ESRB has locale-specific suffixes, so needs special further
462 	 * handling in code. These strings are just the locale-independent parts. */
463 	[AS_CONTENT_RATING_SYSTEM_ESRB] = { "Early Childhood", "Everyone", "Everyone 10+", "Teen", "Mature", "Adults Only", NULL },
464 	[AS_CONTENT_RATING_SYSTEM_IARC] = { "3+", "7+", "12+", "16+", "18+", NULL },
465 };
466 
467 /**
468  * as_content_rating_system_get_formatted_ages:
469  * @system: an #AsContentRatingSystem
470  *
471  * Get an array of all the possible return values of
472  * as_content_rating_system_format_age() for the given @system. The array is
473  * sorted with youngest CSM age first.
474  *
475  * Returns: (transfer full): %NULL-terminated array of human-readable age strings
476  * Since: 0.7.18
477  */
478 gchar **
as_content_rating_system_get_formatted_ages(AsContentRatingSystem system)479 as_content_rating_system_get_formatted_ages (AsContentRatingSystem system)
480 {
481 	g_return_val_if_fail ((int) system < AS_CONTENT_RATING_SYSTEM_LAST, NULL);
482 
483 	/* IARC is the fallback for everything */
484 	if (system == AS_CONTENT_RATING_SYSTEM_UNKNOWN)
485 		system = AS_CONTENT_RATING_SYSTEM_IARC;
486 
487 	/* ESRB is special as it requires localised suffixes */
488 	if (system == AS_CONTENT_RATING_SYSTEM_ESRB) {
489 		g_auto(GStrv) esrb_ages = g_new0 (gchar *, 7);
490 
491 		esrb_ages[0] = get_esrb_string (content_rating_strings[system][0], _("Early Childhood"));
492 		esrb_ages[1] = get_esrb_string (content_rating_strings[system][1], _("Everyone"));
493 		esrb_ages[2] = get_esrb_string (content_rating_strings[system][2], _("Everyone 10+"));
494 		esrb_ages[3] = get_esrb_string (content_rating_strings[system][3], _("Teen"));
495 		esrb_ages[4] = get_esrb_string (content_rating_strings[system][4], _("Mature"));
496 		esrb_ages[5] = get_esrb_string (content_rating_strings[system][5], _("Adults Only"));
497 		esrb_ages[6] = NULL;
498 
499 		return g_steal_pointer (&esrb_ages);
500 	}
501 
502 	return g_strdupv ((gchar **) content_rating_strings[system]);
503 }
504 
505 static guint content_rating_csm_ages[AS_CONTENT_RATING_SYSTEM_LAST][7] = {
506 	/* AS_CONTENT_RATING_SYSTEM_UNKNOWN is handled in code */
507 	[AS_CONTENT_RATING_SYSTEM_INCAA] = { 0, 13, 18 },
508 	[AS_CONTENT_RATING_SYSTEM_ACB] = { 0, 15, 18 },
509 	[AS_CONTENT_RATING_SYSTEM_DJCTQ] = { 0, 10, 12, 14, 16, 18 },
510 	[AS_CONTENT_RATING_SYSTEM_GSRR] = { 0, 6, 12, 15, 18 },
511 	[AS_CONTENT_RATING_SYSTEM_PEGI] = { 3, 7, 12, 16, 18 },
512 	[AS_CONTENT_RATING_SYSTEM_KAVI] = { 3, 7, 12, 16, 18 },
513 	[AS_CONTENT_RATING_SYSTEM_USK] = { 0, 6, 12, 16, 18 },
514 	[AS_CONTENT_RATING_SYSTEM_ESRA] = { 3, 7, 12, 15, 18 },
515 	[AS_CONTENT_RATING_SYSTEM_CERO] = { 0, 12, 15, 17, 18 },
516 	[AS_CONTENT_RATING_SYSTEM_OFLCNZ] = { 0, 13, 15, 16, 18 },
517 	[AS_CONTENT_RATING_SYSTEM_RUSSIA] = { 0, 6, 12, 16, 18 },
518 	[AS_CONTENT_RATING_SYSTEM_MDA] = { 0, 16, 18 },
519 	[AS_CONTENT_RATING_SYSTEM_GRAC] = { 0, 12, 15, 18 },
520 	[AS_CONTENT_RATING_SYSTEM_ESRB] = { 0, 6, 10, 13, 17, 18 },
521 	[AS_CONTENT_RATING_SYSTEM_IARC] = { 3, 7, 12, 16, 18 },
522 };
523 
524 /**
525  * as_content_rating_system_get_csm_ages:
526  * @system: an #AsContentRatingSystem
527  * @length_out: (out) (not optional): return location for the length of the
528  *    returned array
529  *
530  * Get the CSM ages corresponding to the entries returned by
531  * as_content_rating_system_get_formatted_ages() for this @system.
532  *
533  * Returns: (transfer container) (array length=length_out): an array of CSM ages
534  * Since: 0.7.18
535  */
536 const guint *
as_content_rating_system_get_csm_ages(AsContentRatingSystem system,gsize * length_out)537 as_content_rating_system_get_csm_ages (AsContentRatingSystem system, gsize *length_out)
538 {
539 	g_return_val_if_fail ((int) system < AS_CONTENT_RATING_SYSTEM_LAST, NULL);
540 	g_return_val_if_fail (length_out != NULL, NULL);
541 
542 	/* IARC is the fallback for everything */
543 	if (system == AS_CONTENT_RATING_SYSTEM_UNKNOWN)
544 		system = AS_CONTENT_RATING_SYSTEM_IARC;
545 
546 	*length_out = g_strv_length ((gchar **) content_rating_strings[system]);
547 	return content_rating_csm_ages[system];
548 }
549 
550 /*
551  * parse_locale:
552  * @locale: (transfer full): a locale to parse
553  * @language_out: (out) (optional) (nullable): return location for the parsed
554  *    language, or %NULL to ignore
555  * @territory_out: (out) (optional) (nullable): return location for the parsed
556  *    territory, or %NULL to ignore
557  * @codeset_out: (out) (optional) (nullable): return location for the parsed
558  *    codeset, or %NULL to ignore
559  * @modifier_out: (out) (optional) (nullable): return location for the parsed
560  *    modifier, or %NULL to ignore
561  *
562  * Parse @locale as a locale string of the form
563  * `language[_territory][.codeset][@modifier]` — see `man 3 setlocale` for
564  * details.
565  *
566  * On success, %TRUE will be returned, and the components of the locale will be
567  * returned in the given addresses, with each component not including any
568  * separators. Otherwise, %FALSE will be returned and the components will be set
569  * to %NULL.
570  *
571  * @locale is modified, and any returned non-%NULL pointers will point inside
572  * it.
573  *
574  * Returns: %TRUE on success, %FALSE otherwise
575  */
576 static gboolean
parse_locale(gchar * locale,const gchar ** language_out,const gchar ** territory_out,const gchar ** codeset_out,const gchar ** modifier_out)577 parse_locale (gchar *locale  /* (transfer full) */,
578 	      const gchar **language_out,
579 	      const gchar **territory_out,
580 	      const gchar **codeset_out,
581 	      const gchar **modifier_out)
582 {
583 	gchar *separator;
584 	const gchar *language = NULL, *territory = NULL, *codeset = NULL, *modifier = NULL;
585 
586 	separator = strrchr (locale, '@');
587 	if (separator != NULL) {
588 		modifier = separator + 1;
589 		*separator = '\0';
590 	}
591 
592 	separator = strrchr (locale, '.');
593 	if (separator != NULL) {
594 		codeset = separator + 1;
595 		*separator = '\0';
596 	}
597 
598 	separator = strrchr (locale, '_');
599 	if (separator != NULL) {
600 		territory = separator + 1;
601 		*separator = '\0';
602 	}
603 
604 	language = locale;
605 
606 	/* Parse failure? */
607 	if (*language == '\0') {
608 		language = NULL;
609 		territory = NULL;
610 		codeset = NULL;
611 		modifier = NULL;
612 	}
613 
614 	if (language_out != NULL)
615 		*language_out = language;
616 	if (territory_out != NULL)
617 		*territory_out = territory;
618 	if (codeset_out != NULL)
619 		*codeset_out = codeset;
620 	if (modifier_out != NULL)
621 		*modifier_out = modifier;
622 
623 	return (language != NULL);
624 }
625 
626 /**
627  * as_content_rating_system_from_locale:
628  * @locale: a locale, in the format described in `man 3 setlocale`
629  *
630  * Determine the most appropriate #AsContentRatingSystem for the given @locale.
631  * Content rating systems are selected by territory. If no content rating system
632  * seems suitable, %AS_CONTENT_RATING_SYSTEM_IARC is returned.
633  *
634  * Returns: the most relevant #AsContentRatingSystem
635  * Since: 0.7.18
636  */
637 /* data obtained from https://en.wikipedia.org/wiki/Video_game_rating_system */
638 AsContentRatingSystem
as_content_rating_system_from_locale(const gchar * locale)639 as_content_rating_system_from_locale (const gchar *locale)
640 {
641 	g_autofree gchar *locale_copy = g_strdup (locale);
642 	const gchar *territory;
643 
644 	/* Default to IARC for locales which can’t be parsed. */
645 	if (!parse_locale (locale_copy, NULL, &territory, NULL, NULL))
646 		return AS_CONTENT_RATING_SYSTEM_IARC;
647 
648 	/* Argentina */
649 	if (g_strcmp0 (territory, "AR") == 0)
650 		return AS_CONTENT_RATING_SYSTEM_INCAA;
651 
652 	/* Australia */
653 	if (g_strcmp0 (territory, "AU") == 0)
654 		return AS_CONTENT_RATING_SYSTEM_ACB;
655 
656 	/* Brazil */
657 	if (g_strcmp0 (territory, "BR") == 0)
658 		return AS_CONTENT_RATING_SYSTEM_DJCTQ;
659 
660 	/* Taiwan */
661 	if (g_strcmp0 (territory, "TW") == 0)
662 		return AS_CONTENT_RATING_SYSTEM_GSRR;
663 
664 	/* Europe (but not Finland or Germany), India, Israel,
665 	 * Pakistan, Quebec, South Africa */
666 	if ((g_strcmp0 (territory, "GB") == 0) ||
667 	    g_strcmp0 (territory, "AL") == 0 ||
668 	    g_strcmp0 (territory, "AD") == 0 ||
669 	    g_strcmp0 (territory, "AM") == 0 ||
670 	    g_strcmp0 (territory, "AT") == 0 ||
671 	    g_strcmp0 (territory, "AZ") == 0 ||
672 	    g_strcmp0 (territory, "BY") == 0 ||
673 	    g_strcmp0 (territory, "BE") == 0 ||
674 	    g_strcmp0 (territory, "BA") == 0 ||
675 	    g_strcmp0 (territory, "BG") == 0 ||
676 	    g_strcmp0 (territory, "HR") == 0 ||
677 	    g_strcmp0 (territory, "CY") == 0 ||
678 	    g_strcmp0 (territory, "CZ") == 0 ||
679 	    g_strcmp0 (territory, "DK") == 0 ||
680 	    g_strcmp0 (territory, "EE") == 0 ||
681 	    g_strcmp0 (territory, "FR") == 0 ||
682 	    g_strcmp0 (territory, "GE") == 0 ||
683 	    g_strcmp0 (territory, "GR") == 0 ||
684 	    g_strcmp0 (territory, "HU") == 0 ||
685 	    g_strcmp0 (territory, "IS") == 0 ||
686 	    g_strcmp0 (territory, "IT") == 0 ||
687 	    g_strcmp0 (territory, "LZ") == 0 ||
688 	    g_strcmp0 (territory, "XK") == 0 ||
689 	    g_strcmp0 (territory, "LV") == 0 ||
690 	    g_strcmp0 (territory, "FL") == 0 ||
691 	    g_strcmp0 (territory, "LU") == 0 ||
692 	    g_strcmp0 (territory, "LT") == 0 ||
693 	    g_strcmp0 (territory, "MK") == 0 ||
694 	    g_strcmp0 (territory, "MT") == 0 ||
695 	    g_strcmp0 (territory, "MD") == 0 ||
696 	    g_strcmp0 (territory, "MC") == 0 ||
697 	    g_strcmp0 (territory, "ME") == 0 ||
698 	    g_strcmp0 (territory, "NL") == 0 ||
699 	    g_strcmp0 (territory, "NO") == 0 ||
700 	    g_strcmp0 (territory, "PL") == 0 ||
701 	    g_strcmp0 (territory, "PT") == 0 ||
702 	    g_strcmp0 (territory, "RO") == 0 ||
703 	    g_strcmp0 (territory, "SM") == 0 ||
704 	    g_strcmp0 (territory, "RS") == 0 ||
705 	    g_strcmp0 (territory, "SK") == 0 ||
706 	    g_strcmp0 (territory, "SI") == 0 ||
707 	    g_strcmp0 (territory, "ES") == 0 ||
708 	    g_strcmp0 (territory, "SE") == 0 ||
709 	    g_strcmp0 (territory, "CH") == 0 ||
710 	    g_strcmp0 (territory, "TR") == 0 ||
711 	    g_strcmp0 (territory, "UA") == 0 ||
712 	    g_strcmp0 (territory, "VA") == 0 ||
713 	    g_strcmp0 (territory, "IN") == 0 ||
714 	    g_strcmp0 (territory, "IL") == 0 ||
715 	    g_strcmp0 (territory, "PK") == 0 ||
716 	    g_strcmp0 (territory, "ZA") == 0)
717 		return AS_CONTENT_RATING_SYSTEM_PEGI;
718 
719 	/* Finland */
720 	if (g_strcmp0 (territory, "FI") == 0)
721 		return AS_CONTENT_RATING_SYSTEM_KAVI;
722 
723 	/* Germany */
724 	if (g_strcmp0 (territory, "DE") == 0)
725 		return AS_CONTENT_RATING_SYSTEM_USK;
726 
727 	/* Iran */
728 	if (g_strcmp0 (territory, "IR") == 0)
729 		return AS_CONTENT_RATING_SYSTEM_ESRA;
730 
731 	/* Japan */
732 	if (g_strcmp0 (territory, "JP") == 0)
733 		return AS_CONTENT_RATING_SYSTEM_CERO;
734 
735 	/* New Zealand */
736 	if (g_strcmp0 (territory, "NZ") == 0)
737 		return AS_CONTENT_RATING_SYSTEM_OFLCNZ;
738 
739 	/* Russia: Content rating law */
740 	if (g_strcmp0 (territory, "RU") == 0)
741 		return AS_CONTENT_RATING_SYSTEM_RUSSIA;
742 
743 	/* Singapore */
744 	if (g_strcmp0 (territory, "SQ") == 0)
745 		return AS_CONTENT_RATING_SYSTEM_MDA;
746 
747 	/* South Korea */
748 	if (g_strcmp0 (territory, "KR") == 0)
749 		return AS_CONTENT_RATING_SYSTEM_GRAC;
750 
751 	/* USA, Canada, Mexico */
752 	if ((g_strcmp0 (territory, "US") == 0) ||
753 	    g_strcmp0 (territory, "CA") == 0 ||
754 	    g_strcmp0 (territory, "MX") == 0)
755 		return AS_CONTENT_RATING_SYSTEM_ESRB;
756 
757 	/* everything else is IARC */
758 	return AS_CONTENT_RATING_SYSTEM_IARC;
759 }
760 
761 /* Table of the human-readable descriptions for each #AsContentRatingValue for
762  * each content rating category. @desc_none must be non-%NULL, but the other
763  * values may be %NULL if no description is appropriate. In that case, the next
764  * non-%NULL description for a lower #AsContentRatingValue will be used. */
765 static const struct {
766 	const gchar *id;  /* (not nullable) */
767 	const gchar *desc_none;  /* (not nullable) */
768 	const gchar *desc_mild;  /* (nullable) */
769 	const gchar *desc_moderate;  /* (nullable) */
770 	const gchar *desc_intense;  /* (nullable) */
771 } oars_descriptions[] = {
772 	{
773 		"violence-cartoon",
774 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
775 		N_("No cartoon violence"),
776 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
777 		N_("Cartoon characters in unsafe situations"),
778 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
779 		N_("Cartoon characters in aggressive conflict"),
780 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
781 		N_("Graphic violence involving cartoon characters"),
782 	},
783 	{
784 		"violence-fantasy",
785 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
786 		N_("No fantasy violence"),
787 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
788 		N_("Characters in unsafe situations easily distinguishable from reality"),
789 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
790 		N_("Characters in aggressive conflict easily distinguishable from reality"),
791 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
792 		N_("Graphic violence easily distinguishable from reality"),
793 	},
794 	{
795 		"violence-realistic",
796 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
797 		N_("No realistic violence"),
798 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
799 		N_("Mildly realistic characters in unsafe situations"),
800 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
801 		N_("Depictions of realistic characters in aggressive conflict"),
802 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
803 		N_("Graphic violence involving realistic characters"),
804 	},
805 	{
806 		"violence-bloodshed",
807 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
808 		N_("No bloodshed"),
809 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
810 		N_("Unrealistic bloodshed"),
811 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
812 		N_("Realistic bloodshed"),
813 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
814 		N_("Depictions of bloodshed and the mutilation of body parts"),
815 	},
816 	{
817 		"violence-sexual",
818 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
819 		N_("No sexual violence"),
820 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
821 		N_("Rape or other violent sexual behavior"),
822 		NULL,
823 		NULL,
824 	},
825 	{
826 		"drugs-alcohol",
827 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
828 		N_("No references to alcohol"),
829 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
830 		N_("References to alcoholic beverages"),
831 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
832 		N_("Use of alcoholic beverages"),
833 		NULL,
834 	},
835 	{
836 		"drugs-narcotics",
837 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
838 		N_("No references to illicit drugs"),
839 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
840 		N_("References to illicit drugs"),
841 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
842 		N_("Use of illicit drugs"),
843 		NULL,
844 	},
845 	{
846 		"drugs-tobacco",
847 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
848 		N_("No references to tobacco products"),
849 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
850 		N_("References to tobacco products"),
851 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
852 		N_("Use of tobacco products"),
853 		NULL,
854 	},
855 	{
856 		"sex-nudity",
857 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
858 		N_("No nudity of any sort"),
859 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
860 		N_("Brief artistic nudity"),
861 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
862 		N_("Prolonged nudity"),
863 		NULL,
864 	},
865 	{
866 		"sex-themes",
867 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
868 		N_("No references to or depictions of sexual nature"),
869 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
870 		N_("Provocative references or depictions"),
871 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
872 		N_("Sexual references or depictions"),
873 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
874 		N_("Graphic sexual behavior"),
875 	},
876 	{
877 		"language-profanity",
878 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
879 		N_("No profanity of any kind"),
880 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
881 		N_("Mild or infrequent use of profanity"),
882 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
883 		N_("Moderate use of profanity"),
884 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
885 		N_("Strong or frequent use of profanity"),
886 	},
887 	{
888 		"language-humor",
889 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
890 		N_("No inappropriate humor"),
891 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
892 		N_("Slapstick humor"),
893 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
894 		N_("Vulgar or bathroom humor"),
895 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
896 		N_("Mature or sexual humor"),
897 	},
898 	{
899 		"language-discrimination",
900 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
901 		N_("No discriminatory language of any kind"),
902 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
903 		N_("Negativity towards a specific group of people"),
904 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
905 		N_("Discrimination designed to cause emotional harm"),
906 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
907 		N_("Explicit discrimination based on gender, sexuality, race or religion"),
908 	},
909 	{
910 		"money-advertising",
911 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
912 		N_("No advertising of any kind"),
913 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
914 		N_("Product placement"),
915 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
916 		N_("Explicit references to specific brands or trademarked products"),
917 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
918 		N_("Users are encouraged to purchase specific real-world items"),
919 	},
920 	{
921 		"money-gambling",
922 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
923 		N_("No gambling of any kind"),
924 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
925 		N_("Gambling on random events using tokens or credits"),
926 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
927 		N_("Gambling using “play” money"),
928 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
929 		N_("Gambling using real money"),
930 	},
931 	{
932 		"money-purchasing",
933 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
934 		N_("No ability to spend money"),
935 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
936 		N_("Users are encouraged to donate real money"),
937 		NULL,
938 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
939 		N_("Ability to spend real money in-app"),
940 	},
941 	{
942 		"social-chat",
943 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
944 		N_("No way to chat with other users"),
945 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
946 		N_("User-to-user interactions without chat functionality"),
947 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
948 		N_("Moderated chat functionality between users"),
949 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
950 		N_("Uncontrolled chat functionality between users"),
951 	},
952 	{
953 		"social-audio",
954 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
955 		N_("No way to talk with other users"),
956 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
957 		N_("Uncontrolled audio or video chat functionality between users"),
958 		NULL,
959 		NULL,
960 	},
961 	{
962 		"social-contacts",
963 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
964 		N_("No sharing of social network usernames or email addresses"),
965 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
966 		N_("Sharing social network usernames or email addresses"),
967 		NULL,
968 		NULL,
969 	},
970 	{
971 		"social-info",
972 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
973 		N_("No sharing of user information with third parties"),
974 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
975 		N_("Checking for the latest application version"),
976 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
977 		N_("Sharing diagnostic data that does not let others identify the user"),
978 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
979 		N_("Sharing information that lets others identify the user"),
980 	},
981 	{
982 		"social-location",
983 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
984 		N_("No sharing of physical location with other users"),
985 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
986 		N_("Sharing physical location with other users"),
987 		NULL,
988 		NULL,
989 	},
990 
991 	/* v1.1 */
992 	{
993 		"sex-homosexuality",
994 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
995 		N_("No references to homosexuality"),
996 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
997 		N_("Indirect references to homosexuality"),
998 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
999 		N_("Kissing between people of the same gender"),
1000 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1001 		N_("Graphic sexual behavior between people of the same gender"),
1002 	},
1003 	{
1004 		"sex-prostitution",
1005 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1006 		N_("No references to prostitution"),
1007 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1008 		N_("Indirect references to prostitution"),
1009 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1010 		N_("Direct references to prostitution"),
1011 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1012 		N_("Graphic depictions of the act of prostitution"),
1013 	},
1014 	{
1015 		"sex-adultery",
1016 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1017 		N_("No references to adultery"),
1018 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1019 		N_("Indirect references to adultery"),
1020 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1021 		N_("Direct references to adultery"),
1022 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1023 		N_("Graphic depictions of the act of adultery"),
1024 	},
1025 	{
1026 		"sex-appearance",
1027 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1028 		N_("No sexualized characters"),
1029 		NULL,
1030 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1031 		N_("Scantily clad human characters"),
1032 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1033 		N_("Overtly sexualized human characters"),
1034 	},
1035 	{
1036 		"violence-worship",
1037 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1038 		N_("No references to desecration"),
1039 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1040 		N_("Depictions of or references to historical desecration"),
1041 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1042 		N_("Depictions of modern-day human desecration"),
1043 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1044 		N_("Graphic depictions of modern-day desecration"),
1045 	},
1046 	{
1047 		"violence-desecration",
1048 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1049 		N_("No visible dead human remains"),
1050 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1051 		N_("Visible dead human remains"),
1052 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1053 		N_("Dead human remains that are exposed to the elements"),
1054 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1055 		N_("Graphic depictions of desecration of human bodies"),
1056 	},
1057 	{
1058 		"violence-slavery",
1059 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1060 		N_("No references to slavery"),
1061 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1062 		N_("Depictions of or references to historical slavery"),
1063 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1064 		N_("Depictions of modern-day slavery"),
1065 		/* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
1066 		N_("Graphic depictions of modern-day slavery"),
1067 	},
1068 };
1069 
1070 /**
1071  * as_content_rating_attribute_get_description:
1072  * @id: the subsection ID e.g. `violence-cartoon`
1073  * @value: the #AsContentRatingValue, e.g. %AS_CONTENT_RATING_VALUE_INTENSE
1074  *
1075  * Get a human-readable description of what content would be expected to
1076  * require the content rating attribute given by @id and @value.
1077  *
1078  * Returns: a human-readable description of @id and @value
1079  * Since: 0.7.18
1080  */
1081 const gchar *
as_content_rating_attribute_get_description(const gchar * id,AsContentRatingValue value)1082 as_content_rating_attribute_get_description (const gchar *id, AsContentRatingValue value)
1083 {
1084 	gsize i;
1085 
1086 	if ((gint) value < AS_CONTENT_RATING_VALUE_NONE ||
1087 	    (gint) value > AS_CONTENT_RATING_VALUE_INTENSE)
1088 		return NULL;
1089 
1090 	for (i = 0; i < G_N_ELEMENTS (oars_descriptions); i++) {
1091 		if (!g_str_equal (oars_descriptions[i].id, id))
1092 			continue;
1093 
1094 		/* Return the most-intense non-NULL string. */
1095 		if (oars_descriptions[i].desc_intense != NULL && value >= AS_CONTENT_RATING_VALUE_INTENSE)
1096 			return _(oars_descriptions[i].desc_intense);
1097 		if (oars_descriptions[i].desc_moderate != NULL && value >= AS_CONTENT_RATING_VALUE_MODERATE)
1098 			return _(oars_descriptions[i].desc_moderate);
1099 		if (oars_descriptions[i].desc_mild != NULL && value >= AS_CONTENT_RATING_VALUE_MILD)
1100 			return _(oars_descriptions[i].desc_mild);
1101 		if (oars_descriptions[i].desc_none != NULL && value >= AS_CONTENT_RATING_VALUE_NONE)
1102 			return _(oars_descriptions[i].desc_none);
1103 		g_assert_not_reached ();
1104 	}
1105 
1106 	/* This means the requested @id is missing from @oars_descriptions, so
1107 	 * presumably the OARS spec has been updated but appstream-glib hasn’t. */
1108 	g_warn_if_reached ();
1109 
1110 	return NULL;
1111 }
1112 
1113 /* The struct definition below assumes we don’t grow more
1114  * #AsContentRating values. */
1115 G_STATIC_ASSERT (AS_CONTENT_RATING_VALUE_LAST == AS_CONTENT_RATING_VALUE_INTENSE + 1);
1116 
1117 static const struct {
1118 	const gchar	*id;
1119 	OarsVersion	 oars_version;  /* when the key was first added */
1120 	guint		 csm_age_none;  /* for %AS_CONTENT_RATING_VALUE_NONE */
1121 	guint		 csm_age_mild;  /* for %AS_CONTENT_RATING_VALUE_MILD */
1122 	guint		 csm_age_moderate;  /* for %AS_CONTENT_RATING_VALUE_MODERATE */
1123 	guint		 csm_age_intense;  /* for %AS_CONTENT_RATING_VALUE_INTENSE */
1124 } oars_to_csm_mappings[] =  {
1125 	/* Each @id must only appear once. The set of @csm_age_* values for a
1126 	 * given @id must be complete and non-decreasing. */
1127 	/* v1.0 */
1128 	{ "violence-cartoon",	OARS_1_0, 0, 3, 4, 6 },
1129 	{ "violence-fantasy",	OARS_1_0, 0, 3, 7, 8 },
1130 	{ "violence-realistic",	OARS_1_0, 0, 4, 9, 14 },
1131 	{ "violence-bloodshed",	OARS_1_0, 0, 9, 11, 18 },
1132 	{ "violence-sexual",	OARS_1_0, 0, 18, 18, 18 },
1133 	{ "drugs-alcohol",	OARS_1_0, 0, 11, 13, 16 },
1134 	{ "drugs-narcotics",	OARS_1_0, 0, 12, 14, 17 },
1135 	{ "drugs-tobacco",	OARS_1_0, 0, 10, 13, 13 },
1136 	{ "sex-nudity",		OARS_1_0, 0, 12, 14, 14 },
1137 	{ "sex-themes",		OARS_1_0, 0, 13, 14, 15 },
1138 	{ "language-profanity",	OARS_1_0, 0, 8, 11, 14 },
1139 	{ "language-humor",	OARS_1_0, 0, 3, 8, 14 },
1140 	{ "language-discrimination", OARS_1_0, 0, 9, 10, 11 },
1141 	{ "money-advertising",	OARS_1_0, 0, 7, 8, 10 },
1142 	{ "money-gambling",	OARS_1_0, 0, 7, 10, 18 },
1143 	{ "money-purchasing",	OARS_1_0, 0, 12, 14, 15 },
1144 	{ "social-chat",	OARS_1_0, 0, 4, 10, 13 },
1145 	{ "social-audio",	OARS_1_0, 0, 15, 15, 15 },
1146 	{ "social-contacts",	OARS_1_0, 0, 12, 12, 12 },
1147 	{ "social-info",	OARS_1_0, 0, 0, 13, 13 },
1148 	{ "social-location",	OARS_1_0, 0, 13, 13, 13 },
1149 	/* v1.1 additions */
1150 	{ "sex-homosexuality",	OARS_1_1, 0, 10, 13, 15 },
1151 	{ "sex-prostitution",	OARS_1_1, 0, 12, 14, 18 },
1152 	{ "sex-adultery",	OARS_1_1, 0, 8, 10, 18 },
1153 	{ "sex-appearance",	OARS_1_1, 0, 10, 10, 15 },
1154 	{ "violence-worship",	OARS_1_1, 0, 13, 15, 18 },
1155 	{ "violence-desecration", OARS_1_1, 0, 13, 15, 18 },
1156 	{ "violence-slavery",	OARS_1_1, 0, 13, 15, 18 },
1157 };
1158 
1159 static gboolean
is_oars_key(const gchar * id,OarsVersion version)1160 is_oars_key (const gchar *id, OarsVersion version)
1161 {
1162 	for (gsize i = 0; i < G_N_ELEMENTS (oars_to_csm_mappings); i++) {
1163 		if (g_str_equal (id, oars_to_csm_mappings[i].id))
1164 			return (oars_to_csm_mappings[i].oars_version <= version);
1165 	}
1166 	return FALSE;
1167 }
1168 
1169 /**
1170  * as_content_rating_attribute_to_csm_age:
1171  * @id: the subsection ID e.g. `violence-cartoon`
1172  * @value: the #AsContentRatingValue, e.g. %AS_CONTENT_RATING_VALUE_INTENSE
1173  *
1174  * Gets the Common Sense Media approved age for a specific rating level.
1175  *
1176  * Returns: The age in years, or 0 for no details.
1177  *
1178  * Since: 0.7.15
1179  **/
1180 guint
as_content_rating_attribute_to_csm_age(const gchar * id,AsContentRatingValue value)1181 as_content_rating_attribute_to_csm_age (const gchar *id, AsContentRatingValue value)
1182 {
1183 	if (value == AS_CONTENT_RATING_VALUE_UNKNOWN ||
1184 	    value == AS_CONTENT_RATING_VALUE_LAST)
1185 		return 0;
1186 
1187 	for (gsize i = 0; i < G_N_ELEMENTS (oars_to_csm_mappings); i++) {
1188 		if (g_str_equal (id, oars_to_csm_mappings[i].id)) {
1189 			switch (value) {
1190 			case AS_CONTENT_RATING_VALUE_NONE:
1191 				return oars_to_csm_mappings[i].csm_age_none;
1192 			case AS_CONTENT_RATING_VALUE_MILD:
1193 				return oars_to_csm_mappings[i].csm_age_mild;
1194 			case AS_CONTENT_RATING_VALUE_MODERATE:
1195 				return oars_to_csm_mappings[i].csm_age_moderate;
1196 			case AS_CONTENT_RATING_VALUE_INTENSE:
1197 				return oars_to_csm_mappings[i].csm_age_intense;
1198 			case AS_CONTENT_RATING_VALUE_UNKNOWN:
1199 			case AS_CONTENT_RATING_VALUE_LAST:
1200 			default:
1201 				/* Handled above. */
1202 				g_assert_not_reached ();
1203 				return 0;
1204 			}
1205 		}
1206 	}
1207 
1208 	/* @id not found. */
1209 	return 0;
1210 }
1211 
1212 /**
1213  * as_content_rating_attribute_from_csm_age:
1214  * @id: the subsection ID e.g. `violence-cartoon`
1215  * @age: the CSM age
1216  *
1217  * Gets the highest #AsContentRatingValue which is allowed to be seen by the
1218  * given Common Sense Media @age for the given subsection @id.
1219  *
1220  * For example, if the CSM age mappings for `violence-bloodshed` are:
1221  *  * age ≥ 0 for %AS_CONTENT_RATING_VALUE_NONE
1222  *  * age ≥ 9 for %AS_CONTENT_RATING_VALUE_MILD
1223  *  * age ≥ 11 for %AS_CONTENT_RATING_VALUE_MODERATE
1224  *  * age ≥ 18 for %AS_CONTENT_RATING_VALUE_INTENSE
1225  * then calling this function with `violence-bloodshed` and @age set to 17 would
1226  * return %AS_CONTENT_RATING_VALUE_MODERATE. Calling it with age 18 would
1227  * return %AS_CONTENT_RATING_VALUE_INTENSE.
1228  *
1229  * Returns: the #AsContentRatingValue, or %AS_CONTENT_RATING_VALUE_UNKNOWN if
1230  *    unknown
1231  * Since: 0.7.18
1232  */
1233 AsContentRatingValue
as_content_rating_attribute_from_csm_age(const gchar * id,guint age)1234 as_content_rating_attribute_from_csm_age (const gchar *id, guint age)
1235 {
1236 	for (gsize i = 0; G_N_ELEMENTS (oars_to_csm_mappings); i++) {
1237 		if (g_strcmp0 (id, oars_to_csm_mappings[i].id) == 0) {
1238 			if (age >= oars_to_csm_mappings[i].csm_age_intense)
1239 				return AS_CONTENT_RATING_VALUE_INTENSE;
1240 			else if (age >= oars_to_csm_mappings[i].csm_age_moderate)
1241 				return AS_CONTENT_RATING_VALUE_MODERATE;
1242 			else if (age >= oars_to_csm_mappings[i].csm_age_mild)
1243 				return AS_CONTENT_RATING_VALUE_MILD;
1244 			else if (age >= oars_to_csm_mappings[i].csm_age_none)
1245 				return AS_CONTENT_RATING_VALUE_NONE;
1246 			else
1247 				return AS_CONTENT_RATING_VALUE_UNKNOWN;
1248 		}
1249 	}
1250 
1251 	return AS_CONTENT_RATING_VALUE_UNKNOWN;
1252 }
1253 
1254 /**
1255  * as_content_rating_get_all_rating_ids:
1256  *
1257  * Returns a list of all the valid OARS content rating attribute IDs as could
1258  * be passed to as_content_rating_add_attribute() or
1259  * as_content_rating_attribute_to_csm_age().
1260  *
1261  * Returns: (array zero-terminated=1) (transfer container): a %NULL-terminated
1262  *    array of IDs, to be freed with g_free() (the element values are owned by
1263  *    libappstream-glib and must not be freed)
1264  * Since: 0.7.15
1265  */
1266 const gchar **
as_content_rating_get_all_rating_ids(void)1267 as_content_rating_get_all_rating_ids (void)
1268 {
1269 	g_autofree const gchar **ids = NULL;
1270 
1271 	ids = g_new0 (const gchar *, G_N_ELEMENTS (oars_to_csm_mappings) + 1 /* NULL terminator */);
1272 	for (gsize i = 0; i < G_N_ELEMENTS (oars_to_csm_mappings); i++)
1273 		ids[i] = oars_to_csm_mappings[i].id;
1274 
1275 	return g_steal_pointer (&ids);
1276 }
1277 
1278 /**
1279  * as_content_rating_get_minimum_age:
1280  * @content_rating: a #AsContentRating
1281  *
1282  * Gets the lowest Common Sense Media approved age for the content_rating block.
1283  * NOTE: these numbers are based on the data and descriptions available from
1284  * https://www.commonsensemedia.org/about-us/our-mission/about-our-ratings and
1285  * you may disagree with them.
1286  *
1287  * You're free to disagree with these, and of course you should use your own
1288  * brain to work our if your child is able to cope with the concepts enumerated
1289  * here. Some 13 year olds may be fine with the concept of mutilation of body
1290  * parts; others may get nightmares.
1291  *
1292  * Returns: The age in years, 0 for no rating, or G_MAXUINT for no details.
1293  *
1294  * Since: 0.5.12
1295  **/
1296 guint
as_content_rating_get_minimum_age(AsContentRating * content_rating)1297 as_content_rating_get_minimum_age (AsContentRating *content_rating)
1298 {
1299 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
1300 	guint i;
1301 	guint csm_age = 0;
1302 
1303 	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), 0);
1304 
1305 	/* check kind */
1306 	if (g_strcmp0 (priv->kind, "oars-1.0") != 0 &&
1307 	    g_strcmp0 (priv->kind, "oars-1.1") != 0)
1308 		return G_MAXUINT;
1309 
1310 	for (i = 0; i < priv->keys->len; i++) {
1311 		AsContentRatingKey *key;
1312 		guint csm_tmp;
1313 		key = g_ptr_array_index (priv->keys, i);
1314 		csm_tmp = as_content_rating_attribute_to_csm_age (key->id, key->value);
1315 		if (csm_tmp > 0 && csm_tmp > csm_age)
1316 			csm_age = csm_tmp;
1317 	}
1318 	return csm_age;
1319 }
1320 
1321 /**
1322  * as_content_rating_get_kind:
1323  * @content_rating: a #AsContentRating instance.
1324  *
1325  * Gets the content_rating kind.
1326  *
1327  * Returns: a string, e.g. "oars-1.0", or NULL
1328  *
1329  * Since: 0.5.12
1330  **/
1331 const gchar *
as_content_rating_get_kind(AsContentRating * content_rating)1332 as_content_rating_get_kind (AsContentRating *content_rating)
1333 {
1334 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
1335 	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), NULL);
1336 	return priv->kind;
1337 }
1338 
1339 /**
1340  * as_content_rating_set_kind:
1341  * @content_rating: a #AsContentRating instance.
1342  * @kind: the rating kind, e.g. "oars-1.0"
1343  *
1344  * Sets the content rating kind.
1345  *
1346  * Since: 0.5.12
1347  **/
1348 void
as_content_rating_set_kind(AsContentRating * content_rating,const gchar * kind)1349 as_content_rating_set_kind (AsContentRating *content_rating, const gchar *kind)
1350 {
1351 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
1352 	g_return_if_fail (AS_IS_CONTENT_RATING (content_rating));
1353 	as_ref_string_assign_safe (&priv->kind, kind);
1354 }
1355 
1356 /**
1357  * as_content_rating_node_insert: (skip)
1358  * @content_rating: a #AsContentRating instance.
1359  * @parent: the parent #GNode to use.
1360  * @ctx: the #AsNodeContext
1361  *
1362  * Inserts the content_rating into the DOM tree.
1363  *
1364  * Returns: (transfer none): A populated #GNode, or %NULL
1365  *
1366  * Since: 0.5.12
1367  **/
1368 GNode *
as_content_rating_node_insert(AsContentRating * content_rating,GNode * parent,AsNodeContext * ctx)1369 as_content_rating_node_insert (AsContentRating *content_rating,
1370 			       GNode *parent,
1371 			       AsNodeContext *ctx)
1372 {
1373 	AsContentRatingKey *key;
1374 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
1375 	GNode *n;
1376 	guint i;
1377 
1378 	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), NULL);
1379 
1380 	n = as_node_insert (parent, "content_rating", NULL,
1381 			    AS_NODE_INSERT_FLAG_NONE,
1382 			    NULL);
1383 	if (priv->kind != NULL)
1384 		as_node_add_attribute (n, "type", priv->kind);
1385 	for (i = 0; i < priv->keys->len; i++) {
1386 		const gchar *tmp;
1387 		key = g_ptr_array_index (priv->keys, i);
1388 		tmp = as_content_rating_value_to_string (key->value);
1389 		as_node_insert (n, "content_attribute", tmp,
1390 				AS_NODE_INSERT_FLAG_NONE,
1391 				"id", key->id,
1392 				NULL);
1393 	}
1394 	return n;
1395 }
1396 
1397 /**
1398  * as_content_rating_add_attribute:
1399  * @content_rating: a #AsContentRating instance.
1400  * @id: a content rating ID, e.g. `money-gambling`.
1401  * @value: a #AsContentRatingValue, e.g. %AS_CONTENT_RATING_VALUE_MODERATE.
1402  *
1403  * Adds an attribute value to the content rating.
1404  *
1405  * Since: 0.7.14
1406  **/
1407 void
as_content_rating_add_attribute(AsContentRating * content_rating,const gchar * id,AsContentRatingValue value)1408 as_content_rating_add_attribute (AsContentRating *content_rating,
1409 				 const gchar *id,
1410 				 AsContentRatingValue value)
1411 {
1412 	AsContentRatingKey *key = g_slice_new0 (AsContentRatingKey);
1413 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
1414 
1415 	g_return_if_fail (AS_IS_CONTENT_RATING (content_rating));
1416 	g_return_if_fail (id != NULL);
1417 	g_return_if_fail (value != AS_CONTENT_RATING_VALUE_UNKNOWN);
1418 
1419 	key->id = as_ref_string_new (id);
1420 	key->value = value;
1421 	g_ptr_array_add (priv->keys, key);
1422 }
1423 
1424 /**
1425  * as_content_rating_node_parse:
1426  * @content_rating: a #AsContentRating instance.
1427  * @node: a #GNode.
1428  * @ctx: a #AsNodeContext.
1429  * @error: A #GError or %NULL.
1430  *
1431  * Populates the object from a DOM node.
1432  *
1433  * Returns: %TRUE for success
1434  *
1435  * Since: 0.5.12
1436  **/
1437 gboolean
as_content_rating_node_parse(AsContentRating * content_rating,GNode * node,AsNodeContext * ctx,GError ** error)1438 as_content_rating_node_parse (AsContentRating *content_rating, GNode *node,
1439 			      AsNodeContext *ctx, GError **error)
1440 {
1441 	AsContentRatingPrivate *priv = GET_PRIVATE (content_rating);
1442 	GNode *c;
1443 	const gchar *tmp;
1444 	g_autoptr(GHashTable) captions = NULL;
1445 
1446 	g_return_val_if_fail (AS_IS_CONTENT_RATING (content_rating), FALSE);
1447 
1448 	/* get ID */
1449 	tmp = as_node_get_attribute (node, "type");
1450 	if (tmp != NULL)
1451 		as_content_rating_set_kind (content_rating, tmp);
1452 
1453 	/* get keys */
1454 	for (c = node->children; c != NULL; c = c->next) {
1455 		AsContentRatingKey *key;
1456 		if (as_node_get_tag (c) != AS_TAG_CONTENT_ATTRIBUTE)
1457 			continue;
1458 		key = g_slice_new0 (AsContentRatingKey);
1459 		as_ref_string_assign (&key->id, as_node_get_attribute_as_refstr (c, "id"));
1460 		key->value = as_content_rating_value_from_string (as_node_get_data (c));
1461 		g_ptr_array_add (priv->keys, key);
1462 	}
1463 	return TRUE;
1464 }
1465 
1466 /**
1467  * as_content_rating_new:
1468  *
1469  * Creates a new #AsContentRating.
1470  *
1471  * Returns: (transfer full): a #AsContentRating
1472  *
1473  * Since: 0.5.12
1474  **/
1475 AsContentRating *
as_content_rating_new(void)1476 as_content_rating_new (void)
1477 {
1478 	AsContentRating *content_rating;
1479 	content_rating = g_object_new (AS_TYPE_CONTENT, NULL);
1480 	return AS_CONTENT_RATING (content_rating);
1481 }
1482