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
25using Gee;
26using GLib;
27
28extern 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 */
38public 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  /* if null, all backends are allowed */
47  private SmallSet<string>? _backends_allowed;
48  /* if null, no backends are disabled */
49  private SmallSet<string>? _backends_disabled;
50  private HashMap<string, Backend> _prepared_backends;
51  private Map<string, Backend> _prepared_backends_ro;
52  private File _config_file;
53  private GLib.KeyFile _backends_key_file;
54  private HashMap<string,unowned Module> _modules;
55  private static weak BackendStore? _instance = null;
56  private bool _is_prepared = false;
57  private Debug _debug;
58
59  /**
60   * This keyword in the keyfile acts as a wildcard for all backends not already
61   * listed in the same file.
62   *
63   * This is particularly useful for tests which want to ensure they're only
64   * operating with backends they were designed for (and thus may not be able to
65   * enumerate entries for).
66   *
67   * To avoid conflicts, backends must not use this as a name.
68   *
69   * @since 0.4.0
70   */
71  public static string KEY_FILE_GROUP_ALL_OTHERS = "all-others";
72
73  /**
74   * Emitted when a backend has been added to the BackendStore.
75   *
76   * This will not be emitted until after {@link BackendStore.load_backends}
77   * has been called.
78   *
79   * {@link Backend}s referenced in this signal are also included in
80   * {@link BackendStore.enabled_backends}.
81   *
82   * @param backend the new {@link Backend}
83   */
84  public signal void backend_available (Backend backend);
85
86  /**
87   * The list of backends visible to this store which have not been explicitly
88   * disabled.
89   *
90   * This list will be empty before {@link BackendStore.load_backends} has been
91   * called.
92   *
93   * The backends in this list have been prepared and are ready to use.
94   *
95   * @since 0.5.1
96   */
97  public Map<string, Backend> enabled_backends
98    {
99      /* Return a read-only view of the map */
100      get { return this._prepared_backends_ro; }
101
102      private set {}
103    }
104
105  /**
106   * Whether {@link BackendStore.prepare} has successfully completed for this
107   * store.
108   *
109   * @since 0.3.0
110   */
111  public bool is_prepared
112    {
113      get { return this._is_prepared; }
114
115      private set {}
116    }
117
118  /**
119   * Create a new BackendStore.
120   */
121  public static BackendStore dup ()
122    {
123      if (BackendStore._instance == null)
124        {
125          /* use an intermediate variable to force a strong reference */
126          var new_instance = new BackendStore ();
127          BackendStore._instance = new_instance;
128
129          return new_instance;
130        }
131
132      return (!) BackendStore._instance;
133    }
134
135  private BackendStore ()
136    {
137      Object ();
138    }
139
140  construct
141    {
142      /* Treat this as a library init function */
143      var debug_no_colour = Environment.get_variable ("FOLKS_DEBUG_NO_COLOUR");
144      var debug_no_color = Environment.get_variable ("FOLKS_DEBUG_NO_COLOR");
145      this._debug =
146          Debug.dup_with_flags (Environment.get_variable ("G_MESSAGES_DEBUG"),
147              (debug_no_colour == null || debug_no_colour == "0") &&
148              (debug_no_color == null || debug_no_color == "0"));
149
150      /* register the core debug messages */
151      this._debug._register_domain (G_LOG_DOMAIN);
152
153      this._debug.print_status.connect (this._debug_print_status);
154
155      this._modules = new HashMap<string,unowned Module> ();
156      this._backend_hash = new HashMap<string,Backend> ();
157      this._prepared_backends = new HashMap<string,Backend> ();
158      this._prepared_backends_ro = this._prepared_backends.read_only_view;
159    }
160
161  ~BackendStore ()
162    {
163      /* Unprepare all existing backends. */
164      var iter = this._prepared_backends.map_iterator ();
165      while (iter.next () == true)
166        {
167          var backend = iter.get_value ();
168          backend.unprepare.begin ();
169        }
170
171      this._prepared_backends.clear ();
172
173      /* Finalize all the loaded modules that have finalize functions */
174      foreach (var module in this._modules.values)
175        {
176          void* func;
177          if (module.symbol ("module_finalize", out func))
178            {
179              ModuleFinalizeFunc module_finalize = (ModuleFinalizeFunc) func;
180              module_finalize (this);
181            }
182        }
183
184      this._modules.clear ();
185
186      /* Disconnect from the debug handler */
187      this._debug.print_status.disconnect (this._debug_print_status);
188
189      /* manually clear the singleton instance */
190      BackendStore._instance = null;
191    }
192
193  private void _debug_print_status (Debug debug)
194    {
195      const string domain = Debug.STATUS_LOG_DOMAIN;
196      const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
197
198      debug.print_heading (domain, level, "BackendStore (%p)", this);
199      debug.print_line (domain, level, "%u Backends:",
200          this._backend_hash.size);
201
202      debug.indent ();
203
204      foreach (var backend in this._backend_hash.values)
205        {
206          debug.print_heading (domain, level, "Backend (%p)", backend);
207          debug.print_key_value_pairs (domain, level,
208              "Ref. count", this.ref_count.to_string (),
209              "Name", backend.name,
210              "Prepared?", backend.is_prepared ? "yes" : "no",
211              "Quiescent?", backend.is_quiescent ? "yes" : "no"
212          );
213          debug.print_line (domain, level, "%u PersonaStores:",
214              backend.persona_stores.size);
215
216          debug.indent ();
217
218          foreach (var persona_store in backend.persona_stores.values)
219            {
220              string? trust_level = null;
221
222              switch (persona_store.trust_level)
223                {
224                  case PersonaStoreTrust.NONE:
225                    trust_level = "none";
226                    break;
227                  case PersonaStoreTrust.PARTIAL:
228                    trust_level = "partial";
229                    break;
230                  case PersonaStoreTrust.FULL:
231                    trust_level = "full";
232                    break;
233                  default:
234                    assert_not_reached ();
235                }
236
237              var writeable_props = string.joinv (",",
238                  persona_store.always_writeable_properties);
239
240              debug.print_heading (domain, level, "PersonaStore (%p)",
241                  persona_store);
242              debug.print_key_value_pairs (domain, level,
243                  "Ref. count", this.ref_count.to_string (),
244                  "ID", persona_store.id,
245                  "Prepared?", persona_store.is_prepared ? "yes" : "no",
246                  "Is primary store?",
247                  persona_store.is_primary_store ? "yes" : "no",
248                  "Always writeable properties", writeable_props,
249                  "Quiescent?", persona_store.is_quiescent ? "yes" : "no",
250                  "Trust level", trust_level,
251                  "Persona count", persona_store.personas.size.to_string ()
252              );
253            }
254
255          debug.unindent ();
256        }
257
258      debug.unindent ();
259
260      /* Finish with a blank line. The format string must be non-empty. */
261      debug.print_line (domain, level, "%s", "");
262    }
263
264  /**
265   * Prepare the BackendStore for use.
266   *
267   * This must only ever be called before {@link BackendStore.load_backends} is
268   * called for the first time. If it isn't called explicitly,
269   * {@link BackendStore.load_backends} will call it.
270   *
271   * This method is safe to call multiple times concurrently (e.g. an
272   * asynchronous call may begin between a subsequent asynchronous call
273   * beginning and finishing).
274   *
275   * @since 0.3.0
276   */
277  public async void prepare ()
278    {
279      Internal.profiling_start ("preparing BackendStore");
280
281      /* (re-)load the list of disabled backends */
282      yield this._load_disabled_backend_names ();
283
284      if (this._is_prepared == false)
285        {
286          this._is_prepared = true;
287          this.notify_property ("is-prepared");
288        }
289
290      Internal.profiling_end ("preparing BackendStore");
291    }
292
293  /**
294   * Find, load, and prepare all backends which are not disabled.
295   *
296   * Backends will be searched for in the path given by the
297   * ``FOLKS_BACKEND_PATH`` environment variable, if it's set. If it's not set,
298   * backends will be searched for in a path set at compilation time.
299   *
300   * This method is not safe to call multiple times concurrently.
301   *
302   * @throws GLib.Error currently unused
303   */
304  public async void load_backends () throws GLib.Error
305    {
306      assert (Module.supported());
307
308      Internal.profiling_start ("loading backends in BackendStore");
309
310      yield this.prepare ();
311
312      /* unload backends that have been disabled since they were loaded */
313      foreach (var backend_existing in this._backend_hash.values)
314        {
315          yield this._backend_unload_if_needed (backend_existing);
316        }
317
318      Internal.profiling_point ("unloaded backends in BackendStore");
319
320      string? _path = Environment.get_variable ("FOLKS_BACKEND_PATH");
321      string path;
322
323      if (_path == null)
324        {
325          path = BuildConf.BACKEND_DIR;
326
327          debug ("Using built-in backend dir '%s' (override with " +
328              "environment variable FOLKS_BACKEND_PATH)", path);
329        }
330      else
331        {
332          path = (!) _path;
333
334          debug ("Using environment variable FOLKS_BACKEND_PATH = " +
335              "'%s' to look for backends", path);
336        }
337
338      var modules = new HashMap<string, File> ();
339      var path_split = path.split (":");
340      foreach (unowned string subpath in path_split)
341        {
342          var file = File.new_for_path (subpath);
343
344          bool is_file;
345          bool is_dir;
346          yield BackendStore._get_file_info (file, out is_file, out is_dir);
347          if (is_file)
348            {
349              modules.set (subpath, file);
350            }
351          else if (is_dir)
352            {
353              var cur_modules = yield this._get_modules_from_dir (file);
354              if (cur_modules != null)
355                {
356                  foreach (var entry in ((!) cur_modules).entries)
357                    {
358                      modules.set (entry.key, entry.value);
359                    }
360                }
361            }
362          else
363            {
364              critical ("FOLKS_BACKEND_PATH component '%s' is not a regular " +
365                  "file or directory; ignoring...",
366                  subpath);
367              assert_not_reached ();
368            }
369        }
370
371      Internal.profiling_point ("found modules in BackendStore");
372
373      /* this will load any new modules found in the backends dir and will
374       * prepare and unprepare backends such that they match the state in the
375       * backend store key file */
376      foreach (var module in modules.values)
377        this._load_module_from_file (module);
378
379      Internal.profiling_point ("loaded modules in BackendStore");
380
381      /* this is populated indirectly from _load_module_from_file(), above */
382      var backends_remaining = 1;
383      foreach (var backend in this._backend_hash.values)
384        {
385          backends_remaining++;
386          this._backend_load_if_needed.begin (backend, (o, r) =>
387            {
388              this._backend_load_if_needed.end (r);
389              backends_remaining--;
390
391              if (backends_remaining == 0)
392                {
393                  this.load_backends.callback ();
394                }
395            });
396        }
397      backends_remaining--;
398      if (backends_remaining > 0)
399        {
400          yield;
401        }
402
403      Internal.profiling_end ("loading backends in BackendStore");
404    }
405
406  /* This method is not safe to call multiple times concurrently, since there's
407   * a race in updating this._prepared_backends. */
408  private async void _backend_load_if_needed (Backend backend)
409    {
410      if (this._backend_is_enabled (backend.name))
411        {
412          if (!this._prepared_backends.has_key (backend.name))
413            {
414              try
415                {
416                  yield backend.prepare ();
417
418                  debug ("New backend '%s' prepared", backend.name);
419                  this._prepared_backends.set (backend.name, backend);
420                  this.backend_available (backend);
421                }
422              catch (GLib.Error e)
423                {
424                  if (e is DBusError.SERVICE_UNKNOWN)
425                    {
426                      /* Don’t warn if a D-Bus service is unknown; it probably
427                       * means the backend is deliberately not running and the
428                       * user is running folks from git, so hasn’t appropriately
429                       * enabled/disabled backends from building. */
430                      debug ("Error preparing Backend '%s': %s",
431                          backend.name, e.message);
432                    }
433                  else
434                    {
435                      warning ("Error preparing Backend '%s': %s",
436                          backend.name, e.message);
437                    }
438                }
439            }
440        }
441    }
442
443  /* This method is not safe to call multiple times concurrently, since there's
444   * a race in updating this._prepared_backends. */
445  private async bool _backend_unload_if_needed (Backend backend)
446    {
447      var unloaded = false;
448
449      if (!this._backend_is_enabled (backend.name))
450        {
451          Backend? backend_existing = this._backend_hash.get (backend.name);
452          if (backend_existing != null)
453            {
454              try
455                {
456                  yield ((!) backend_existing).unprepare ();
457                }
458              catch (GLib.Error e)
459                {
460                  warning ("Error unpreparing Backend '%s': %s", backend.name,
461                      e.message);
462                }
463
464              this._prepared_backends.unset (((!) backend_existing).name);
465
466              unloaded = true;
467            }
468        }
469
470      return unloaded;
471    }
472
473  /**
474   * Add a new {@link Backend} to the BackendStore.
475   *
476   * @param backend the {@link Backend} to add
477   */
478  public void add_backend (Backend backend)
479    {
480      /* Purge any other backend with the same name; re-add if enabled */
481      Backend? backend_existing = this._backend_hash.get (backend.name);
482      if (backend_existing != null && backend_existing != backend)
483        {
484          ((!) backend_existing).unprepare.begin ();
485          this._prepared_backends.unset (((!) backend_existing).name);
486        }
487
488      this._debug._register_domain (backend.name);
489
490      this._backend_hash.set (backend.name, backend);
491    }
492
493  private bool _backend_is_enabled (string name)
494    {
495      var all_others_enabled = true;
496
497      if (this._backends_allowed != null &&
498          !(name in (!) this._backends_allowed))
499        return false;
500
501      if (this._backends_disabled != null &&
502          name in (!) this._backends_disabled)
503        return false;
504
505      try
506        {
507          all_others_enabled = this._backends_key_file.get_boolean (
508              BackendStore.KEY_FILE_GROUP_ALL_OTHERS, "enabled");
509        }
510      catch (KeyFileError e)
511        {
512          if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
513              !(e is KeyFileError.KEY_NOT_FOUND))
514            {
515              warning ("Couldn't determine whether to enable or disable " +
516                  "backends not listed in backend key file. Defaulting to %s.",
517                  all_others_enabled ? "enabled" : "disabled");
518            }
519          else
520            {
521              debug ("No catch-all entry in the backend key file. %s " +
522                  "unlisted backends.",
523                  all_others_enabled ? "Enabling" : "Disabling");
524            }
525
526          /* fall back to the default in case of any level of failure */
527        }
528
529      var enabled = true;
530      try
531        {
532          enabled = this._backends_key_file.get_boolean (name, "enabled");
533        }
534      catch (KeyFileError e)
535        {
536          /* if there's no entry for this backend, use the default set above */
537          if ((e is KeyFileError.GROUP_NOT_FOUND) ||
538              (e is KeyFileError.KEY_NOT_FOUND))
539            {
540              debug ("Found no entry for backend '%s'.enabled in backend " +
541                  "keyfile. %s according to '%s' setting.",
542                  name,
543                  all_others_enabled ? "Enabling" : "Disabling",
544                  BackendStore.KEY_FILE_GROUP_ALL_OTHERS);
545              enabled = all_others_enabled;
546            }
547          else if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
548              !(e is KeyFileError.KEY_NOT_FOUND))
549            {
550              warning ("Couldn't check enabled state of backend '%s': %s\n" +
551                  "Disabling backend.",
552                  name, e.message);
553              enabled = false;
554            }
555        }
556
557      return enabled;
558    }
559
560  /**
561   * Get a backend from the store by name. If a backend is returned, its
562   * reference count is increased.
563   *
564   * @param name the backend name to retrieve
565   * @return the backend, or ``null`` if none could be found
566   *
567   * @since 0.3.5
568   */
569  public Backend? dup_backend_by_name (string name)
570    {
571      return this._backend_hash.get (name);
572    }
573
574  /**
575   * List the currently loaded backends.
576   *
577   * @return a list of the backends currently in the BackendStore
578   */
579  public Collection<Backend> list_backends ()
580    {
581      return this._backend_hash.values.read_only_view;
582    }
583
584  /**
585   * Enable a backend.
586   *
587   * Mark a backend as enabled, such that the BackendStore will always attempt
588   * to load it when {@link BackendStore.load_backends} is called. This will
589   * not load the backend if it's not currently loaded.
590   *
591   * This method is safe to call multiple times concurrently (e.g. an
592   * asynchronous call may begin after a previous asynchronous call for the same
593   * backend name has begun and before it has finished).
594   *
595   * If the backend is disallowed by the FOLKS_BACKENDS_ALLOWED
596   * and/or FOLKS_BACKENDS_DISABLED environment variables, this method
597   * will store the fact that it should be enabled in future, but will
598   * not enable it during this application run.
599   *
600   * @param name the name of the backend to enable
601   * @since 0.3.2
602   */
603  public async void enable_backend (string name)
604    {
605      this._backends_key_file.set_boolean (name, "enabled", true);
606      yield this._save_key_file ();
607    }
608
609  /**
610   * Disable a backend.
611   *
612   * Mark a backend as disabled, such that it won't be loaded even when the
613   * client application is restarted. This will not remove the backend if it's
614   * already loaded.
615   *
616   * This method is safe to call multiple times concurrently (e.g. an
617   * asynchronous call may begin after a previous asynchronous call for the same
618   * backend name has begun and before it has finished).
619   *
620   * @param name the name of the backend to disable
621   * @since 0.3.2
622   */
623  public async void disable_backend (string name)
624    {
625      this._backends_key_file.set_boolean (name, "enabled", false);
626      yield this._save_key_file ();
627    }
628
629  /* This method is safe to call multiple times concurrently. */
630  private async HashMap<string, File>? _get_modules_from_dir (File dir)
631    {
632      debug ("Searching for modules in folder '%s' ..", dir.get_path ());
633
634      var attributes =
635          FileAttribute.STANDARD_NAME + "," +
636          FileAttribute.STANDARD_TYPE + "," +
637          FileAttribute.STANDARD_IS_SYMLINK + "," +
638          FileAttribute.STANDARD_SYMLINK_TARGET + "," +
639          FileAttribute.STANDARD_CONTENT_TYPE;
640
641      GLib.List<FileInfo> infos;
642      try
643        {
644          FileEnumerator enumerator =
645            yield dir.enumerate_children_async (attributes,
646                FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
647
648          infos = yield enumerator.next_files_async (int.MAX,
649              Priority.DEFAULT, null);
650        }
651      catch (Error error)
652        {
653          /* Translators: the first parameter is a folder path and the second
654           * is an error message. */
655          critical (_("Error listing contents of folder ‘%s’: %s"),
656              dir.get_path (), error.message);
657
658          return null;
659        }
660
661      var modules_final = new HashMap<string, File> ();
662
663      string? _path = Environment.get_variable ("FOLKS_BACKEND_PATH");
664      foreach (var info in infos)
665        {
666          var file = dir.get_child (info.get_name ());
667
668          /* Handle symlinks by dereferencing them. If we look at two symlinks
669           * with the same target, we don’t end up loading that backend twice
670           * due to hashing the backend’s absolute path in @modules_final.
671           *
672           * We can’t just ignore symlinks due to the way Tinycorelinux installs
673           * software: /usr/local/lib/folks/41/backends/bluez/bluez.so is a
674           * symlink to
675           * /tmp/tcloop/folks/usr/local/lib/folks/41/backends/bluez/bluez.so,
676           * a loopback squashfs mount of the folks file system. Ignoring
677           * symlinks means we would never load backends in that environment. */
678          if (info.get_is_symlink ())
679            {
680              debug ("Handling symlink ‘%s’ to ‘%s’.",
681                  file.get_path (), info.get_symlink_target ());
682
683              var old_file = file;
684              file = dir.resolve_relative_path (info.get_symlink_target ());
685
686              try
687                {
688                  info =
689                      yield file.query_info_async (attributes,
690                          FileQueryInfoFlags.NONE);
691                }
692              catch (Error error)
693                {
694                  /* Translators: the first parameter is a folder path and the second
695                   * is an error message. */
696                  warning (_("Error querying info for target ‘%s’ of symlink ‘%s’: %s"),
697                      file.get_path (), old_file.get_path (), error.message);
698
699                  continue;
700                }
701            }
702
703          /* Handle proper files. */
704          var file_type = info.get_file_type ();
705          unowned string content_type = info.get_content_type ();
706
707          string? mime = ContentType.get_mime_type (content_type);
708
709          if (file_type == FileType.DIRECTORY)
710            {
711              var modules = yield this._get_modules_from_dir (file);
712              if (modules != null)
713                {
714                  foreach (var entry in ((!) modules).entries)
715                    {
716                      modules_final.set (entry.key, entry.value);
717                    }
718                }
719            }
720          else if (mime == "application/x-sharedlib")
721            {
722              var path = file.get_path ();
723              if (path != null)
724                {
725                  modules_final.set ((!) path, file);
726                }
727            }
728          else if (mime == null)
729            {
730              warning (
731                  "The content type of '%s' could not be determined. Have you installed shared-mime-info?",
732                  file.get_path ());
733            }
734          /*
735           * We should have only .la .so and sub-directories, except if FOLKS_BACKEND_PATH is set.
736           * Then we will run into all kinds of files.
737           */
738          else if (_path == null &&
739                   mime != "application/x-sharedlib" &&
740                   mime != "application/x-shared-library-la" &&
741                   mime != "inode/directory")
742            {
743              warning ("The content type of '%s' appears to be '%s' which looks suspicious. Have you installed shared-mime-info?", file.get_path (), mime);
744            }
745        }
746
747      debug ("Finished searching for modules in folder '%s'",
748          dir.get_path ());
749
750      return modules_final;
751    }
752
753  private void _load_module_from_file (File file)
754    {
755      var _file_path = file.get_path ();
756      if (_file_path == null)
757        {
758          return;
759        }
760      var file_path = (!) _file_path;
761
762      if (this._modules.has_key (file_path))
763        return;
764
765      var _module = Module.open (file_path, ModuleFlags.BIND_LOCAL);
766      if (_module == null)
767        {
768          warning ("Failed to load module from path '%s': %s",
769                    file_path, Module.error ());
770
771          return;
772        }
773      unowned Module module = (!) _module;
774
775      void* function;
776
777      /* this causes the module to call add_backend() for its backends (adding
778       * them to the backend hash); any backends that already existed will be
779       * removed if they've since been disabled */
780      if (!module.symbol("module_init", out function))
781        {
782          warning ("Failed to find entry point function '%s' in '%s': %s",
783                    "module_init",
784                    file_path,
785                    Module.error ());
786
787          return;
788        }
789
790      ModuleInitFunc module_init = (ModuleInitFunc) function;
791      assert (module_init != null);
792
793      this._modules.set (file_path, module);
794
795      /* We don't want our modules to ever unload */
796      module.make_resident ();
797
798      module_init (this);
799
800      debug ("Loaded module source: '%s'", module.name ());
801    }
802
803  /* This method is safe to call multiple times concurrently. */
804  private async static void _get_file_info (File file,
805      out bool is_file,
806      out bool is_dir)
807    {
808      FileInfo file_info;
809      is_file = false;
810      is_dir = false;
811
812      try
813        {
814          /* Query for the MIME type; if the file doesn't exist, we'll get an
815           * appropriate error back, so this also checks for existence. */
816          file_info = yield file.query_info_async (FileAttribute.STANDARD_TYPE,
817              FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
818        }
819      catch (Error error)
820        {
821          if (error is IOError.NOT_FOUND)
822            {
823              /* Translators: the parameter is a filename. */
824              critical (_("File or directory ‘%s’ does not exist."),
825                  file.get_path ());
826            }
827          else
828            {
829              /* Translators: the parameter is a filename. */
830              critical (_("Failed to get content type for ‘%s’."),
831                  file.get_path ());
832            }
833
834          return;
835        }
836
837      is_file = (file_info.get_file_type () == FileType.REGULAR);
838      is_dir = (file_info.get_file_type () == FileType.DIRECTORY);
839    }
840
841  /* This method is safe to call multiple times concurrently. */
842  private async void _load_disabled_backend_names ()
843    {
844      /* If set, this is a list of allowed backends. No others can be enabled,
845       * even if the keyfile says they ought to be.
846       * The default is equivalent to "all", which allows any backend.
847       *
848       * Regression tests and benchmarks can use this to restrict themselves
849       * to a small set of backends for which they have done the necessary
850       * setup/configuration/sandboxing. */
851      var envvar = Environment.get_variable ("FOLKS_BACKENDS_ALLOWED");
852
853      if (envvar != null)
854        {
855          /* Allow space, comma or colon separation, consistent with
856           * g_parse_debug_string(). */
857          var tokens = envvar.split_set (" ,:");
858
859          this._backends_allowed = new SmallSet<string> ();
860
861          foreach (unowned string s in tokens)
862            {
863              if (s == "all")
864                {
865                  this._backends_allowed = null;
866                  break;
867                }
868
869              if (s != "")
870                this._backends_allowed.add (s);
871            }
872
873          if (this._backends_allowed != null)
874            {
875              debug ("Backends limited by FOLKS_BACKENDS_ALLOWED:");
876
877              foreach (unowned string s in tokens)
878                debug ("Backend '%s' is allowed", s);
879
880              debug ("All other backends disabled by FOLKS_BACKENDS_ALLOWED");
881            }
882        }
883
884      /* If set, this is a list of disallowed backends.
885       * They are not enabled, even if the keyfile says they ought to be. */
886      envvar = Environment.get_variable ("FOLKS_BACKENDS_DISABLED");
887
888      if (envvar != null)
889        {
890          var tokens = envvar.split_set (" ,:");
891
892          this._backends_disabled = new SmallSet<string> ();
893
894          foreach (unowned string s in tokens)
895            {
896              if (s != "")
897                {
898                  debug ("Backend '%s' disabled by FOLKS_BACKENDS_DISABLED", s);
899                  this._backends_disabled.add (s);
900                }
901            }
902        }
903
904      File file;
905      unowned string? path = Environment.get_variable (
906          "FOLKS_BACKEND_STORE_KEY_FILE_PATH");
907      if (path == null)
908        {
909          file = File.new_for_path (Environment.get_user_data_dir ());
910          file = file.get_child ("folks");
911          file = file.get_child ("backends.ini");
912
913          debug ("Using built-in backends key file '%s' (override with " +
914              "environment variable FOLKS_BACKEND_STORE_KEY_FILE_PATH)",
915              file.get_path ());
916        }
917      else
918        {
919          file = File.new_for_path ((!) path);
920          debug ("Using environment variable " +
921              "FOLKS_BACKEND_STORE_KEY_FILE_PATH = '%s' to load the backends " +
922              "key file.", (!) path);
923        }
924
925      this._config_file = file;
926
927      /* Load the disabled backends file */
928      var key_file = new GLib.KeyFile ();
929      try
930        {
931          uint8[] contents;
932
933          yield file.load_contents_async (null, out contents, null);
934          unowned string contents_s = (string) contents;
935
936          if (contents_s.length > 0)
937            {
938              key_file.load_from_data (contents_s,
939                  contents_s.length, KeyFileFlags.KEEP_COMMENTS);
940            }
941        }
942      catch (Error e1)
943        {
944          if (!(e1 is IOError.NOT_FOUND))
945            {
946              warning ("The backends key file '%s' could not be loaded: %s",
947                  file.get_path (), e1.message);
948              return;
949            }
950        }
951      finally
952        {
953          /* Update the key file in memory, whether the new one is empty or
954           * full. */
955          this._backends_key_file = (owned) key_file;
956        }
957    }
958
959  /* This method is safe to call multiple times concurrently. */
960  private async void _save_key_file ()
961    {
962      var key_file_data = this._backends_key_file.to_data ();
963
964      debug ("Saving backend key file '%s'.", this._config_file.get_path ());
965
966      try
967        {
968          /* Note: We have to use key_file_data.size () here to get its length
969           * in _bytes_ rather than _characters_. bgo#628930.
970           * In Vala >= 0.11, string.size() has been deprecated in favour of
971           * string.length (which now returns the byte length, whereas in
972           * Vala <= 0.10, it returned the character length). FIXME: We need to
973           * take this into account until we depend explicitly on
974           * Vala >= 0.11. */
975          yield this._config_file.replace_contents_async (key_file_data.data,
976              null, false, FileCreateFlags.PRIVATE,
977              null, null);
978        }
979      catch (Error e)
980        {
981          warning ("Could not write updated backend key file '%s': %s",
982              this._config_file.get_path (), e.message);
983        }
984    }
985}
986