1/*
2 * Seahorse
3 *
4 * Copyright (C) 2008 Stefan Walter
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as
8 * published by the Free Software Foundation; either version 2.1 of
9 * the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this program; if not, see <http://www.gnu.org/licenses/>.
18 */
19
20namespace Seahorse {
21namespace Gkr {
22
23private class MyService : Secret.Service {
24	public override GLib.Type get_collection_gtype() {
25		return typeof(Keyring);
26	}
27
28	public override GLib.Type get_item_gtype() {
29		return typeof(Item);
30	}
31}
32
33public class Backend: GLib.Object , Gcr.Collection, Seahorse.Backend {
34	public string name {
35		get { return NAME; }
36	}
37
38	public string label {
39		get { return _("Passwords"); }
40	}
41
42	public string description {
43		get { return _("Stored personal passwords, credentials and secrets"); }
44	}
45
46    public ActionGroup actions {
47        owned get { return this._actions; }
48    }
49
50	public GLib.HashTable<string, string> aliases {
51		get { return this._aliases; }
52	}
53
54	private bool _loaded;
55	public bool loaded {
56		get { return this._loaded; }
57	}
58
59	public Secret.Service? service {
60		get { return this._service; }
61	}
62
63	private static Backend? _instance = null;
64	private Secret.Service _service;
65	private GLib.HashTable<string, Keyring> _keyrings;
66	private GLib.HashTable<string, string> _aliases;
67	private ActionGroup _actions;
68
69	construct {
70		return_val_if_fail(_instance == null, null);
71		Backend._instance = this;
72
73		this._actions = BackendActions.instance(this);
74		this._keyrings = new GLib.HashTable<string, Keyring>(GLib.str_hash, GLib.str_equal);
75		this._aliases = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal);
76
77		Secret.Service.open.begin(typeof(MyService), null,
78		                          Secret.ServiceFlags.OPEN_SESSION, null, (obj, res) => {
79			try {
80				this._service = Secret.Service.open.end(res);
81				this._service.notify["collections"].connect((obj, pspec) => {
82					refresh_collections();
83				});
84				this._service.load_collections.begin(null, (obj, res) => {
85					try {
86						this._service.load_collections.end(res);
87						refresh_collections();
88					} catch (GLib.Error e) {
89						warning("couldn't load all secret collections: %s", e.message);
90					}
91				});
92				refresh_aliases();
93			} catch (GLib.Error err) {
94				GLib.warning("couldn't connect to secret service: %s", err.message);
95			}
96			notify_property("service");
97		});
98	}
99
100	public override void dispose() {
101		this._aliases.remove_all();
102		this._keyrings.remove_all();
103		base.dispose();
104	}
105
106	public void refresh_collections() {
107		var seen = new GLib.GenericSet<string>(GLib.str_hash, GLib.str_equal);
108		var keyrings = this._service.get_collections();
109
110		string object_path;
111		foreach (var keyring in keyrings) {
112			object_path = keyring.get_object_path();
113
114			/* Don't list the session keyring */
115			if (this._aliases.lookup("session") == object_path)
116				continue;
117
118			var uri = "secret-service://%s".printf(object_path);
119			seen.add(uri);
120			if (this._keyrings.lookup(uri) == null) {
121				this._keyrings.insert(uri, (Keyring)keyring);
122				emit_added(keyring);
123			}
124		}
125
126		/* Remove any that we didn't find */
127		var iter = GLib.HashTableIter<string, Keyring>(this._keyrings);
128		string uri;
129		while (iter.next(out uri, null)) {
130			if (!seen.contains(uri)) {
131				var keyring = this._keyrings.lookup(uri);
132				iter.remove();
133				emit_removed(keyring);
134			}
135		}
136
137		if (!_loaded) {
138			_loaded = true;
139			notify_property("loaded");
140		}
141	}
142
143	public uint get_length() {
144		return this._keyrings.size();
145	}
146
147	public GLib.List<weak GLib.Object> get_objects() {
148		return get_keyrings();
149	}
150
151    public bool contains(GLib.Object object) {
152        var keyring = object as Gkr.Keyring;
153        if (keyring == null)
154            return false;
155
156        return this._keyrings.lookup(keyring.uri) == keyring;
157    }
158
159	public Place? lookup_place(string uri) {
160		return this._keyrings.lookup(uri);
161	}
162
163	public static void initialize() {
164		return_if_fail(Backend._instance == null);
165		(new Backend()).register();
166		return_if_fail(Backend._instance != null);
167	}
168
169	public static Backend instance() {
170		return_val_if_fail(Backend._instance != null, null);
171		return Backend._instance;
172	}
173
174	public GLib.List<unowned Keyring> get_keyrings() {
175		return this._keyrings.get_values();
176	}
177
178	private async void read_alias(string name) {
179		if (this._service == null)
180			return;
181
182		try {
183		    var object_path = this._service.read_alias_dbus_path_sync(name);
184            if (object_path != null) {
185                this._aliases[name] = object_path;
186                notify_property("aliases");
187            }
188        } catch (GLib.Error err) {
189            warning("Couldn't read secret service alias %s: %s", name, err.message);
190        }
191	}
192
193	private void refresh_aliases() {
194		read_alias.begin ("default");
195		read_alias.begin ("session");
196		read_alias.begin ("login");
197	}
198
199	public void refresh() {
200		refresh_aliases();
201		refresh_collections();
202	}
203	public bool has_alias(string alias,
204	                      Keyring keyring) {
205		string object_path = keyring.get_object_path();
206		return this._aliases.lookup(alias) == object_path;
207	}
208}
209
210public class BackendActions : Seahorse.ActionGroup {
211	public Backend backend { construct; get; }
212	private static WeakRef _instance;
213	private bool _initialized;
214
215    private const ActionEntry[] BACKEND_ACTIONS = {
216        { "keyring-new",      on_new_keyring },
217        { "keyring-item-new", on_new_item    },
218        { "copy-secret",      on_copy_secret },
219    };
220
221	construct {
222		this._initialized = false;
223
224		this.backend.notify.connect_after((pspec) => {
225			if (pspec.name == "service")
226				return;
227			if (this._initialized)
228				return;
229			if (this.backend.service == null)
230					return;
231
232			this._initialized = true;
233            add_action_entries(BACKEND_ACTIONS, this);
234		});
235
236		this.backend.notify_property("service");
237	}
238
239    private BackendActions(Backend backend) {
240        GLib.Object(
241            prefix: "gkr",
242            backend: backend
243        );
244    }
245
246    private void on_new_keyring(SimpleAction action, Variant? param) {
247        var dialog = new KeyringAdd(this.catalog);
248
249        int response = dialog.run();
250        if (response == Gtk.ResponseType.ACCEPT)
251            this.catalog.activate_action("focus-place", "secret-service");
252        dialog.destroy();
253    }
254
255    private void on_new_item(SimpleAction action, Variant? param) {
256        var dialog = new ItemAdd(this.catalog);
257
258        int response = dialog.run();
259        if (response == Gtk.ResponseType.ACCEPT)
260            this.catalog.activate_action("focus-place", "secret-service");
261        dialog.destroy();
262    }
263
264    private void on_copy_secret(SimpleAction action, Variant? param) {
265        return_if_fail (this.catalog != null);
266
267        var selected = this.catalog.get_selected_objects();
268        // We checked for this in set_actions_for_selected_objects()
269        return_if_fail (selected.length() == 1);
270
271        var selected_item = selected.data as Gkr.Item;
272        return_if_fail (selected_item != null);
273
274        var clipboard = Gtk.Clipboard.get_default(this.catalog.get_display());
275        selected_item.copy_secret_to_clipboard.begin(clipboard, (obj, res) => {
276            try {
277                selected_item.copy_secret_to_clipboard.end(res);
278            } catch (GLib.Error e) {
279                Util.show_error(this.catalog, "Couldn't copy secret", e.message);
280            }
281        });
282    }
283
284    public override void set_actions_for_selected_objects(List<GLib.Object> objects) {
285        // Allow "copy secret" if there is only 1 Gkr.Item selected
286        bool can_copy_secret = false;
287
288        if (objects.length() == 1) {
289            can_copy_secret = objects.data is Gkr.Item;
290        }
291
292        ((SimpleAction) lookup_action("copy-secret")).set_enabled(can_copy_secret);
293    }
294
295	public static ActionGroup instance(Backend backend) {
296		BackendActions? actions = (BackendActions?)_instance.get();
297		if (actions != null)
298			return actions;
299		actions = new BackendActions(backend);
300		_instance.set(actions);
301		return actions;
302	}
303}
304
305}
306}
307