Post-release version bump
[platform/upstream/folks.git] / folks / backend-store.vala
1 /*
2  * Copyright (C) 2008 Nokia Corporation.
3  * Copyright (C) 2008 Zeeshan Ali (Khattak) <zeeshanak@gnome.org>.
4  * Copyright (C) 2010 Collabora Ltd.
5  *
6  * This library is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Lesser General Public License as published by
8  * the Free Software Foundation, either version 2.1 of the License, or
9  * (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this library.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  * Authors: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
20  *          Travis Reitter <travis.reitter@collabora.co.uk>
21  *
22  * This file was originally part of Rygel.
23  */
24
25 using Gee;
26 using GLib;
27
28 extern const string G_LOG_DOMAIN;
29
30 /**
31  * Responsible for backend loading.
32  *
33  * The BackendStore manages the set of available Folks backends. The
34  * {@link BackendStore.load_backends} function loads all compatible and enabled
35  * backends and the {@link BackendStore.backend_available} signal notifies when
36  * these backends are ready.
37  */
38 public class Folks.BackendStore : Object {
39   [CCode (has_target = false)]
40   private delegate void ModuleInitFunc (BackendStore store);
41   [CCode (has_target = false)]
42   private delegate void ModuleFinalizeFunc (BackendStore store);
43
44   /* this contains all backends, regardless of enabled or prepared state */
45   private HashMap<string,Backend> _backend_hash;
46   private HashMap<string, Backend> _prepared_backends;
47   private Map<string, Backend> _prepared_backends_ro;
48   private File _config_file;
49   private GLib.KeyFile _backends_key_file;
50   private HashMap<string,unowned Module> _modules;
51   private static weak BackendStore? _instance = null;
52   private bool _is_prepared = false;
53   private Debug _debug;
54
55   /**
56    * This keyword in the keyfile acts as a wildcard for all backends not already
57    * listed in the same file.
58    *
59    * This is particularly useful for tests which want to ensure they're only
60    * operating with backends they were designed for (and thus may not be able to
61    * enumerate entries for).
62    *
63    * To avoid conflicts, backends must not use this as a name.
64    *
65    * @since 0.4.0
66    */
67   public static string KEY_FILE_GROUP_ALL_OTHERS = "all-others";
68
69   /**
70    * Emitted when a backend has been added to the BackendStore.
71    *
72    * This will not be emitted until after {@link BackendStore.load_backends}
73    * has been called.
74    *
75    * {@link Backend}s referenced in this signal are also included in
76    * {@link BackendStore.enabled_backends}.
77    *
78    * @param backend the new {@link Backend}
79    */
80   public signal void backend_available (Backend backend);
81
82   /**
83    * The list of backends visible to this store which have not been explicitly
84    * disabled.
85    *
86    * This list will be empty before {@link BackendStore.load_backends} has been
87    * called.
88    *
89    * The backends in this list have been prepared and are ready to use.
90    *
91    * @since 0.5.1
92    */
93   public Map<string, Backend> enabled_backends
94     {
95       /* Return a read-only view of the map */
96       get { return this._prepared_backends_ro; }
97
98       private set {}
99     }
100
101   /**
102    * Whether {@link BackendStore.prepare} has successfully completed for this
103    * store.
104    *
105    * @since 0.3.0
106    */
107   public bool is_prepared
108     {
109       get { return this._is_prepared; }
110
111       private set {}
112     }
113
114   /**
115    * Create a new BackendStore.
116    */
117   public static BackendStore dup ()
118     {
119       if (BackendStore._instance == null)
120         {
121           /* use an intermediate variable to force a strong reference */
122           var new_instance = new BackendStore ();
123           BackendStore._instance = new_instance;
124
125           return new_instance;
126         }
127
128       return (!) BackendStore._instance;
129     }
130
131   private BackendStore ()
132     {
133       Object ();
134     }
135
136   construct
137     {
138       /* Treat this as a library init function */
139       var debug_no_colour = Environment.get_variable ("FOLKS_DEBUG_NO_COLOUR");
140       this._debug =
141           Debug.dup_with_flags (Environment.get_variable ("FOLKS_DEBUG"),
142               (debug_no_colour == null || debug_no_colour == "0"));
143
144       /* register the core debug messages */
145       this._debug._register_domain (G_LOG_DOMAIN);
146
147       this._debug.print_status.connect (this._debug_print_status);
148
149       this._modules = new HashMap<string,unowned Module> (str_hash, str_equal);
150       this._backend_hash = new HashMap<string,Backend> (str_hash, str_equal);
151       this._prepared_backends = new HashMap<string,Backend> (str_hash,
152           str_equal);
153       this._prepared_backends_ro = this._prepared_backends.read_only_view;
154     }
155
156   ~BackendStore ()
157     {
158       /* Finalize all the loaded modules that have finalize functions */
159       foreach (var module in this._modules.values)
160         {
161           void* func;
162           if (module.symbol ("module_finalize", out func))
163             {
164               ModuleFinalizeFunc module_finalize = (ModuleFinalizeFunc) func;
165               module_finalize (this);
166             }
167         }
168
169       /* Disconnect from the debug handler */
170       this._debug.print_status.disconnect (this._debug_print_status);
171
172       /* manually clear the singleton instance */
173       BackendStore._instance = null;
174     }
175
176   private void _debug_print_status (Debug debug)
177     {
178       const string domain = Debug.STATUS_LOG_DOMAIN;
179       const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
180
181       debug.print_heading (domain, level, "BackendStore (%p)", this);
182       debug.print_line (domain, level, "%u Backends:",
183           this._backend_hash.size);
184
185       debug.indent ();
186
187       foreach (var backend in this._backend_hash.values)
188         {
189           debug.print_heading (domain, level, "Backend (%p)", backend);
190           debug.print_key_value_pairs (domain, level,
191               "Ref. count", this.ref_count.to_string (),
192               "Name", backend.name,
193               "Prepared?", backend.is_prepared ? "yes" : "no",
194               "Quiescent?", backend.is_quiescent ? "yes" : "no"
195           );
196           debug.print_line (domain, level, "%u PersonaStores:",
197               backend.persona_stores.size);
198
199           debug.indent ();
200
201           foreach (var persona_store in backend.persona_stores.values)
202             {
203               string? trust_level = null;
204
205               switch (persona_store.trust_level)
206                 {
207                   case PersonaStoreTrust.NONE:
208                     trust_level = "none";
209                     break;
210                   case PersonaStoreTrust.PARTIAL:
211                     trust_level = "partial";
212                     break;
213                   case PersonaStoreTrust.FULL:
214                     trust_level = "full";
215                     break;
216                   default:
217                     assert_not_reached ();
218                 }
219
220               var writeable_props = string.joinv (",",
221                   persona_store.always_writeable_properties);
222
223               debug.print_heading (domain, level, "PersonaStore (%p)",
224                   persona_store);
225               debug.print_key_value_pairs (domain, level,
226                   "Ref. count", this.ref_count.to_string (),
227                   "ID", persona_store.id,
228                   "Prepared?", persona_store.is_prepared ? "yes" : "no",
229                   "Is primary store?",
230                   persona_store.is_primary_store ? "yes" : "no",
231                   "Always writeable properties", writeable_props,
232                   "Quiescent?", persona_store.is_quiescent ? "yes" : "no",
233                   "Trust level", trust_level,
234                   "Persona count", persona_store.personas.size.to_string ()
235               );
236             }
237
238           debug.unindent ();
239         }
240
241       debug.unindent ();
242
243       debug.print_line (domain, level, "");
244     }
245
246   /**
247    * Prepare the BackendStore for use.
248    *
249    * This must only ever be called before {@link BackendStore.load_backends} is
250    * called for the first time. If it isn't called explicitly,
251    * {@link BackendStore.load_backends} will call it.
252    *
253    * @since 0.3.0
254    */
255   public async void prepare ()
256     {
257       /* (re-)load the list of disabled backends */
258       yield this._load_disabled_backend_names ();
259
260       if (this._is_prepared == true)
261         return;
262       this._is_prepared = true;
263
264       this.notify_property ("is-prepared");
265     }
266
267   /**
268    * Find, load, and prepare all backends which are not disabled.
269    *
270    * Backends will be searched for in the path given by the `FOLKS_BACKEND_PATH`
271    * environment variable, if it's set. If it's not set, backends will be
272    * searched for in a path set at compilation time.
273    */
274   public async void load_backends () throws GLib.Error
275     {
276       assert (Module.supported());
277
278       yield this.prepare ();
279
280       /* unload backends that have been disabled since they were loaded */
281       foreach (var backend_existing in this._backend_hash.values)
282         {
283           yield this._backend_unload_if_needed (backend_existing);
284         }
285
286       string? _path = Environment.get_variable ("FOLKS_BACKEND_PATH");
287       string path;
288
289       if (_path == null)
290         {
291           path = BuildConf.BACKEND_DIR;
292
293           debug ("Using built-in backend dir '%s' (override with " +
294               "environment variable FOLKS_BACKEND_PATH)", path);
295         }
296       else
297         {
298           path = (!) _path;
299
300           debug ("Using environment variable FOLKS_BACKEND_PATH = " +
301               "'%s' to look for backends", path);
302         }
303
304       var modules = new HashMap<string, File> ();
305       var path_split = path.split (":");
306       foreach (unowned string subpath in path_split)
307         {
308           var file = File.new_for_path (subpath);
309
310           bool is_file;
311           bool is_dir;
312           yield this._get_file_info (file, out is_file, out is_dir);
313           if (is_file)
314             {
315               modules.set (subpath, file);
316             }
317           else if (is_dir)
318             {
319               var cur_modules = yield this._get_modules_from_dir (file);
320               if (cur_modules != null)
321                 {
322                   foreach (var entry in ((!) cur_modules).entries)
323                     {
324                       modules.set (entry.key, entry.value);
325                     }
326                 }
327             }
328           else
329             {
330               critical ("FOLKS_BACKEND_PATH component '%s' is not a regular " +
331                   "file or directory; ignoring...",
332                   subpath);
333               assert_not_reached ();
334             }
335         }
336
337       /* this will load any new modules found in the backends dir and will
338        * prepare and unprepare backends such that they match the state in the
339        * backend store key file */
340       foreach (var module in modules.values)
341         this._load_module_from_file (module);
342
343       /* this is populated indirectly from _load_module_from_file(), above */
344       foreach (var backend in this._backend_hash.values)
345         yield this._backend_load_if_needed (backend);
346     }
347
348   private async void _backend_load_if_needed (Backend backend)
349     {
350       if (this._backend_is_enabled (backend.name))
351         {
352           if (!this._prepared_backends.has_key (backend.name))
353             {
354               try
355                 {
356                   yield backend.prepare ();
357
358                   debug ("New backend '%s' prepared", backend.name);
359                   this._prepared_backends.set (backend.name, backend);
360                   this.backend_available (backend);
361                 }
362               catch (GLib.Error e)
363                 {
364                   /* Translators: the first parameter is a backend name, and the
365                    * second is an error message. */
366                   warning (_("Error preparing Backend '%s': %s"),
367                       backend.name, e.message);
368                 }
369             }
370         }
371     }
372
373   private async bool _backend_unload_if_needed (Backend backend)
374     {
375       var unloaded = false;
376
377       if (!this._backend_is_enabled (backend.name))
378         {
379           Backend? backend_existing = this._backend_hash.get (backend.name);
380           if (backend_existing != null)
381             {
382               try
383                 {
384                   yield ((!) backend_existing).unprepare ();
385                 }
386               catch (GLib.Error e)
387                 {
388                   warning ("Error unpreparing Backend '%s': %s", backend.name,
389                       e.message);
390                 }
391
392               this._prepared_backends.unset (((!) backend_existing).name);
393
394               unloaded = true;
395             }
396         }
397
398       return unloaded;
399     }
400
401   /**
402    * Add a new {@link Backend} to the BackendStore.
403    *
404    * @param backend the {@link Backend} to add
405    */
406   public void add_backend (Backend backend)
407     {
408       /* Purge any other backend with the same name; re-add if enabled */
409       Backend? backend_existing = this._backend_hash.get (backend.name);
410       if (backend_existing != null && backend_existing != backend)
411         {
412           ((!) backend_existing).unprepare ();
413           this._prepared_backends.unset (((!) backend_existing).name);
414         }
415
416       this._debug._register_domain (backend.name);
417
418       this._backend_hash.set (backend.name, backend);
419     }
420
421   private bool _backend_is_enabled (string name)
422     {
423       var all_others_enabled = true;
424       try
425         {
426           all_others_enabled = this._backends_key_file.get_boolean (
427               this.KEY_FILE_GROUP_ALL_OTHERS, "enabled");
428         }
429       catch (KeyFileError e)
430         {
431           if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
432               !(e is KeyFileError.KEY_NOT_FOUND))
433             {
434               warning ("Couldn't determine whether to enable or disable " +
435                   "backends not listed in backend key file. Defaulting to %s.",
436                   all_others_enabled ? "enabled" : "disabled");
437             }
438           else
439             {
440               debug ("No catch-all entry in the backend key file. %s " +
441                   "unlisted backends.",
442                   all_others_enabled ? "Enabling" : "Disabling");
443             }
444
445           /* fall back to the default in case of any level of failure */
446         }
447
448       var enabled = true;
449       try
450         {
451           enabled = this._backends_key_file.get_boolean (name, "enabled");
452         }
453       catch (KeyFileError e)
454         {
455           /* if there's no entry for this backend, use the default set above */
456           if ((e is KeyFileError.GROUP_NOT_FOUND) ||
457               (e is KeyFileError.KEY_NOT_FOUND))
458             {
459               debug ("Found no entry for backend '%s'.enabled in backend " +
460                   "keyfile. %s according to '%s' setting.",
461                   name,
462                   all_others_enabled ? "Enabling" : "Disabling",
463                   this.KEY_FILE_GROUP_ALL_OTHERS);
464               enabled = all_others_enabled;
465             }
466           else if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
467               !(e is KeyFileError.KEY_NOT_FOUND))
468             {
469               warning ("Couldn't check enabled state of backend '%s': %s\n" +
470                   "Disabling backend.",
471                   name, e.message);
472               enabled = false;
473             }
474         }
475
476       return enabled;
477     }
478
479   /**
480    * Get a backend from the store by name. If a backend is returned, its
481    * reference count is increased.
482    *
483    * @param name the backend name to retrieve
484    * @return the backend, or `null` if none could be found
485    *
486    * @since 0.3.5
487    */
488   public Backend? dup_backend_by_name (string name)
489     {
490       return this._backend_hash.get (name);
491     }
492
493   /**
494    * List the currently loaded backends.
495    *
496    * @return a list of the backends currently in the BackendStore
497    */
498   public Collection<Backend> list_backends ()
499     {
500       return this._backend_hash.values.read_only_view;
501     }
502
503   /**
504    * Enable a backend.
505    *
506    * Mark a backend as enabled, such that the BackendStore will always attempt
507    * to load it when {@link BackendStore.load_backends} is called. This will
508    * not load the backend if it's not currently loaded.
509    *
510    * @param name the name of the backend to enable
511    * @since 0.3.2
512    */
513   public async void enable_backend (string name)
514     {
515       this._backends_key_file.set_boolean (name, "enabled", true);
516       yield this._save_key_file ();
517     }
518
519   /**
520    * Disable a backend.
521    *
522    * Mark a backend as disabled, such that it won't be loaded even when the
523    * client application is restarted. This will not remove the backend if it's
524    * already loaded.
525    *
526    * @param name the name of the backend to disable
527    * @since 0.3.2
528    */
529   public async void disable_backend (string name)
530     {
531       this._backends_key_file.set_boolean (name, "enabled", false);
532       yield this._save_key_file ();
533     }
534
535   private async HashMap<string, File>? _get_modules_from_dir (File dir)
536     {
537       debug ("Searching for modules in folder '%s' ..", dir.get_path ());
538
539       var attributes =
540           FileAttribute.STANDARD_NAME + "," +
541           FileAttribute.STANDARD_TYPE + "," +
542           FileAttribute.STANDARD_IS_SYMLINK + "," +
543           FileAttribute.STANDARD_CONTENT_TYPE;
544
545       GLib.List<FileInfo> infos;
546       try
547         {
548           FileEnumerator enumerator =
549             yield dir.enumerate_children_async (attributes,
550                 FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
551
552           infos = yield enumerator.next_files_async (int.MAX,
553               Priority.DEFAULT, null);
554         }
555       catch (Error error)
556         {
557           /* Translators: the first parameter is a folder path and the second
558            * is an error message. */
559           critical (_("Error listing contents of folder '%s': %s"),
560               dir.get_path (), error.message);
561
562           return null;
563         }
564
565       var modules_final = new HashMap<string, File> (str_hash, str_equal);
566
567       foreach (var info in infos)
568         {
569           var file = dir.get_child (info.get_name ());
570           var file_type = info.get_file_type ();
571           unowned string content_type = info.get_content_type ();
572           /* don't load the library multiple times for its various symlink
573            * aliases */
574           var is_symlink = info.get_is_symlink ();
575
576           string? mime = ContentType.get_mime_type (content_type);
577
578           if (file_type == FileType.DIRECTORY)
579             {
580               var modules = yield this._get_modules_from_dir (file);
581               if (modules != null)
582                 {
583                   foreach (var entry in ((!) modules).entries)
584                     {
585                       modules_final.set (entry.key, entry.value);
586                     }
587                 }
588             }
589           else if (mime == "application/x-sharedlib" && !is_symlink)
590             {
591               var path = file.get_path ();
592               if (path != null)
593                 {
594                   modules_final.set ((!) path, file);
595                 }
596             }
597           else if (mime == null)
598             {
599               warning (
600                   /* Translators: the parameter is a filename. */
601                   _("The content type of '%s' could not be determined. Have you installed shared-mime-info?"),
602                   file.get_path ());
603             }
604         }
605
606       debug ("Finished searching for modules in folder '%s'",
607           dir.get_path ());
608
609       return modules_final;
610     }
611
612   private void _load_module_from_file (File file)
613     {
614       var _file_path = file.get_path ();
615       if (_file_path == null)
616         {
617           return;
618         }
619       var file_path = (!) _file_path;
620
621       if (this._modules.has_key (file_path))
622         return;
623
624       var _module = Module.open (file_path, ModuleFlags.BIND_LOCAL);
625       if (_module == null)
626         {
627           /* Translators: the first parameter is a filename and the second is an
628            * error message. */
629           warning (_("Failed to load module from path '%s': %s"),
630                     file_path, Module.error ());
631
632           return;
633         }
634       unowned Module module = (!) _module;
635
636       void* function;
637
638       /* this causes the module to call add_backend() for its backends (adding
639        * them to the backend hash); any backends that already existed will be
640        * removed if they've since been disabled */
641       if (!module.symbol("module_init", out function))
642         {
643           /* Translators: the first parameter is a function name, the second is
644            * a filename and the third is an error message. */
645           warning (_("Failed to find entry point function '%s' in '%s': %s"),
646                     "module_init",
647                     file_path,
648                     Module.error ());
649
650           return;
651         }
652
653       ModuleInitFunc module_init = (ModuleInitFunc) function;
654       assert (module_init != null);
655
656       this._modules.set (file_path, module);
657
658       /* We don't want our modules to ever unload */
659       module.make_resident ();
660
661       module_init (this);
662
663       debug ("Loaded module source: '%s'", module.name ());
664     }
665
666   private async static void _get_file_info (File file,
667       out bool is_file,
668       out bool is_dir)
669     {
670       FileInfo file_info;
671       is_file = false;
672       is_dir = false;
673
674       try
675         {
676           /* Query for the MIME type; if the file doesn't exist, we'll get an
677            * appropriate error back, so this also checks for existence. */
678           file_info = yield file.query_info_async (FileAttribute.STANDARD_TYPE,
679               FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
680         }
681       catch (Error error)
682         {
683           if (error is IOError.NOT_FOUND)
684             {
685               /* Translators: the parameter is a filename. */
686               critical (_("File or directory '%s' does not exist."),
687                   file.get_path ());
688             }
689           else
690             {
691               /* Translators: the parameter is a filename. */
692               critical (_("Failed to get content type for '%s'."),
693                   file.get_path ());
694             }
695
696           return;
697         }
698
699       is_file = (file_info.get_file_type () == FileType.REGULAR);
700       is_dir = (file_info.get_file_type () == FileType.DIRECTORY);
701     }
702
703   private async void _load_disabled_backend_names ()
704     {
705       File file;
706       unowned string? path = Environment.get_variable (
707           "FOLKS_BACKEND_STORE_KEY_FILE_PATH");
708       if (path == null)
709         {
710           file = File.new_for_path (Environment.get_user_data_dir ());
711           file = file.get_child ("folks");
712           file = file.get_child ("backends.ini");
713
714           debug ("Using built-in backends key file '%s' (override with " +
715               "environment variable FOLKS_BACKEND_STORE_KEY_FILE_PATH)",
716               file.get_path ());
717         }
718       else
719         {
720           file = File.new_for_path ((!) path);
721           debug ("Using environment variable " +
722               "FOLKS_BACKEND_STORE_KEY_FILE_PATH = '%s' to load the backends " +
723               "key file.", (!) path);
724         }
725
726       this._config_file = file;
727
728       /* Load the disabled backends file */
729       this._backends_key_file = new GLib.KeyFile ();
730       try
731         {
732           uint8 *contents = null;
733
734           yield file.load_contents_async (null, out contents, null);
735           var contents_s = (string) contents;
736
737           if (contents_s.length > 0)
738             {
739               this._backends_key_file.load_from_data (contents_s,
740                   contents_s.length, KeyFileFlags.KEEP_COMMENTS);
741             }
742         }
743       catch (Error e1)
744         {
745           if (!(e1 is IOError.NOT_FOUND))
746             {
747               warning ("The backends key file '%s' could not be loaded: %s",
748                   file.get_path (), e1.message);
749               return;
750             }
751         }
752     }
753
754   private async void _save_key_file ()
755     {
756       var key_file_data = this._backends_key_file.to_data ();
757
758       debug ("Saving backend key file '%s'.", this._config_file.get_path ());
759
760       try
761         {
762           /* Note: We have to use key_file_data.size () here to get its length
763            * in _bytes_ rather than _characters_. bgo#628930.
764            * In Vala >= 0.11, string.size() has been deprecated in favour of
765            * string.length (which now returns the byte length, whereas in
766            * Vala <= 0.10, it returned the character length). FIXME: We need to
767            * take this into account until we depend explicitly on
768            * Vala >= 0.11. */
769           yield this._config_file.replace_contents_async (key_file_data.data,
770               null, false, FileCreateFlags.PRIVATE,
771               null, null);
772         }
773       catch (Error e)
774         {
775           warning ("Could not write updated backend key file '%s': %s",
776               this._config_file.get_path (), e.message);
777         }
778     }
779 }