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