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