unattended-setup-box.vala 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. // This file is part of GNOME Boxes. License: LGPLv2+
  2. [GtkTemplate (ui = "/org/gnome/Boxes/ui/unattended-setup-box.ui")]
  3. private class Boxes.UnattendedSetupBox : Gtk.Box {
  4. private const string KEY_FILE = "setup-data.conf";
  5. private const string EXPRESS_KEY = "express-install";
  6. private const string USERNAME_KEY = "username";
  7. private const string PASSWORD_KEY = "password";
  8. private const string PRODUCTKEY_KEY = "product-key";
  9. public bool ready_for_express {
  10. get {
  11. return username != "" &&
  12. !needs_password &&
  13. (product_key_format == null ||
  14. product_key_entry.text_length == product_key_format.length);
  15. }
  16. }
  17. public bool ready_to_create {
  18. get {
  19. return !express_toggle.active || ready_for_express;
  20. }
  21. }
  22. public bool express_install {
  23. get {
  24. return express_toggle.active;
  25. }
  26. set {
  27. express_toggle.active = value;
  28. }
  29. }
  30. public string username {
  31. get { return username_entry.text; }
  32. }
  33. public string password {
  34. owned get { return password_entry.text; }
  35. }
  36. public string hidden_password {
  37. owned get {
  38. if (password_entry.text.length > 0) {
  39. var str = "";
  40. for (var i = 0; i < password_entry.text_length; i++)
  41. str += password_entry.get_invisible_char ().to_string ();
  42. return str;
  43. } else
  44. return _("no password");
  45. }
  46. }
  47. public string product_key {
  48. owned get {
  49. return (product_key_entry != null)? product_key_entry.text : null;
  50. }
  51. }
  52. public string avatar_path {
  53. set {
  54. try {
  55. var pixbuf = new Gdk.Pixbuf.from_file_at_scale (value, 64, 64, true);
  56. user_avatar.pixbuf = round_image (pixbuf);
  57. } catch (GLib.Error error) {
  58. debug ("Failed to load user avatar file '%s': %s", value, error.message);
  59. }
  60. }
  61. }
  62. public signal void user_wants_to_create (); // User wants to already create the VM
  63. private bool _needs_password;
  64. private bool needs_password {
  65. get {
  66. if (password != "")
  67. return false;
  68. return _needs_password;
  69. }
  70. set {
  71. _needs_password = value;
  72. }
  73. }
  74. [GtkChild]
  75. private unowned Gtk.InfoBar needs_internet_bar;
  76. [GtkChild]
  77. private unowned Gtk.Label needs_internet_label;
  78. [GtkChild]
  79. private unowned Gtk.Grid setup_grid;
  80. [GtkChild]
  81. private unowned Gtk.Label express_label;
  82. [GtkChild]
  83. private unowned Gtk.Switch express_toggle;
  84. [GtkChild]
  85. private unowned Gtk.Image user_avatar;
  86. [GtkChild]
  87. private unowned Gtk.Entry username_entry;
  88. [GtkChild]
  89. private unowned Gtk.Notebook password_notebook;
  90. [GtkChild]
  91. private unowned Gtk.Entry password_entry;
  92. [GtkChild]
  93. private unowned Gtk.Label product_key_label;
  94. [GtkChild]
  95. private unowned Gtk.Entry product_key_entry;
  96. private string? product_key_format;
  97. private string media_path;
  98. private Cancellable cancellable = new Cancellable ();
  99. private GLib.KeyFile keyfile;
  100. private Secret.Schema secret_password_schema
  101. = new Secret.Schema ("org.gnome.Boxes",
  102. Secret.SchemaFlags.NONE,
  103. "gnome-boxes-media-path", Secret.SchemaAttributeType.STRING);
  104. public UnattendedSetupBox (InstallerMedia media, string? product_key_format, bool needs_internet) {
  105. this.product_key_format = product_key_format;
  106. var msg = _("Express installation of %s requires an internet connection.").printf (media.label);
  107. needs_internet_label.label = msg;
  108. needs_internet_bar.visible = needs_internet;
  109. var unnatended_installer = media as UnattendedInstaller;
  110. needs_password = unnatended_installer.needs_password;
  111. media_path = media.device_file;
  112. keyfile = new GLib.KeyFile ();
  113. try {
  114. var filename = get_user_unattended (KEY_FILE);
  115. keyfile.load_from_file (filename, KeyFileFlags.KEEP_COMMENTS);
  116. set_entry_text_from_key (username_entry, USERNAME_KEY, Environment.get_user_name ());
  117. set_entry_text_from_key (password_entry, PASSWORD_KEY);
  118. set_entry_text_from_key (product_key_entry, PRODUCTKEY_KEY);
  119. if (password != "") {
  120. password_notebook.next_page ();
  121. } else {
  122. Secret.password_lookup.begin (secret_password_schema, cancellable, (obj, res) => {
  123. try {
  124. var credentials_str = Secret.password_lookup.end (res);
  125. if (credentials_str == null || credentials_str == "")
  126. return;
  127. try {
  128. var credentials_variant = GLib.Variant.parse (null, credentials_str, null, null);
  129. string password_str;
  130. if (!credentials_variant.lookup ("password", "s", out password_str))
  131. throw new Boxes.Error.INVALID ("couldn't unpack a string for the 'password' key");
  132. if (password_str != null && password_str != "") {
  133. password_entry.text = password_str;
  134. password_notebook.next_page ();
  135. }
  136. } catch (GLib.Error error) {
  137. warning ("Failed to parse password from the keyring: %s", error.message);
  138. }
  139. } catch (GLib.IOError.CANCELLED error) {
  140. return;
  141. } catch (GLib.Error error) {
  142. warning ("Failed to lookup password for '%s' from the keyring: %s",
  143. media_path,
  144. error.message);
  145. }
  146. }, "gnome-boxes-media-path", media_path);
  147. }
  148. try {
  149. keyfile.remove_key (media_path, PASSWORD_KEY);
  150. } catch (GLib.Error error) {
  151. debug ("Failed to remove key '%s' under '%s': %s", PASSWORD_KEY, media_path, error.message);
  152. }
  153. } catch (GLib.Error error) {
  154. debug ("%s either doesn't already exist or we failed to load it: %s", KEY_FILE, error.message);
  155. }
  156. setup_express_toggle (media.os_media.live, needs_internet);
  157. if (product_key_format != null) {
  158. product_key_label.visible = true;
  159. product_key_entry.visible = true;
  160. product_key_entry.width_chars = product_key_format.length;
  161. product_key_entry.max_length = product_key_format.length;
  162. }
  163. foreach (var child in setup_grid.get_children ())
  164. if (child != express_label && child != express_toggle)
  165. express_toggle.bind_property ("active", child, "sensitive", BindingFlags.SYNC_CREATE);
  166. }
  167. public override void dispose () {
  168. if (cancellable != null) {
  169. cancellable.cancel ();
  170. cancellable = null;
  171. }
  172. base.dispose ();
  173. }
  174. public void clean_up () {
  175. NetworkMonitor.get_default ().network_changed.disconnect (update_express_toggle);
  176. }
  177. public void save_settings () {
  178. keyfile.set_boolean (media_path, EXPRESS_KEY, express_install);
  179. keyfile.set_string (media_path, USERNAME_KEY, username);
  180. keyfile.set_string (media_path, PRODUCTKEY_KEY, product_key);
  181. var filename = get_user_unattended (KEY_FILE);
  182. try {
  183. keyfile.save_to_file (filename);
  184. } catch (GLib.Error error) {
  185. debug ("Error saving settings for '%s': %s", media_path, error.message);
  186. }
  187. if (password != null && password != "") {
  188. var variant_builder = new GLib.VariantBuilder (GLib.VariantType.VARDICT);
  189. var password_variant = new GLib.Variant ("s", password);
  190. variant_builder.add ("{sv}", "password", password_variant);
  191. var credentials_variant = variant_builder.end ();
  192. var credentials_str = credentials_variant.print (true);
  193. var label = _("GNOME Boxes credentials for “%s”").printf (media_path);
  194. Secret.password_store.begin (secret_password_schema,
  195. Secret.COLLECTION_DEFAULT,
  196. label,
  197. credentials_str,
  198. null,
  199. (obj, res) => {
  200. try {
  201. Secret.password_store.end (res);
  202. } catch (GLib.Error error) {
  203. warning ("Failed to store password for '%s' in the keyring: %s", media_path, error.message);
  204. }
  205. }, "gnome-boxes-media-path", media_path);
  206. }
  207. }
  208. private void setup_express_toggle (bool live, bool needs_internet) {
  209. try {
  210. express_toggle.active = keyfile.get_boolean (media_path, EXPRESS_KEY);
  211. } catch (GLib.Error error) {
  212. debug ("Failed to read key '%s' under '%s': %s\n", EXPRESS_KEY, media_path, error.message);
  213. express_toggle.active = !live;
  214. }
  215. if (!needs_internet)
  216. return;
  217. var network_monitor = NetworkMonitor.get_default ();
  218. update_express_toggle (network_monitor.get_network_available ());
  219. network_monitor.network_changed.connect (update_express_toggle);
  220. }
  221. private void update_express_toggle(bool network_available) {
  222. if (network_available) {
  223. express_toggle.sensitive = true;
  224. } else {
  225. express_toggle.sensitive = false;
  226. express_toggle.active = false;
  227. }
  228. }
  229. private void set_entry_text_from_key (Gtk.Entry entry, string key, string? default_value = null) {
  230. string? str = null;
  231. try {
  232. str = keyfile.get_string (media_path, key);
  233. } catch (GLib.Error error) {
  234. debug ("Failed to read key '%s' under '%s': %s\n", key, media_path, error.message);
  235. }
  236. if (str != null && str != "")
  237. entry.text = str;
  238. else if (default_value != null)
  239. entry.text = default_value;
  240. }
  241. [GtkCallback]
  242. private void on_mandatory_input_changed () {
  243. notify_property ("ready-to-create");
  244. }
  245. [GtkCallback]
  246. private void on_username_entry_activated () {
  247. if (ready_for_express)
  248. user_wants_to_create ();
  249. else if (username != "" && product_key_format != null)
  250. product_key_entry.grab_focus (); // If username is provided, must be product key thats still not provided
  251. }
  252. [GtkCallback]
  253. private void on_password_button_clicked () {
  254. password_notebook.next_page ();
  255. password_entry.grab_focus ();
  256. }
  257. [GtkCallback]
  258. private void on_password_entry_changed () {
  259. cancellable.cancel ();
  260. cancellable = new Cancellable ();
  261. }
  262. [GtkCallback]
  263. private bool on_password_entry_focus_out () {
  264. if (password_entry.text_length == 0)
  265. password_notebook.prev_page ();
  266. return false;
  267. }
  268. [GtkCallback]
  269. private void on_password_entry_activated () {
  270. if (ready_for_express)
  271. user_wants_to_create ();
  272. else if (username == "")
  273. username_entry.grab_focus ();
  274. else if (product_key_format != null)
  275. product_key_entry.grab_focus ();
  276. }
  277. [GtkCallback]
  278. private void on_key_entry_activated () {
  279. if (ready_for_express)
  280. user_wants_to_create ();
  281. else if (product_key_entry.text_length == product_key_format.length)
  282. username_entry.grab_focus (); // If product key is provided, must be username thats still not provided.
  283. }
  284. private bool we_inserted_text;
  285. [GtkCallback]
  286. private void on_key_text_inserted (string text, int text_length, ref int position) {
  287. if (we_inserted_text)
  288. return;
  289. var result = "";
  290. for (uint i = 0, j = position; i < text_length && j < product_key_format.length; ) {
  291. var character = text.get (i);
  292. var allowed_char = product_key_format.get (j);
  293. var skip_input_char = false;
  294. switch (allowed_char) {
  295. case '@': // Any character
  296. break;
  297. case '%': // Alphabet
  298. if (!character.isalpha ())
  299. skip_input_char = true;
  300. break;
  301. case '#': // Numeric
  302. if (!character.isdigit ())
  303. skip_input_char = true;
  304. break;
  305. case '$': // Alphnumeric
  306. if (!character.isalnum ())
  307. skip_input_char = true;
  308. break;
  309. default: // Hardcoded character required
  310. if (character != allowed_char) {
  311. result += allowed_char.to_string ();
  312. j++;
  313. continue;
  314. }
  315. break;
  316. }
  317. i++;
  318. if (skip_input_char)
  319. continue;
  320. result += character.to_string ();
  321. j++;
  322. }
  323. if (result != "") {
  324. we_inserted_text = true;
  325. product_key_entry.insert_text (result.up (), result.length, ref position);
  326. we_inserted_text = false;
  327. }
  328. Signal.stop_emission_by_name (product_key_entry, "insert-text");
  329. }
  330. [GtkCallback]
  331. private void on_secondary_icon_clicked () {
  332. password_entry.visibility = !password_entry.visibility;
  333. password_entry.secondary_icon_name = password_entry.visibility ?
  334. "eye-open-negative-filled-symbolic" :
  335. "eye-not-looking-symbolic";
  336. }
  337. }