1/*
2 * Copyright (C) 2010 Collabora Ltd.
3 *
4 * This library is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 2.1 of the License, or
7 * (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors:
18 *       Philip Withnall <philip.withnall@collabora.co.uk>
19 */
20
21using GLib;
22using Gee;
23using Folks;
24using Folks.Backends.Kf;
25
26/**
27 * A persona subclass which represents a single persona from a simple key file.
28 *
29 * @since 0.1.13
30 */
31public class Folks.Backends.Kf.Persona : Folks.Persona,
32    AliasDetails,
33    AntiLinkable,
34    ImDetails,
35    LocalIdDetails,
36    WebServiceDetails
37{
38  private HashMultiMap<string, ImFieldDetails> _im_addresses;
39  private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
40  private string _alias = ""; /* must not be null */
41  private const string[] _linkable_properties =
42    {
43      "im-addresses",
44      "web-service-addresses",
45      "local-ids",
46      null /* FIXME: https://bugzilla.gnome.org/show_bug.cgi?id=682698 */
47    };
48  private const string[] _writeable_properties =
49    {
50      "alias",
51      "im-addresses",
52      "web-service-addresses",
53      "anti-links",
54      null /* FIXME: https://bugzilla.gnome.org/show_bug.cgi?id=682698 */
55    };
56
57  /**
58   * {@inheritDoc}
59   */
60  public override string[] linkable_properties
61    {
62      get { return Kf.Persona._linkable_properties; }
63    }
64
65  /**
66   * {@inheritDoc}
67   *
68   * @since 0.6.0
69   */
70  public override string[] writeable_properties
71    {
72      get { return Kf.Persona._writeable_properties; }
73    }
74
75  /**
76   * {@inheritDoc}
77   *
78   * @since 0.1.15
79   */
80  [CCode (notify = false)]
81  public string alias
82    {
83      get { return this._alias; }
84      set { this.change_alias.begin (value); }
85    }
86
87  /**
88   * {@inheritDoc}
89   *
90   * @since 0.6.2
91   */
92  public async void change_alias (string alias) throws PropertyError
93    {
94      /* Deal with badly-behaved callers. */
95      if (alias == null)
96        {
97          alias = "";
98        }
99
100      if (this._alias == alias)
101        {
102          return;
103        }
104
105      debug ("Setting alias of Kf.Persona '%s' to '%s'.", this.uid, alias);
106
107      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
108      key_file.set_string (this.display_id, "__alias", alias);
109      yield ((Kf.PersonaStore) this.store).save_key_file ();
110
111      this._alias = alias;
112      this.notify_property ("alias");
113    }
114
115  /**
116   * {@inheritDoc}
117   */
118  [CCode (notify = false)]
119  public MultiMap<string, ImFieldDetails> im_addresses
120    {
121      get { return this._im_addresses; }
122      set { this.change_im_addresses.begin (value); }
123    }
124
125  /**
126   * {@inheritDoc}
127   *
128   * @since 0.6.2
129   */
130  public async void change_im_addresses (
131      MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
132    {
133      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
134
135      /* Remove the current IM addresses from the key file */
136      foreach (var protocol1 in this._im_addresses.get_keys ())
137        {
138          try
139            {
140              key_file.remove_key (this.display_id, protocol1);
141            }
142          catch (KeyFileError e1)
143            {
144              /* Ignore the error, since it's just a group or key not found
145               * error. */
146            }
147        }
148
149      /* Add the new IM addresses to the key file and build a normalised
150       * table of them to set as the new property value */
151      var new_im_addresses = new HashMultiMap<string, ImFieldDetails> (
152          null, null, AbstractFieldDetails<string>.hash_static,
153          AbstractFieldDetails<string>.equal_static);
154
155      foreach (var protocol2 in im_addresses.get_keys ())
156        {
157          var addresses = im_addresses.get (protocol2);
158          var normalised_addresses = new SmallSet<string> ();
159
160          foreach (var im_fd in addresses)
161            {
162              string normalised_address;
163              try
164                {
165                  normalised_address = ImDetails.normalise_im_address (
166                      im_fd.value, protocol2);
167                }
168               catch (ImDetailsError e2)
169                {
170                  throw new PropertyError.INVALID_VALUE (
171                      /* Translators: this is an error message for if the user
172                       * provides an invalid IM address. The first parameter is
173                       * an IM address (e.g. “foo@jabber.org”), the second is
174                       * the name of a protocol (e.g. “jabber”) and the third is
175                       * an error message. */
176                      _("Invalid IM address ‘%s’ for protocol ‘%s’: %s"),
177                      im_fd.value, protocol2, e2.message);
178                }
179
180              normalised_addresses.add (normalised_address);
181              var new_im_fd = new ImFieldDetails (normalised_address);
182              new_im_addresses.set (protocol2, new_im_fd);
183            }
184
185          string[] addrs = (string[]) normalised_addresses.to_array ();
186          addrs.length = normalised_addresses.size;
187
188          key_file.set_string_list (this.display_id, protocol2, addrs);
189        }
190
191      /* Get the PersonaStore to save the key file */
192      yield ((Kf.PersonaStore) this.store).save_key_file ();
193
194      this._im_addresses = new_im_addresses;
195      this.notify_property ("im-addresses");
196    }
197
198  /**
199   * {@inheritDoc}
200   */
201  [CCode (notify = false)]
202  public MultiMap<string, WebServiceFieldDetails> web_service_addresses
203    {
204      get { return this._web_service_addresses; }
205      set { this.change_web_service_addresses.begin (value); }
206    }
207
208  /**
209   * {@inheritDoc}
210   *
211   * @since 0.6.2
212   */
213  public async void change_web_service_addresses (
214      MultiMap<string, WebServiceFieldDetails> web_service_addresses)
215          throws PropertyError
216    {
217      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
218
219      /* Remove the current web service addresses from the key file */
220      foreach (var web_service1 in this._web_service_addresses.get_keys ())
221        {
222          try
223            {
224              key_file.remove_key (this.display_id,
225                  "web-service." + web_service1);
226            }
227          catch (KeyFileError e)
228            {
229              /* Ignore the error, since it's just a group or key not found
230               * error. */
231            }
232        }
233
234      /* Add the new web service addresses to the key file and build a
235       * table of them to set as the new property value */
236      var new_web_service_addresses =
237        new HashMultiMap<string, WebServiceFieldDetails> (
238            null, null, AbstractFieldDetails<string>.hash_static,
239            AbstractFieldDetails<string>.equal_static);
240
241      foreach (var web_service2 in web_service_addresses.get_keys ())
242        {
243          var ws_fds = web_service_addresses.get (web_service2);
244
245          string[] addrs = new string[0];
246          foreach (var ws_fd1 in ws_fds)
247            addrs += ws_fd1.value;
248
249          key_file.set_string_list (this.display_id,
250              "web-service." + web_service2, addrs);
251
252          foreach (var ws_fd2 in ws_fds)
253            new_web_service_addresses.set (web_service2, ws_fd2);
254        }
255
256      /* Get the PersonaStore to save the key file */
257      yield ((Kf.PersonaStore) this.store).save_key_file ();
258
259      this._web_service_addresses = new_web_service_addresses;
260      this.notify_property ("web-service-addresses");
261    }
262
263  private SmallSet<string> _anti_links;
264  private Set<string> _anti_links_ro;
265
266  /**
267   * {@inheritDoc}
268   *
269   * @since 0.7.3
270   */
271  [CCode (notify = false)]
272  public Set<string> anti_links
273    {
274      get { return this._anti_links_ro; }
275      set { this.change_anti_links.begin (value); }
276    }
277
278  /**
279   * {@inheritDoc}
280   *
281   * @since 0.7.3
282   */
283  public async void change_anti_links (Set<string> anti_links)
284      throws PropertyError
285    {
286      if (Folks.Internal.equal_sets<string> (anti_links, this.anti_links))
287        {
288          return;
289        }
290
291      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
292
293      /* Skip the persona's UID; don't allow reflexive anti-links. */
294      anti_links.remove (this.uid);
295
296      key_file.set_string_list (this.display_id,
297          Kf.PersonaStore.anti_links_key_name, anti_links.to_array ());
298
299      /* Get the PersonaStore to save the key file */
300      yield ((Kf.PersonaStore) this.store).save_key_file ();
301
302      /* Update the stored anti-links. */
303      this._anti_links.clear ();
304      this._anti_links.add_all (anti_links);
305      this.notify_property ("anti-links");
306    }
307
308  private SmallSet<string> _local_ids;
309  private Set<string> _local_ids_ro;
310
311  /**
312   * {@inheritDoc}
313   *
314   * @since 0.14.0
315   */
316
317  [CCode (notify = false)]
318  public Set<string> local_ids
319    {
320      get
321        {
322          if (this._local_ids.contains (this.iid) == false)
323            {
324              this._local_ids.add (this.iid);
325            }
326          return this._local_ids_ro;
327        }
328      set { this.change_local_ids.begin (value); }
329    }
330
331  /**
332   * {@inheritDoc}
333   *
334   * @since 0.14.0
335   */
336  public async void change_local_ids (Set<string> local_ids)
337      throws PropertyError
338    {
339      if (Folks.Internal.equal_sets<string> (local_ids, this._local_ids))
340        {
341          return;
342        }
343
344      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
345
346      /* Skip the persona's UID; don't allow reflexive anti-links. */
347      //anti_links.remove (this.uid);
348
349      key_file.set_string_list (this.display_id,
350          "__local-ids", local_ids.to_array ());
351
352      /* Get the PersonaStore to save the key file */
353      yield ((Kf.PersonaStore) this.store).save_key_file ();
354
355      /* Update the stored local_ids. */
356      this._local_ids.clear ();
357      this._local_ids.add_all (local_ids);
358      this.notify_property ("local-ids");
359    }
360
361  /**
362   * Create a new persona.
363   *
364   * Create a new persona for the {@link PersonaStore} ``store``, representing
365   * the Persona given by the group ``uid`` in the key file ``key_file``.
366   */
367  public Persona (string id, Folks.PersonaStore store)
368    {
369      var iid = store.id + ":" + id;
370      var uid = Folks.Persona.build_uid ("key-file", store.id, id);
371
372      Object (display_id: id,
373              iid: iid,
374              uid: uid,
375              store: store,
376              is_user: false);
377    }
378
379  construct
380    {
381      debug ("Adding key-file Persona '%s' (IID '%s', group '%s')", this.uid,
382          this.iid, this.display_id);
383
384      this._im_addresses = new HashMultiMap<string, ImFieldDetails> (
385          null, null, AbstractFieldDetails<string>.hash_static,
386          AbstractFieldDetails<string>.equal_static);
387      this._web_service_addresses =
388        new HashMultiMap<string, WebServiceFieldDetails> (
389          null, null, AbstractFieldDetails<string>.hash_static,
390          AbstractFieldDetails<string>.equal_static);
391      this._anti_links = new SmallSet<string> ();
392      this._anti_links_ro = this._anti_links.read_only_view;
393      this._local_ids = new SmallSet<string> ();
394      this._local_ids_ro = this._local_ids.read_only_view;
395
396      /* Load the IM addresses from the key file */
397      unowned KeyFile key_file = ((Kf.PersonaStore) this.store).get_key_file ();
398
399      try
400        {
401          var keys = key_file.get_keys (this.display_id);
402          foreach (unowned string key in keys)
403            {
404              /* Alias */
405              if (key == "__alias")
406                {
407                  this._alias = key_file.get_string (this.display_id, key);
408
409                  if (this._alias == null)
410                    {
411                      this._alias = "";
412                    }
413
414                  debug ("    Loaded alias '%s'.", this._alias);
415                  continue;
416                }
417
418              /* Anti-links. */
419              if (key == Kf.PersonaStore.anti_links_key_name)
420                {
421                  var anti_link_array =
422                      key_file.get_string_list (this.display_id, key);
423
424                  if (anti_link_array != null)
425                    {
426                      foreach (var anti_link in anti_link_array)
427                        {
428                          this._anti_links.add (anti_link);
429                        }
430
431                      debug ("    Loaded %u anti-links.",
432                          anti_link_array.length);
433                      continue;
434                    }
435                }
436              /* Local-ids. */
437              if (key == "__local_ids")
438                {
439                  var local_ids_array =
440                      key_file.get_string_list (this.display_id, key);
441
442                  if (local_ids_array != null)
443                    {
444                      foreach (var local_id in local_ids_array)
445                        {
446                          this.local_ids.add (local_id);
447                        }
448
449                      debug ("    Loaded %u local_ids.",
450                          local_ids_array.length);
451                      continue;
452                    }
453                }
454
455
456              /* Web service addresses */
457              var decomposed_key = key.split(".", 2);
458              if (decomposed_key.length == 2 &&
459                  decomposed_key[0] == "web-service")
460                {
461                  unowned string web_service = decomposed_key[1];
462                  var web_service_addresses = key_file.get_string_list (
463                      this.display_id, web_service);
464
465                  foreach (var web_service_address in web_service_addresses)
466                    {
467                      this._web_service_addresses.set (web_service,
468                          new WebServiceFieldDetails (web_service_address));
469                    }
470
471                  continue;
472                }
473
474              /* IM addresses */
475              unowned string protocol = key;
476              var im_addresses = key_file.get_string_list (
477                  this.display_id, protocol);
478
479              foreach (var im_address in im_addresses)
480                {
481                  string address;
482                  try
483                    {
484                      address = ImDetails.normalise_im_address (im_address,
485                          protocol);
486                    }
487                  catch (ImDetailsError e)
488                    {
489                      /* Warn of and ignore any invalid IM addresses */
490                      warning (e.message);
491                      continue;
492                    }
493
494                  var im_fd = new ImFieldDetails (address);
495                  this._im_addresses.set (protocol, im_fd);
496                }
497            }
498        }
499      catch (KeyFileError e)
500        {
501          /* We get a GROUP_NOT_FOUND exception if we're creating a new
502           * Persona, since it doesn't yet exist in the key file. We shouldn't
503           * get any other exceptions, since we're iterating through a list of
504           * keys we've just retrieved. */
505          if (!(e is KeyFileError.GROUP_NOT_FOUND))
506            {
507              /* Translators: the parameter is an error message. */
508              warning (_("Couldn’t load data from key file: %s"), e.message);
509            }
510        }
511    }
512
513  /**
514   * {@inheritDoc}
515   */
516  public override void linkable_property_to_links (string prop_name,
517      Folks.Persona.LinkablePropertyCallback callback)
518    {
519      if (prop_name == "im-addresses")
520        {
521          var iter = this._im_addresses.map_iterator ();
522
523          while (iter.next ())
524            callback (iter.get_key () + ":" + iter.get_value ().value);
525        }
526      else if (prop_name == "local-ids")
527        {
528          if (this._local_ids != null)
529          foreach (var id in this._local_ids)
530            {
531              callback (id);
532            }
533        }
534      else if (prop_name == "web-service-addresses")
535        {
536          var iter = this.web_service_addresses.map_iterator ();
537
538          while (iter.next ())
539            callback (iter.get_key () + ":" + iter.get_value ().value);
540        }
541      else
542        {
543          /* Chain up */
544          base.linkable_property_to_links (prop_name, callback);
545        }
546    }
547}
548