1 /* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3 * Copyright © 2015 Gustavo Noronha Silva <gns@gnome.org>
4 * Copyright © 2016-2017 Igalia S.L.
5 *
6 * This file is part of Epiphany.
7 *
8 * Epiphany is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * Epiphany is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 #include "config.h"
23 #include "ephy-permissions-manager.h"
24
25 #include "ephy-file-helpers.h"
26 #include "ephy-string.h"
27
28 #define G_SETTINGS_ENABLE_BACKEND 1
29 #include <gio/gsettingsbackend.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <webkit2/webkit2.h>
33
34 struct _EphyPermissionsManager {
35 GObject parent_instance;
36
37 GHashTable *origins_mapping;
38 GHashTable *settings_mapping;
39
40 GHashTable *permission_type_permitted_origins;
41 GHashTable *permission_type_denied_origins;
42
43 GSettingsBackend *backend;
44 };
45
G_DEFINE_TYPE(EphyPermissionsManager,ephy_permissions_manager,G_TYPE_OBJECT)46 G_DEFINE_TYPE (EphyPermissionsManager, ephy_permissions_manager, G_TYPE_OBJECT)
47
48 #define PERMISSIONS_FILENAME "permissions.ini"
49
50 static void
51 ephy_permissions_manager_init (EphyPermissionsManager *manager)
52 {
53 g_autofree char *filename = NULL;
54
55 manager->origins_mapping = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
56 manager->settings_mapping = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
57
58 /* We cannot use a key_destroy_func here because we need to be able to update
59 * the GList keys without destroying the contents of the lists. */
60 manager->permission_type_permitted_origins = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL);
61 manager->permission_type_denied_origins = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL);
62
63 filename = g_build_filename (ephy_profile_dir (), PERMISSIONS_FILENAME, NULL);
64 manager->backend = g_keyfile_settings_backend_new (filename, "/", NULL);
65 }
66
67 static void
free_cached_origin_list(gpointer key,gpointer value,gpointer user_data)68 free_cached_origin_list (gpointer key,
69 gpointer value,
70 gpointer user_data)
71 {
72 g_list_free_full ((GList *)value, (GDestroyNotify)webkit_security_origin_unref);
73 }
74
75 static void
ephy_permissions_manager_dispose(GObject * object)76 ephy_permissions_manager_dispose (GObject *object)
77 {
78 EphyPermissionsManager *manager = EPHY_PERMISSIONS_MANAGER (object);
79
80 g_clear_pointer (&manager->origins_mapping, g_hash_table_destroy);
81 g_clear_pointer (&manager->settings_mapping, g_hash_table_destroy);
82
83 if (manager->permission_type_permitted_origins != NULL) {
84 g_hash_table_foreach (manager->permission_type_permitted_origins, free_cached_origin_list, NULL);
85 g_hash_table_destroy (manager->permission_type_permitted_origins);
86 manager->permission_type_permitted_origins = NULL;
87 }
88
89 if (manager->permission_type_denied_origins != NULL) {
90 g_hash_table_foreach (manager->permission_type_denied_origins, free_cached_origin_list, NULL);
91 g_hash_table_destroy (manager->permission_type_denied_origins);
92 manager->permission_type_denied_origins = NULL;
93 }
94
95 g_clear_object (&manager->backend);
96
97 G_OBJECT_CLASS (ephy_permissions_manager_parent_class)->dispose (object);
98 }
99
100 static void
ephy_permissions_manager_class_init(EphyPermissionsManagerClass * klass)101 ephy_permissions_manager_class_init (EphyPermissionsManagerClass *klass)
102 {
103 GObjectClass *object_class = G_OBJECT_CLASS (klass);
104
105 object_class->dispose = ephy_permissions_manager_dispose;
106 }
107
108 static GSettings *
ephy_permissions_manager_get_settings_for_origin(EphyPermissionsManager * manager,const char * origin)109 ephy_permissions_manager_get_settings_for_origin (EphyPermissionsManager *manager,
110 const char *origin)
111 {
112 char *origin_path;
113 char *trimmed_protocol;
114 GSettings *settings;
115 WebKitSecurityOrigin *security_origin;
116 char *pos;
117
118 g_assert (origin != NULL);
119
120 settings = g_hash_table_lookup (manager->origins_mapping, origin);
121 if (settings)
122 return settings;
123
124 /* Cannot contain consecutive slashes in GSettings path... */
125 security_origin = webkit_security_origin_new_for_uri (origin);
126 trimmed_protocol = g_strdup (webkit_security_origin_get_protocol (security_origin));
127 pos = strchr (trimmed_protocol, '/');
128 if (pos != NULL)
129 *pos = '\0';
130
131 origin_path = g_strdup_printf ("/org/gnome/epiphany/permissions/%s/%s/%u/",
132 trimmed_protocol,
133 webkit_security_origin_get_host (security_origin),
134 webkit_security_origin_get_port (security_origin));
135
136 settings = g_settings_new_with_backend_and_path ("org.gnome.Epiphany.permissions", manager->backend, origin_path);
137 g_free (trimmed_protocol);
138 g_free (origin_path);
139 webkit_security_origin_unref (security_origin);
140
141 /* Note that settings is owned only by the first hash table! */
142 g_hash_table_insert (manager->origins_mapping, g_strdup (origin), settings);
143 g_hash_table_insert (manager->settings_mapping, settings, g_strdup (origin));
144
145 return settings;
146 }
147
148 EphyPermissionsManager *
ephy_permissions_manager_new(void)149 ephy_permissions_manager_new (void)
150 {
151 return EPHY_PERMISSIONS_MANAGER (g_object_new (EPHY_TYPE_PERMISSIONS_MANAGER, NULL));
152 }
153
154 static const char *
permission_type_to_string(EphyPermissionType type)155 permission_type_to_string (EphyPermissionType type)
156 {
157 switch (type) {
158 case EPHY_PERMISSION_TYPE_SHOW_NOTIFICATIONS:
159 return "notifications-permission";
160 case EPHY_PERMISSION_TYPE_SAVE_PASSWORD:
161 return "save-password-permission";
162 case EPHY_PERMISSION_TYPE_ACCESS_LOCATION:
163 return "geolocation-permission";
164 case EPHY_PERMISSION_TYPE_ACCESS_MICROPHONE:
165 return "audio-device-permission";
166 case EPHY_PERMISSION_TYPE_ACCESS_WEBCAM:
167 return "video-device-permission";
168 case EPHY_PERMISSION_TYPE_SHOW_ADS:
169 return "advertisement-permission";
170 case EPHY_PERMISSION_TYPE_AUTOPLAY_POLICY:
171 return "autoplay-permission";
172 default:
173 g_assert_not_reached ();
174 }
175 }
176
177 EphyPermission
ephy_permissions_manager_get_permission(EphyPermissionsManager * manager,EphyPermissionType type,const char * origin)178 ephy_permissions_manager_get_permission (EphyPermissionsManager *manager,
179 EphyPermissionType type,
180 const char *origin)
181 {
182 GSettings *settings;
183
184 g_assert (type != EPHY_PERMISSION_TYPE_ACCESS_WEBCAM_AND_MICROPHONE);
185
186 settings = ephy_permissions_manager_get_settings_for_origin (manager, origin);
187 return g_settings_get_enum (settings, permission_type_to_string (type));
188 }
189
190 static gint
webkit_security_origin_compare(WebKitSecurityOrigin * a,WebKitSecurityOrigin * b)191 webkit_security_origin_compare (WebKitSecurityOrigin *a,
192 WebKitSecurityOrigin *b)
193 {
194 const char *protocol_a, *protocol_b;
195 const char *host_a, *host_b;
196
197 protocol_a = webkit_security_origin_get_protocol (a);
198 protocol_b = webkit_security_origin_get_protocol (b);
199 host_a = webkit_security_origin_get_host (a);
200 host_b = webkit_security_origin_get_host (b);
201
202 /* Security origins objects with NULL protocol or host do not represent the
203 * same origin as others with NULL protocol or host. That is, they're not
204 * equal. Therefore, they cannot be ordered, and must not be passed to this
205 * compare function.
206 */
207 g_assert (protocol_a != NULL);
208 g_assert (protocol_b != NULL);
209 g_assert (host_a != NULL);
210 g_assert (host_b != NULL);
211
212 return strcmp (protocol_a, protocol_b) || strcmp (host_a, host_b) ||
213 webkit_security_origin_get_port (b) - webkit_security_origin_get_port (a);
214 }
215
216 static void
maybe_add_origin_to_permission_type_cache(GHashTable * permissions,EphyPermissionType type,WebKitSecurityOrigin * origin)217 maybe_add_origin_to_permission_type_cache (GHashTable *permissions,
218 EphyPermissionType type,
219 WebKitSecurityOrigin *origin)
220 {
221 GList *origins;
222 GList *l;
223
224 /* Add origin to the appropriate permissions cache if (a) the cache already
225 * exists, and (b) it does not already contain origin. */
226 origins = g_hash_table_lookup (permissions, GINT_TO_POINTER (type));
227 if (origins != NULL) {
228 l = g_list_find_custom (origins, origin, (GCompareFunc)webkit_security_origin_compare);
229 if (l == NULL) {
230 origins = g_list_prepend (origins, webkit_security_origin_ref (origin));
231 g_hash_table_replace (permissions, GINT_TO_POINTER (type), origins);
232 }
233 }
234 }
235
236 static void
maybe_remove_origin_from_permission_type_cache(GHashTable * permissions,EphyPermissionType type,WebKitSecurityOrigin * origin)237 maybe_remove_origin_from_permission_type_cache (GHashTable *permissions,
238 EphyPermissionType type,
239 WebKitSecurityOrigin *origin)
240 {
241 GList *origins;
242 GList *l;
243
244 /* Remove origin from the appropriate permissions cache if (a) the cache
245 * exists, and (b) it contains origin. */
246 origins = g_hash_table_lookup (permissions, GINT_TO_POINTER (type));
247 if (origins != NULL) {
248 l = g_list_find_custom (origins, origin, (GCompareFunc)webkit_security_origin_compare);
249 if (l != NULL) {
250 webkit_security_origin_unref (l->data);
251 origins = g_list_remove_link (origins, l);
252 g_hash_table_replace (permissions, GINT_TO_POINTER (type), origins);
253 }
254 }
255 }
256
257 void
ephy_permissions_manager_set_permission(EphyPermissionsManager * manager,EphyPermissionType type,const char * origin,EphyPermission permission)258 ephy_permissions_manager_set_permission (EphyPermissionsManager *manager,
259 EphyPermissionType type,
260 const char *origin,
261 EphyPermission permission)
262 {
263 WebKitSecurityOrigin *webkit_origin;
264 GSettings *settings;
265
266 g_assert (type != EPHY_PERMISSION_TYPE_ACCESS_WEBCAM_AND_MICROPHONE);
267
268 webkit_origin = webkit_security_origin_new_for_uri (origin);
269 if (webkit_origin == NULL)
270 return;
271
272 settings = ephy_permissions_manager_get_settings_for_origin (manager, origin);
273 g_settings_set_enum (settings, permission_type_to_string (type), permission);
274
275 switch (permission) {
276 case EPHY_PERMISSION_UNDECIDED:
277 maybe_remove_origin_from_permission_type_cache (manager->permission_type_permitted_origins, type, webkit_origin);
278 maybe_remove_origin_from_permission_type_cache (manager->permission_type_denied_origins, type, webkit_origin);
279 break;
280 case EPHY_PERMISSION_DENY:
281 maybe_remove_origin_from_permission_type_cache (manager->permission_type_permitted_origins, type, webkit_origin);
282 maybe_add_origin_to_permission_type_cache (manager->permission_type_denied_origins, type, webkit_origin);
283 break;
284 case EPHY_PERMISSION_PERMIT:
285 maybe_add_origin_to_permission_type_cache (manager->permission_type_permitted_origins, type, webkit_origin);
286 maybe_remove_origin_from_permission_type_cache (manager->permission_type_denied_origins, type, webkit_origin);
287 break;
288 default:
289 g_assert_not_reached ();
290 }
291
292 webkit_security_origin_unref (webkit_origin);
293 }
294
295 static WebKitSecurityOrigin *
group_name_to_security_origin(const char * group)296 group_name_to_security_origin (const char *group)
297 {
298 char **tokens;
299 WebKitSecurityOrigin *origin = NULL;
300
301 /* Should be of form org/gnome/epiphany/permissions/http/example.com/0 */
302 tokens = g_strsplit (group, "/", -1);
303 if (g_strv_length (tokens) == 7 && tokens[4] != NULL && tokens[5] != NULL && tokens[6] != NULL)
304 origin = webkit_security_origin_new (tokens[4], tokens[5], atoi (tokens[6]));
305
306 g_strfreev (tokens);
307
308 return origin;
309 }
310
311 static WebKitSecurityOrigin *
origin_for_keyfile_key(GKeyFile * file,const char * filename,const char * group,const char * key,EphyPermissionType type,gboolean permit)312 origin_for_keyfile_key (GKeyFile *file,
313 const char *filename,
314 const char *group,
315 const char *key,
316 EphyPermissionType type,
317 gboolean permit)
318 {
319 WebKitSecurityOrigin *origin = NULL;
320
321 if (strcmp (permission_type_to_string (type), key) == 0) {
322 g_autoptr (GError) error = NULL;
323 g_autofree char *value = NULL;
324
325 value = g_key_file_get_string (file, group, key, &error);
326 if (error != NULL) {
327 g_warning ("Error processing %s group %s key %s: %s",
328 filename, group, key, error->message);
329 return NULL;
330 }
331
332 if ((permit && strcmp (value, "'allow'") == 0) ||
333 (!permit && strcmp (value, "'deny'") == 0))
334 origin = group_name_to_security_origin (group);
335 }
336
337 return origin;
338 }
339
340 static GList *
origins_for_keyfile_group(GKeyFile * file,const char * filename,const char * group,EphyPermissionType type,gboolean permit)341 origins_for_keyfile_group (GKeyFile *file,
342 const char *filename,
343 const char *group,
344 EphyPermissionType type,
345 gboolean permit)
346 {
347 g_auto (GStrv) keys = NULL;
348 gsize keys_length;
349 GList *origins = NULL;
350 g_autoptr (GError) error = NULL;
351
352 keys = g_key_file_get_keys (file, group, &keys_length, &error);
353 if (error != NULL) {
354 g_warning ("Error processing %s group %s: %s", filename, group, error->message);
355 return NULL;
356 }
357
358 for (guint i = 0; i < keys_length; i++) {
359 WebKitSecurityOrigin *origin;
360
361 origin = origin_for_keyfile_key (file, filename, group, keys[i], type, permit);
362 if (origin)
363 origins = g_list_prepend (origins, origin);
364 }
365
366 return origins;
367 }
368
369 static GList *
ephy_permissions_manager_get_matching_origins(EphyPermissionsManager * manager,EphyPermissionType type,gboolean permit)370 ephy_permissions_manager_get_matching_origins (EphyPermissionsManager *manager,
371 EphyPermissionType type,
372 gboolean permit)
373 {
374 GKeyFile *file;
375 char *filename;
376 char **groups = NULL;
377 gsize groups_length;
378 GList *origins = NULL;
379 GError *error = NULL;
380
381 /* Return results from cache, if they exist. */
382 if (permit) {
383 origins = g_hash_table_lookup (manager->permission_type_permitted_origins, GINT_TO_POINTER (type));
384 if (origins != NULL)
385 return origins;
386 } else {
387 origins = g_hash_table_lookup (manager->permission_type_denied_origins, GINT_TO_POINTER (type));
388 if (origins != NULL)
389 return origins;
390 }
391
392 /* Not cached. Load results from GSettings keyfile. Do it manually because the
393 * GSettings API is not designed to be used for enumerating settings. */
394 file = g_key_file_new ();
395 filename = g_build_filename (ephy_profile_dir (), PERMISSIONS_FILENAME, NULL);
396
397 g_key_file_load_from_file (file, filename, G_KEY_FILE_NONE, &error);
398 if (error != NULL) {
399 if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
400 g_warning ("Error processing %s: %s", filename, error->message);
401 g_error_free (error);
402 goto out;
403 }
404
405 groups = g_key_file_get_groups (file, &groups_length);
406 for (guint i = 0; i < groups_length; i++)
407 origins = g_list_concat (origins,
408 origins_for_keyfile_group (file, filename, groups[i], type, permit));
409
410 /* Cache the results. */
411 if (origins != NULL) {
412 g_hash_table_insert (permit ? manager->permission_type_permitted_origins
413 : manager->permission_type_denied_origins,
414 GINT_TO_POINTER (type),
415 origins);
416 }
417
418 out:
419 g_key_file_unref (file);
420 g_strfreev (groups);
421 g_free (filename);
422
423 return origins;
424 }
425
426 GList *
ephy_permissions_manager_get_permitted_origins(EphyPermissionsManager * manager,EphyPermissionType type)427 ephy_permissions_manager_get_permitted_origins (EphyPermissionsManager *manager,
428 EphyPermissionType type)
429 {
430 return ephy_permissions_manager_get_matching_origins (manager, type, TRUE);
431 }
432
433 GList *
ephy_permissions_manager_get_denied_origins(EphyPermissionsManager * manager,EphyPermissionType type)434 ephy_permissions_manager_get_denied_origins (EphyPermissionsManager *manager,
435 EphyPermissionType type)
436 {
437 return ephy_permissions_manager_get_matching_origins (manager, type, FALSE);
438 }
439
440 static EphyPermission
js_permissions_manager_get_permission(EphyPermissionsManager * manager,EphyPermissionType type,const char * origin)441 js_permissions_manager_get_permission (EphyPermissionsManager *manager,
442 EphyPermissionType type,
443 const char *origin)
444 {
445 return ephy_permissions_manager_get_permission (manager, type, origin);
446 }
447
448 void
ephy_permissions_manager_export_to_js_context(EphyPermissionsManager * manager,JSCContext * js_context,JSCValue * js_namespace)449 ephy_permissions_manager_export_to_js_context (EphyPermissionsManager *manager,
450 JSCContext *js_context,
451 JSCValue *js_namespace)
452 {
453 JSCClass *js_class;
454 JSCValue *js_enum, *js_enum_value;
455 JSCValue *js_permissions_manager;
456
457 js_class = jsc_context_register_class (js_context, "PermissionsManager", NULL, NULL, NULL);
458 jsc_class_add_method (js_class,
459 "permission",
460 G_CALLBACK (js_permissions_manager_get_permission), NULL, NULL,
461 G_TYPE_INT, 2,
462 G_TYPE_INT, G_TYPE_STRING);
463
464 js_permissions_manager = jsc_value_new_object (js_context, manager, js_class);
465 jsc_value_object_set_property (js_namespace, "permissionsManager", js_permissions_manager);
466 g_object_unref (js_permissions_manager);
467
468 js_enum = jsc_value_new_object (js_context, NULL, NULL);
469 js_enum_value = jsc_value_new_number (js_context, EPHY_PERMISSION_UNDECIDED);
470 jsc_value_object_set_property (js_enum, "UNDECIDED", js_enum_value);
471 g_object_unref (js_enum_value);
472 js_enum_value = jsc_value_new_number (js_context, EPHY_PERMISSION_DENY);
473 jsc_value_object_set_property (js_enum, "DENY", js_enum_value);
474 g_object_unref (js_enum_value);
475 js_enum_value = jsc_value_new_number (js_context, EPHY_PERMISSION_PERMIT);
476 jsc_value_object_set_property (js_enum, "PERMIT", js_enum_value);
477 g_object_unref (js_enum_value);
478 jsc_value_object_set_property (js_namespace, "Permission", js_enum);
479 g_object_unref (js_enum);
480
481 js_enum = jsc_value_new_object (js_context, NULL, NULL);
482 js_enum_value = jsc_value_new_number (js_context, EPHY_PERMISSION_TYPE_SHOW_NOTIFICATIONS);
483 jsc_value_object_set_property (js_enum, "SHOW_NOTIFICATIONS", js_enum_value);
484 g_object_unref (js_enum_value);
485 js_enum_value = jsc_value_new_number (js_context, EPHY_PERMISSION_TYPE_SAVE_PASSWORD);
486 jsc_value_object_set_property (js_enum, "SAVE_PASSWORD", js_enum_value);
487 g_object_unref (js_enum_value);
488 js_enum_value = jsc_value_new_number (js_context, EPHY_PERMISSION_TYPE_ACCESS_LOCATION);
489 jsc_value_object_set_property (js_enum, "ACCESS_LOCATION", js_enum_value);
490 g_object_unref (js_enum_value);
491 js_enum_value = jsc_value_new_number (js_context, EPHY_PERMISSION_TYPE_ACCESS_MICROPHONE);
492 jsc_value_object_set_property (js_enum, "ACCESS_MICROPHONE", js_enum_value);
493 g_object_unref (js_enum_value);
494 js_enum_value = jsc_value_new_number (js_context, EPHY_PERMISSION_TYPE_ACCESS_WEBCAM);
495 jsc_value_object_set_property (js_enum, "ACCESS_WEBCAM", js_enum_value);
496 g_object_unref (js_enum_value);
497 js_enum_value = jsc_value_new_number (js_context, EPHY_PERMISSION_TYPE_SHOW_ADS);
498 jsc_value_object_set_property (js_enum, "SHOW_ADS", js_enum_value);
499 g_object_unref (js_enum_value);
500 jsc_value_object_set_property (js_namespace, "PermissionType", js_enum);
501 g_object_unref (js_enum);
502 }
503