1 /********************************************************************\
2  * FileDialog.c -- file-handling utility dialogs for gnucash.       *
3  *                                                                  *
4  * Copyright (C) 1997 Robin D. Clark                                *
5  * Copyright (C) 1998, 1999, 2000 Linas Vepstas                     *
6  *                                                                  *
7  * This program is free software; you can redistribute it and/or    *
8  * modify it under the terms of the GNU General Public License as   *
9  * published by the Free Software Foundation; either version 2 of   *
10  * the License, or (at your option) any later version.              *
11  *                                                                  *
12  * This program is distributed in the hope that it will be useful,  *
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
15  * GNU General Public License for more details.                     *
16  *                                                                  *
17  * You should have received a copy of the GNU General Public License*
18  * along with this program; if not, write to the Free Software      *
19  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.        *
20 \********************************************************************/
21 
22 #include <config.h>
23 
24 #include <gtk/gtk.h>
25 #include <glib/gi18n.h>
26 #include <errno.h>
27 #include <string.h>
28 
29 #include "dialog-utils.h"
30 #include "assistant-xml-encoding.h"
31 #include "gnc-commodity.h"
32 #include "gnc-component-manager.h"
33 #include "gnc-engine.h"
34 #include "Account.h"
35 #include "gnc-file.h"
36 #include "gnc-features.h"
37 #include "gnc-filepath-utils.h"
38 #include "gnc-gui-query.h"
39 #include "gnc-hooks.h"
40 #include "gnc-keyring.h"
41 #include "gnc-splash.h"
42 #include "gnc-ui.h"
43 #include "gnc-ui-util.h"
44 #include "gnc-uri-utils.h"
45 #include "gnc-window.h"
46 #include "gnc-plugin-file-history.h"
47 #include "qof.h"
48 #include "Scrub.h"
49 #include "TransLog.h"
50 #include "gnc-session.h"
51 #include "gnc-state.h"
52 #include "gnc-autosave.h"
53 #include <gnc-sx-instance-model.h>
54 #include <SX-book.h>
55 
56 /** GLOBALS *********************************************************/
57 /* This static indicates the debugging module that this .o belongs to.  */
58 static QofLogModule log_module = GNC_MOD_GUI;
59 
60 static GNCShutdownCB shutdown_cb = NULL;
61 static gint save_in_progress = 0;
62 
63 // gnc_file_dialog_int is used both by gnc_file_dialog and gnc_file_dialog_multi
64 static GSList *
gnc_file_dialog_int(GtkWindow * parent,const char * title,GList * filters,const char * starting_dir,GNCFileDialogType type,gboolean multi)65 gnc_file_dialog_int (GtkWindow *parent,
66                      const char * title,
67                      GList * filters,
68                      const char * starting_dir,
69                      GNCFileDialogType type,
70                      gboolean multi
71                      )
72 {
73     GtkWidget *file_box;
74     const char *internal_name;
75     char *file_name = NULL;
76     gchar * okbutton = NULL;
77     const gchar *ok_icon = NULL;
78     GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
79     gint response;
80     GSList* file_name_list = NULL;
81 
82     ENTER(" ");
83 
84     switch (type)
85     {
86     case GNC_FILE_DIALOG_OPEN:
87         action = GTK_FILE_CHOOSER_ACTION_OPEN;
88         okbutton = _("_Open");
89         if (title == NULL)
90             title = _("Open");
91         break;
92     case GNC_FILE_DIALOG_IMPORT:
93         action = GTK_FILE_CHOOSER_ACTION_OPEN;
94         okbutton = _("_Import");
95         if (title == NULL)
96             title = _("Import");
97         break;
98     case GNC_FILE_DIALOG_SAVE:
99         action = GTK_FILE_CHOOSER_ACTION_SAVE;
100         okbutton = _("_Save");
101         if (title == NULL)
102             title = _("Save");
103         break;
104     case GNC_FILE_DIALOG_EXPORT:
105         action = GTK_FILE_CHOOSER_ACTION_SAVE;
106         okbutton = _("_Export");
107         ok_icon = "go-next";
108         if (title == NULL)
109             title = _("Export");
110         break;
111 
112     }
113 
114     file_box = gtk_file_chooser_dialog_new(
115                    title,
116                    parent,
117                    action,
118                    _("_Cancel"), GTK_RESPONSE_CANCEL,
119                    NULL);
120     if (multi)
121         gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (file_box), TRUE);
122 
123     if (ok_icon)
124         gnc_gtk_dialog_add_button(file_box, okbutton, ok_icon, GTK_RESPONSE_ACCEPT);
125     else
126         gtk_dialog_add_button(GTK_DIALOG(file_box),
127                               okbutton, GTK_RESPONSE_ACCEPT);
128 
129     if (starting_dir)
130         gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER (file_box),
131                                             starting_dir);
132 
133     gtk_window_set_modal(GTK_WINDOW(file_box), TRUE);
134 
135     if (filters != NULL)
136     {
137         GList* filter;
138         GtkFileFilter* all_filter = gtk_file_filter_new();
139 
140         for (filter = filters; filter; filter = filter->next)
141         {
142             g_return_val_if_fail(GTK_IS_FILE_FILTER(filter->data), NULL);
143             gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (file_box),
144                                          GTK_FILE_FILTER (filter->data));
145         }
146 
147         gtk_file_filter_set_name (all_filter, _("All files"));
148         gtk_file_filter_add_pattern (all_filter, "*");
149         gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (file_box), all_filter);
150 
151         /* Note: You cannot set a file filter and preselect a file name.
152          * The latter wins, and the filter ends up disabled.  Since we are
153          * only setting the starting directory for the chooser dialog,
154          * everything works as expected. */
155         gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (file_box),
156                                      GTK_FILE_FILTER (filters->data));
157         g_list_free (filters);
158     }
159 
160     response = gtk_dialog_run(GTK_DIALOG(file_box));
161 
162     // Set the name for this dialog so it can be easily manipulated with css
163     gtk_widget_set_name (GTK_WIDGET(file_box), "gnc-id-file");
164 
165     if (response == GTK_RESPONSE_ACCEPT)
166     {
167         if (multi)
168         {
169             file_name_list = gtk_file_chooser_get_filenames (GTK_FILE_CHOOSER (file_box));
170         }
171         else
172         {
173             /* look for constructs like postgres://foo */
174             internal_name = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER (file_box));
175             if (internal_name != NULL)
176             {
177                 if (strstr (internal_name, "file://") == internal_name)
178                 {
179                     /* nope, a local file name */
180                     internal_name = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER (file_box));
181                 }
182                 file_name = g_strdup(internal_name);
183             }
184             file_name_list = g_slist_append (file_name_list, file_name);
185         }
186     }
187     gtk_widget_destroy(GTK_WIDGET(file_box));
188     LEAVE("%s", file_name ? file_name : "(null)");
189     return file_name_list;
190 }
191 
192 /********************************************************************\
193  * gnc_file_dialog                                                  *
194  *   Pops up a file selection dialog (either a "Save As" or an      *
195  *   "Open"), and returns the name of the file the user selected.   *
196  *   (This function does not return until the user selects a file   *
197  *   or presses "Cancel" or the window manager destroy button)      *
198  *                                                                  *
199  * Args:   title        - the title of the window                   *
200  *         filters      - list of GtkFileFilters to use, will be    *
201  *                        freed automatically                       *
202  *         default_dir  - start the chooser in this directory       *
203  *         type         - what type of dialog (open, save, etc.)    *
204  * Return: containing the name of the file the user selected        *
205  \********************************************************************/
206 char *
gnc_file_dialog(GtkWindow * parent,const char * title,GList * filters,const char * starting_dir,GNCFileDialogType type)207 gnc_file_dialog (GtkWindow *parent,
208                  const char * title,
209                  GList * filters,
210                  const char * starting_dir,
211                  GNCFileDialogType type
212                  )
213 {
214     gchar* file_name = NULL;
215     GSList* ret = gnc_file_dialog_int (parent, title, filters, starting_dir, type, FALSE);
216     if (ret)
217         file_name = g_strdup (ret->data);
218     g_slist_free_full (ret, g_free);
219     return file_name;
220 }
221 
222 /********************************************************************\
223  * gnc_file_dialog_multi                                            *
224  *   Pops up a file selection dialog (either a "Save As" or an      *
225  *   "Open"), and returns the name of the files the user selected.  *
226  *   Similar to gnc_file_dialog with allowing multi-file selection  *
227  *                                                                  *
228  * Args:   title        - the title of the window                   *
229  *         filters      - list of GtkFileFilters to use, will be    *
230  *                        freed automatically                       *
231  *         default_dir  - start the chooser in this directory       *
232  *         type         - what type of dialog (open, save, etc.)    *
233  * Return: GList containing the names of the selected files         *
234  \********************************************************************/
235 
236 GSList *
gnc_file_dialog_multi(GtkWindow * parent,const char * title,GList * filters,const char * starting_dir,GNCFileDialogType type)237 gnc_file_dialog_multi (GtkWindow *parent,
238                        const char * title,
239                        GList * filters,
240                        const char * starting_dir,
241                        GNCFileDialogType type
242                        )
243 {
244     return gnc_file_dialog_int (parent, title, filters, starting_dir, type, TRUE);
245 }
246 
247 gboolean
show_session_error(GtkWindow * parent,QofBackendError io_error,const char * newfile,GNCFileDialogType type)248 show_session_error (GtkWindow *parent,
249                     QofBackendError io_error,
250                     const char *newfile,
251                     GNCFileDialogType type)
252 {
253     GtkWidget *dialog;
254     gboolean uh_oh = TRUE;
255     const char *fmt, *label;
256     gchar *displayname;
257     gint response;
258 
259     if (NULL == newfile)
260     {
261         displayname = g_strdup(_("(null)"));
262     }
263     else if (!gnc_uri_targets_local_fs (newfile)) /* Hide the db password in error messages */
264         displayname = gnc_uri_normalize_uri ( newfile, FALSE);
265     else
266     {
267         /* Strip the protocol from the file name and ensure absolute filename. */
268         char *uri = gnc_uri_normalize_uri(newfile, FALSE);
269         displayname = gnc_uri_get_path(uri);
270         g_free(uri);
271     }
272 
273     switch (io_error)
274     {
275     case ERR_BACKEND_NO_ERR:
276         uh_oh = FALSE;
277         break;
278 
279     case ERR_BACKEND_NO_HANDLER:
280         fmt = _("No suitable backend was found for %s.");
281         gnc_error_dialog(parent, fmt, displayname);
282         break;
283 
284     case ERR_BACKEND_NO_BACKEND:
285         fmt = _("The URL %s is not supported by this version of GnuCash.");
286         gnc_error_dialog (parent, fmt, displayname);
287         break;
288 
289     case ERR_BACKEND_BAD_URL:
290         fmt = _("Can't parse the URL %s.");
291         gnc_error_dialog (parent, fmt, displayname);
292         break;
293 
294     case ERR_BACKEND_CANT_CONNECT:
295         fmt = _("Can't connect to %s. "
296                 "The host, username or password were incorrect.");
297         gnc_error_dialog (parent, fmt, displayname);
298         break;
299 
300     case ERR_BACKEND_CONN_LOST:
301         fmt = _("Can't connect to %s. "
302                 "Connection was lost, unable to send data.");
303         gnc_error_dialog (parent, fmt, displayname);
304         break;
305 
306     case ERR_BACKEND_TOO_NEW:
307         fmt = _("This file/URL appears to be from a newer version "
308                 "of GnuCash. You must upgrade your version of GnuCash "
309                 "to work with this data.");
310         gnc_error_dialog (parent, "%s", fmt);
311         break;
312 
313     case ERR_BACKEND_NO_SUCH_DB:
314         fmt = _("The database %s doesn't seem to exist. "
315                 "Do you want to create it?");
316         if (gnc_verify_dialog (parent, TRUE, fmt, displayname))
317         {
318             uh_oh = FALSE;
319         }
320         break;
321 
322     case ERR_BACKEND_LOCKED:
323         switch (type)
324         {
325         case GNC_FILE_DIALOG_OPEN:
326         default:
327             label = _("Open");
328             fmt = _("GnuCash could not obtain the lock for %s. "
329                     "That database may be in use by another user, "
330                     "in which case you should not open the database. "
331                     "Do you want to proceed with opening the database?");
332             break;
333 
334         case GNC_FILE_DIALOG_IMPORT:
335             label = _("Import");
336             fmt = _("GnuCash could not obtain the lock for %s. "
337                     "That database may be in use by another user, "
338                     "in which case you should not import the database. "
339                     "Do you want to proceed with importing the database?");
340             break;
341 
342         case GNC_FILE_DIALOG_SAVE:
343             label = _("Save");
344             fmt = _("GnuCash could not obtain the lock for %s. "
345                     "That database may be in use by another user, "
346                     "in which case you should not save the database. "
347                     "Do you want to proceed with saving the database?");
348             break;
349 
350         case GNC_FILE_DIALOG_EXPORT:
351             label = _("Export");
352             fmt = _("GnuCash could not obtain the lock for %s. "
353                     "That database may be in use by another user, "
354                     "in which case you should not export the database. "
355                     "Do you want to proceed with exporting the database?");
356             break;
357         }
358 
359         dialog = gtk_message_dialog_new(parent,
360                                         GTK_DIALOG_DESTROY_WITH_PARENT,
361                                         GTK_MESSAGE_QUESTION,
362                                         GTK_BUTTONS_NONE,
363                                         fmt,
364                                         displayname);
365         gtk_dialog_add_buttons(GTK_DIALOG(dialog),
366                                _("_Cancel"), GTK_RESPONSE_CANCEL,
367                                label, GTK_RESPONSE_YES,
368                                NULL);
369         if (!parent)
370             gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), FALSE);
371         response = gtk_dialog_run(GTK_DIALOG(dialog));
372         gtk_widget_destroy(dialog);
373         uh_oh = (response != GTK_RESPONSE_YES);
374         break;
375 
376     case ERR_BACKEND_READONLY:
377         fmt = _("GnuCash could not write to %s. "
378                 "That database may be on a read-only file system, "
379                 "you may not have write permission for the directory "
380                 "or your anti-virus software is preventing this action.");
381         gnc_error_dialog (parent, fmt, displayname);
382         break;
383 
384     case ERR_BACKEND_DATA_CORRUPT:
385         fmt = _("The file/URL %s "
386                 "does not contain GnuCash data or the data is corrupt.");
387         gnc_error_dialog (parent, fmt, displayname);
388         break;
389 
390     case ERR_BACKEND_SERVER_ERR:
391         fmt = _("The server at URL %s "
392                 "experienced an error or encountered bad or corrupt data.");
393         gnc_error_dialog (parent, fmt, displayname);
394         break;
395 
396     case ERR_BACKEND_PERM:
397         fmt = _("You do not have permission to access %s.");
398         gnc_error_dialog (parent, fmt, displayname);
399         break;
400 
401     case ERR_BACKEND_MISC:
402         fmt = _("An error occurred while processing %s.");
403         gnc_error_dialog (parent, fmt, displayname);
404         break;
405 
406     case ERR_FILEIO_FILE_BAD_READ:
407         fmt = _("There was an error reading the file. "
408                 "Do you want to continue?");
409         if (gnc_verify_dialog (parent, TRUE, "%s", fmt))
410         {
411             uh_oh = FALSE;
412         }
413         break;
414 
415     case ERR_FILEIO_PARSE_ERROR:
416         fmt = _("There was an error parsing the file %s.");
417         gnc_error_dialog (parent, fmt, displayname);
418         break;
419 
420     case ERR_FILEIO_FILE_EMPTY:
421         fmt = _("The file %s is empty.");
422         gnc_error_dialog (parent, fmt, displayname);
423         break;
424 
425     case ERR_FILEIO_FILE_NOT_FOUND:
426         if (type == GNC_FILE_DIALOG_SAVE)
427         {
428             uh_oh = FALSE;
429         }
430         else
431         {
432             if (gnc_history_test_for_file (displayname))
433             {
434                 fmt = _("The file/URI %s could not be found.\n\nThe file is in the history list, do you want to remove it?");
435                 if (gnc_verify_dialog (parent, FALSE, fmt, displayname))
436                     gnc_history_remove_file (displayname);
437             }
438             else
439             {
440                 fmt = _("The file/URI %s could not be found.");
441                 gnc_error_dialog (parent, fmt, displayname);
442             }
443         }
444         break;
445 
446     case ERR_FILEIO_FILE_TOO_OLD:
447         fmt = _("This file is from an older version of GnuCash. "
448                 "Do you want to continue?");
449         if (gnc_verify_dialog (parent, TRUE, "%s", fmt))
450         {
451             uh_oh = FALSE;
452         }
453         break;
454 
455     case ERR_FILEIO_UNKNOWN_FILE_TYPE:
456         fmt = _("The file type of file %s is unknown.");
457         gnc_error_dialog(parent, fmt, displayname);
458         break;
459 
460     case ERR_FILEIO_BACKUP_ERROR:
461         fmt = _("Could not make a backup of the file %s");
462         gnc_error_dialog(parent, fmt, displayname);
463         break;
464 
465     case ERR_FILEIO_WRITE_ERROR:
466         fmt = _("Could not write to file %s. Check that you have "
467                 "permission to write to this file and that "
468                 "there is sufficient space to create it.");
469         gnc_error_dialog(parent, fmt, displayname);
470         break;
471 
472     case ERR_FILEIO_FILE_EACCES:
473         fmt = _("No read permission to read from file %s.");
474         gnc_error_dialog (parent, fmt, displayname);
475         break;
476 
477     case ERR_FILEIO_RESERVED_WRITE:
478         /* Translators: the first %s is a path in the filesystem,
479            the second %s is PACKAGE_NAME, which by default is "GnuCash" */
480         fmt = _("You attempted to save in\n%s\nor a subdirectory thereof. "
481                 "This is not allowed as %s reserves that directory for internal use.\n\n"
482                 "Please try again in a different directory.");
483         gnc_error_dialog (parent, fmt, gnc_userdata_dir(), PACKAGE_NAME);
484         break;
485 
486     case ERR_SQL_DB_TOO_OLD:
487         fmt = _("This database is from an older version of GnuCash. "
488                 "Select OK to upgrade it to the current version, Cancel "
489                 "to mark it read-only.");
490 
491         response = gnc_ok_cancel_dialog(parent, GTK_RESPONSE_CANCEL, "%s", fmt);
492         uh_oh = (response == GTK_RESPONSE_CANCEL);
493         break;
494 
495     case ERR_SQL_DB_TOO_NEW:
496         fmt = _("This database is from a newer version of GnuCash. "
497                 "This version can read it, but cannot safely save to it. "
498                 "It will be marked read-only until you do File->Save As, "
499                 "but data may be lost in writing to the old version.");
500         gnc_warning_dialog (parent, "%s", fmt);
501         uh_oh = TRUE;
502         break;
503 
504     case ERR_SQL_DB_BUSY:
505         fmt = _("The SQL database is in use by other users, "
506                 "and the upgrade cannot be performed until they logoff. "
507                 "If there are currently no other users, consult the "
508                 "documentation to learn how to clear out dangling login "
509                 "sessions.");
510         gnc_error_dialog (parent, "%s", fmt);
511         break;
512 
513     case ERR_SQL_BAD_DBI:
514 
515         fmt = _("The library \"libdbi\" installed on your system doesn't correctly "
516                 "store large numbers. This means GnuCash cannot use SQL databases "
517                 "correctly. Gnucash will not open or save to SQL databases until this is "
518                 "fixed by installing a different version of \"libdbi\". Please see "
519                 "https://bugs.gnucash.org/show_bug.cgi?id=611936 for more "
520                 "information.");
521 
522         gnc_error_dialog (parent, "%s", fmt);
523         break;
524 
525     case ERR_SQL_DBI_UNTESTABLE:
526 
527         fmt = _("GnuCash could not complete a critical test for the presence of "
528                 "a bug in the \"libdbi\" library. This may be caused by a "
529                 "permissions misconfiguration of your SQL database. Please see "
530                 "https://bugs.gnucash.org/show_bug.cgi?id=645216 for more "
531                 "information.");
532 
533         gnc_error_dialog (parent, "%s", fmt);
534         break;
535 
536     case ERR_FILEIO_FILE_UPGRADE:
537         fmt = _("This file is from an older version of GnuCash and will be "
538                 "upgraded when saved by this version. You will not be able "
539                 "to read the saved file from the older version of Gnucash "
540                 "(it will report an \"error parsing the file\"). If you wish "
541                 "to preserve the old version, exit without saving.");
542         gnc_warning_dialog (parent, "%s", fmt);
543         uh_oh = FALSE;
544         break;
545 
546     default:
547         PERR("FIXME: Unhandled error %d", io_error);
548         fmt = _("An unknown I/O error (%d) occurred.");
549         gnc_error_dialog (parent, fmt, io_error);
550         break;
551     }
552 
553     g_free (displayname);
554     return uh_oh;
555 }
556 
557 static void
gnc_add_history(QofSession * session)558 gnc_add_history (QofSession * session)
559 {
560     const gchar *url;
561     char *file;
562 
563     if (!session) return;
564 
565     url = qof_session_get_url ( session );
566     if ( !strlen (url) )
567         return;
568 
569     if (gnc_uri_targets_local_fs (url))
570         file = gnc_uri_get_path ( url );
571     else
572         file = gnc_uri_normalize_uri ( url, FALSE ); /* Note that the password is not saved in history ! */
573 
574     gnc_history_add_file (file);
575     g_free (file);
576 }
577 
578 static void
gnc_book_opened(void)579 gnc_book_opened (void)
580 {
581     gnc_hook_run(HOOK_BOOK_OPENED, gnc_get_current_session());
582 }
583 
584 void
gnc_file_new(GtkWindow * parent)585 gnc_file_new (GtkWindow *parent)
586 {
587     QofSession *session;
588 
589     /* If user attempts to start a new session before saving results of
590      * the last one, prompt them to clean up their act. */
591     if (!gnc_file_query_save (parent, TRUE))
592         return;
593 
594     if (gnc_current_session_exist())
595     {
596         session = gnc_get_current_session ();
597 
598         /* close any ongoing file sessions, and free the accounts.
599          * disable events so we don't get spammed by redraws. */
600         qof_event_suspend ();
601 
602         gnc_hook_run(HOOK_BOOK_CLOSED, session);
603 
604         gnc_close_gui_component_by_session (session);
605         gnc_state_save (session);
606         gnc_clear_current_session();
607         qof_event_resume ();
608     }
609 
610     /* start a new book */
611     gnc_get_current_session ();
612 
613     gnc_hook_run(HOOK_NEW_BOOK, NULL);
614 
615     gnc_gui_refresh_all ();
616 
617     /* Call this after re-enabling events. */
618     gnc_book_opened ();
619 }
620 
621 gboolean
gnc_file_query_save(GtkWindow * parent,gboolean can_cancel)622 gnc_file_query_save (GtkWindow *parent, gboolean can_cancel)
623 {
624     QofBook *current_book;
625 
626     if (!gnc_current_session_exist())
627         return TRUE;
628 
629     current_book = qof_session_get_book (gnc_get_current_session ());
630     /* Remove any pending auto-save timeouts */
631     gnc_autosave_remove_timer(current_book);
632 
633     /* If user wants to mess around before finishing business with
634      * the old file, give him a chance to figure out what's up.
635      * Pose the question as a "while" loop, so that if user screws
636      * up the file-selection dialog, we don't blow him out of the water;
637      * instead, give them another chance to say "no" to the verify box.
638      */
639     while (qof_book_session_not_saved(current_book))
640     {
641         GtkWidget *dialog;
642         gint response;
643         const char *title = _("Save changes to the file?");
644         /* This should be the same message as in gnc-main-window.c */
645         time64 oldest_change;
646         gint minutes;
647 
648         dialog = gtk_message_dialog_new(parent,
649                                         GTK_DIALOG_DESTROY_WITH_PARENT,
650                                         GTK_MESSAGE_QUESTION,
651                                         GTK_BUTTONS_NONE,
652                                         "%s", title);
653         oldest_change = qof_book_get_session_dirty_time(current_book);
654         minutes = (gnc_time (NULL) - oldest_change) / 60 + 1;
655         gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
656                 ngettext("If you don't save, changes from the past %d minute will be discarded.",
657                          "If you don't save, changes from the past %d minutes will be discarded.",
658                          minutes), minutes);
659         gtk_dialog_add_button(GTK_DIALOG(dialog),
660                               _("Continue _Without Saving"), GTK_RESPONSE_OK);
661 
662         if (can_cancel)
663             gtk_dialog_add_button(GTK_DIALOG(dialog),
664                                   _("_Cancel"), GTK_RESPONSE_CANCEL);
665         gtk_dialog_add_button(GTK_DIALOG(dialog),
666                               _("_Save"), GTK_RESPONSE_YES);
667 
668         gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_YES);
669 
670         response = gtk_dialog_run(GTK_DIALOG(dialog));
671         gtk_widget_destroy(dialog);
672 
673         switch (response)
674         {
675         case GTK_RESPONSE_YES:
676             gnc_file_save (parent);
677             /* Go check the loop condition. */
678             break;
679 
680         case GTK_RESPONSE_CANCEL:
681         default:
682             if (can_cancel)
683                 return FALSE;
684             /* No cancel function available. */
685             /* Fall through */
686 
687         case GTK_RESPONSE_OK:
688             return TRUE;
689         }
690     }
691 
692     return TRUE;
693 }
694 
695 
696 /* private utilities for file open; done in two stages */
697 
698 #define RESPONSE_NEW 1
699 #define RESPONSE_OPEN 2
700 #define RESPONSE_QUIT 3
701 #define RESPONSE_READONLY 4
702 #define RESPONSE_FILE 5
703 
704 static gboolean
gnc_post_file_open(GtkWindow * parent,const char * filename,gboolean is_readonly)705 gnc_post_file_open (GtkWindow *parent, const char * filename, gboolean is_readonly)
706 {
707     QofSession *new_session;
708     QofBook *new_book;
709     GList *invalid_account_names;
710     gboolean uh_oh = FALSE;
711     char * newfile;
712     QofBackendError io_err = ERR_BACKEND_NO_ERR;
713 
714     gchar *scheme   = NULL;
715     gchar *hostname = NULL;
716     gchar *username = NULL;
717     gchar *password = NULL;
718     gchar *path = NULL;
719     gint32 port = 0;
720 
721 
722     ENTER("filename %s", filename);
723 RESTART:
724     if (!filename || (*filename == '\0')) return FALSE;
725 
726     /* Convert user input into a normalized uri
727      * Note that the normalized uri for internal use can have a password */
728     newfile = gnc_uri_normalize_uri ( filename, TRUE );
729     if (!newfile)
730     {
731         show_session_error (parent,
732                             ERR_FILEIO_FILE_NOT_FOUND, filename,
733                             GNC_FILE_DIALOG_OPEN);
734         return FALSE;
735     }
736 
737     gnc_uri_get_components (newfile, &scheme, &hostname,
738                             &port, &username, &password, &path);
739 
740     /* If the file to open is a database, and no password was given,
741      * attempt to look it up in a keyring. If that fails the keyring
742      * function will ask the user to enter a password. The user can
743      * cancel this dialog, in which case the open file action will be
744      * abandoned.
745      * Note newfile is normalized uri so we can safely call
746      * gnc_uri_is_file_scheme on it.
747      */
748     if (!gnc_uri_is_file_scheme (scheme) && !password)
749     {
750         gboolean have_valid_pw = FALSE;
751         have_valid_pw = gnc_keyring_get_password ( NULL, scheme, hostname, port,
752                         path, &username, &password );
753         if (!have_valid_pw)
754             return FALSE;
755 
756         /* Got password. Recreate the uri to use internally. */
757         g_free ( newfile );
758         newfile = gnc_uri_create_uri ( scheme, hostname, port,
759                                        username, password, path);
760     }
761 
762     /* For file based uri's, remember the directory as the default. */
763     if (gnc_uri_is_file_scheme(scheme))
764     {
765         gchar *default_dir = g_path_get_dirname(path);
766         gnc_set_default_directory (GNC_PREFS_GROUP_OPEN_SAVE, default_dir);
767         g_free(default_dir);
768     }
769 
770     /* disable events while moving over to the new set of accounts;
771      * the mass deletion of accounts and transactions during
772      * switchover would otherwise cause excessive redraws. */
773     qof_event_suspend ();
774 
775     /* Change the mouse to a busy cursor */
776     gnc_set_busy_cursor (NULL, TRUE);
777 
778     /* -------------- BEGIN CORE SESSION CODE ------------- */
779     /* -- this code is almost identical in FileOpen and FileSaveAs -- */
780     if (gnc_current_session_exist())
781     {
782         QofSession *current_session = gnc_get_current_session();
783         gnc_hook_run(HOOK_BOOK_CLOSED, current_session);
784         gnc_close_gui_component_by_session (current_session);
785         gnc_state_save (current_session);
786         gnc_clear_current_session();
787     }
788 
789     /* load the accounts from the users datafile */
790     /* but first, check to make sure we've got a session going. */
791     new_session = qof_session_new (qof_book_new());
792 
793     // Begin the new session. If we are in read-only mode, ignore the locks.
794     qof_session_begin (new_session, newfile,
795                        is_readonly ? SESSION_READ_ONLY : SESSION_NORMAL_OPEN);
796     io_err = qof_session_get_error (new_session);
797 
798     if (ERR_BACKEND_BAD_URL == io_err)
799     {
800         gchar *directory;
801         show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_OPEN);
802         if (g_file_test (filename, G_FILE_TEST_IS_DIR))
803             directory = g_strdup (filename);
804         else
805             directory = gnc_get_default_directory (GNC_PREFS_GROUP_OPEN_SAVE);
806 
807         filename = gnc_file_dialog (parent, NULL, NULL, directory,
808                                     GNC_FILE_DIALOG_OPEN);
809         /* Suppress trying to save the empty session. */
810         qof_book_mark_session_saved (qof_session_get_book (new_session));
811         qof_session_destroy (new_session);
812         new_session = NULL;
813         g_free (directory);
814         goto RESTART;
815     }
816     /* if file appears to be locked, ask the user ... */
817     else if (ERR_BACKEND_LOCKED == io_err || ERR_BACKEND_READONLY == io_err)
818     {
819         GtkWidget *dialog;
820         gchar *displayname = NULL;
821 
822         char *fmt1 = _("GnuCash could not obtain the lock for %s.");
823         char *fmt2 = ((ERR_BACKEND_LOCKED == io_err) ?
824                       _("That database may be in use by another user, "
825                         "in which case you should not open the database. "
826                         "What would you like to do?") :
827                       _("That database may be on a read-only file system, "
828                         "you may not have write permission for the directory, "
829                         "or your anti-virus software is preventing this action. "
830                         "If you proceed you may not be able to save any changes. "
831                         "What would you like to do?")
832                      );
833         int rc;
834 
835         /* Hide the db password and local filesystem schemes in error messages */
836         if (!gnc_uri_is_file_uri (newfile))
837             displayname = gnc_uri_normalize_uri ( newfile, FALSE);
838         else
839             displayname = gnc_uri_get_path (newfile);
840 
841         dialog = gtk_message_dialog_new(parent,
842                                         0,
843                                         GTK_MESSAGE_WARNING,
844                                         GTK_BUTTONS_NONE,
845                                         fmt1, displayname);
846         gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
847                 "%s", fmt2);
848         gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), FALSE);
849 
850         gnc_gtk_dialog_add_button(dialog, _("Open _Read-Only"),
851                                   "emblem-readonly", RESPONSE_READONLY);
852 
853         gnc_gtk_dialog_add_button(dialog, _("Create _New File"),
854                                   "document-new-symbolic", RESPONSE_NEW);
855 
856         gnc_gtk_dialog_add_button(dialog, _("Open _Anyway"),
857                                   "document-open-symbolic", RESPONSE_OPEN);
858 
859         gnc_gtk_dialog_add_button(dialog, _("Open _Folder"),
860                                   "folder-open-symbolic", RESPONSE_FILE);
861 
862         if (shutdown_cb)
863         {
864             gtk_dialog_add_button(GTK_DIALOG(dialog),
865                                   _("_Quit"), RESPONSE_QUIT);
866             gtk_dialog_set_default_response (GTK_DIALOG(dialog), RESPONSE_QUIT);
867         }
868         else
869             gtk_dialog_set_default_response (GTK_DIALOG(dialog), RESPONSE_FILE);
870 
871         rc = gtk_dialog_run(GTK_DIALOG(dialog));
872         gtk_widget_destroy(dialog);
873         g_free (displayname);
874 
875         if (rc == GTK_RESPONSE_DELETE_EVENT)
876         {
877             rc = shutdown_cb ? RESPONSE_QUIT : RESPONSE_FILE;
878         }
879         switch (rc)
880         {
881         case RESPONSE_QUIT:
882             if (shutdown_cb)
883                 shutdown_cb(0);
884             g_assert(1);
885             break;
886         case RESPONSE_READONLY:
887             is_readonly = TRUE;
888             /* user told us to open readonly. We do ignore locks (just as before), but now also force the opening. */
889             qof_session_begin (new_session, newfile, SESSION_READ_ONLY);
890             break;
891         case RESPONSE_OPEN:
892             /* user told us to ignore locks. So ignore them. */
893             qof_session_begin (new_session, newfile, SESSION_BREAK_LOCK);
894             break;
895         case RESPONSE_NEW:
896             /* Can't use the given file, so just create a new
897              * database so that the user will get a window that
898              * they can click "Exit" on.
899              */
900             gnc_file_new (parent);
901             break;
902         default:
903             /* Can't use the given file, so open a file browser dialog
904              * so they can choose a different file and get a window that
905              * they can click "Exit" on.
906              */
907             gnc_file_open (parent);
908             break;
909         }
910     }
911     /* if the database doesn't exist, ask the user ... */
912     else if ((ERR_BACKEND_NO_SUCH_DB == io_err))
913     {
914         if (!show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_OPEN))
915         {
916             /* user told us to create a new database. Do it. We
917                      * shouldn't have to worry about locking or clobbering,
918                      * it's supposed to be new. */
919             qof_session_begin (new_session, newfile, SESSION_NEW_STORE);
920         }
921     }
922 
923     /* Check for errors again, since above may have cleared the lock.
924      * If its still locked, still, doesn't exist, still too old, then
925      * don't bother with the message, just die. */
926     io_err = qof_session_get_error (new_session);
927     if ((ERR_BACKEND_LOCKED == io_err) ||
928             (ERR_BACKEND_READONLY == io_err) ||
929             (ERR_BACKEND_NO_SUCH_DB == io_err))
930     {
931         uh_oh = TRUE;
932     }
933 
934     else
935     {
936         uh_oh = show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_OPEN);
937     }
938 
939     if (!uh_oh)
940     {
941         Account *new_root;
942 
943         /* If the new "file" is a database, attempt to store the password
944          * in a keyring. GnuCash itself will not save it.
945          */
946         if ( !gnc_uri_is_file_scheme (scheme))
947             gnc_keyring_set_password ( scheme, hostname, port,
948                                        path, username, password );
949 
950         xaccLogDisable();
951         gnc_window_show_progress(_("Loading user data..."), 0.0);
952         qof_session_load (new_session, gnc_window_show_progress);
953         gnc_window_show_progress(NULL, -1.0);
954         xaccLogEnable();
955 
956         if (is_readonly)
957         {
958             // If the user chose "open read-only" above, make sure to have this
959             // read-only here.
960             qof_book_mark_readonly(qof_session_get_book(new_session));
961         }
962 
963         /* check for i/o error, put up appropriate error dialog */
964         io_err = qof_session_pop_error (new_session);
965 
966         if (io_err == ERR_FILEIO_NO_ENCODING)
967         {
968             if (gnc_xml_convert_single_file (newfile))
969             {
970                 /* try to load once again */
971                 gnc_window_show_progress(_("Loading user data..."), 0.0);
972                 qof_session_load (new_session, gnc_window_show_progress);
973                 gnc_window_show_progress(NULL, -1.0);
974                 xaccLogEnable();
975                 io_err = qof_session_get_error (new_session);
976             }
977             else
978             {
979                 io_err = ERR_FILEIO_PARSE_ERROR;
980             }
981         }
982 
983         uh_oh = show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_OPEN);
984         /* Attempt to update the database if it's too old */
985         if ( !uh_oh && io_err == ERR_SQL_DB_TOO_OLD )
986         {
987             gnc_window_show_progress(_("Re-saving user data..."), 0.0);
988             qof_session_safe_save(new_session, gnc_window_show_progress);
989             io_err = qof_session_get_error(new_session);
990             uh_oh = show_session_error(parent, io_err, newfile, GNC_FILE_DIALOG_SAVE);
991         }
992         /* Database is either too old and couldn't (or user didn't
993          * want it to) be updated or it's too new. Mark it as
994          * read-only
995          */
996         if (uh_oh && (io_err == ERR_SQL_DB_TOO_OLD ||
997                       io_err == ERR_SQL_DB_TOO_NEW))
998         {
999             qof_book_mark_readonly(qof_session_get_book(new_session));
1000             uh_oh = FALSE;
1001         }
1002         new_root = gnc_book_get_root_account (qof_session_get_book (new_session));
1003         if (uh_oh) new_root = NULL;
1004 
1005         /* Umm, came up empty-handed, but no error:
1006          * The backend forgot to set an error. So make one up. */
1007         if (!uh_oh && !new_root)
1008         {
1009             uh_oh = show_session_error (parent, ERR_BACKEND_MISC, newfile,
1010                                         GNC_FILE_DIALOG_OPEN);
1011         }
1012 
1013         /* test for unknown features. */
1014         if (!uh_oh)
1015         {
1016             QofBook *book = qof_session_get_book (new_session);
1017             gchar *msg = gnc_features_test_unknown (book);
1018             Account *template_root = gnc_book_get_template_root (book);
1019 
1020             if (msg)
1021             {
1022                 uh_oh = TRUE;
1023 
1024                 // XXX: should pull out the file name here */
1025                 gnc_error_dialog (parent, msg, "");
1026                 g_free (msg);
1027             }
1028             if (template_root != NULL)
1029             {
1030                 GList *child = NULL;
1031                 GList *children = gnc_account_get_descendants (template_root);
1032 
1033                 for (child = children; child; child = g_list_next (child))
1034                 {
1035                     Account *acc = GNC_ACCOUNT (child->data);
1036                     GList *splits = xaccAccountGetSplitList (acc);
1037                     g_list_foreach (splits,
1038                                     (GFunc)gnc_sx_scrub_split_numerics, NULL);
1039                 }
1040                 g_list_free (children);
1041             }
1042         }
1043     }
1044 
1045     g_free (scheme);
1046     g_free (hostname);
1047     g_free (username);
1048     g_free (password);
1049     g_free (path);
1050 
1051     gnc_unset_busy_cursor (NULL);
1052 
1053     /* going down -- abandon ship */
1054     if (uh_oh)
1055     {
1056         xaccLogDisable();
1057         qof_session_destroy (new_session);
1058         xaccLogEnable();
1059 
1060         /* well, no matter what, I think it's a good idea to have a root
1061          * account around.  For example, early in the gnucash startup
1062          * sequence, the user opens a file; if this open fails for any
1063          * reason, we don't want to leave them high & dry without a root
1064          * account, because if the user continues, then bad things will
1065          * happen. */
1066         gnc_get_current_session ();
1067 
1068         g_free (newfile);
1069 
1070         qof_event_resume ();
1071         gnc_gui_refresh_all ();
1072 
1073         return FALSE;
1074     }
1075 
1076     /* if we got to here, then we've successfully gotten a new session */
1077     /* close up the old file session (if any) */
1078     gnc_set_current_session(new_session);
1079 
1080     /* --------------- END CORE SESSION CODE -------------- */
1081 
1082     /* clean up old stuff, and then we're outta here. */
1083     gnc_add_history (new_session);
1084 
1085     g_free (newfile);
1086 
1087     qof_event_resume ();
1088     gnc_gui_refresh_all ();
1089 
1090     /* Call this after re-enabling events. */
1091     gnc_book_opened ();
1092 
1093     /* Check for account names that may contain the current separator character
1094      * and inform the user if there are any */
1095     new_book = gnc_get_current_book();
1096     invalid_account_names = gnc_account_list_name_violations ( new_book,
1097                             gnc_get_account_separator_string() );
1098     if ( invalid_account_names )
1099     {
1100         gchar *message = gnc_account_name_violations_errmsg ( gnc_get_account_separator_string(),
1101                          invalid_account_names );
1102         gnc_warning_dialog(parent, "%s", message);
1103         g_free ( message );
1104         g_list_free_full (invalid_account_names, g_free);
1105     }
1106 
1107     // Fix account color slots being set to 'Not Set', should run once on a book
1108     qof_event_suspend();
1109     xaccAccountScrubColorNotSet (gnc_get_current_book());
1110     qof_event_resume();
1111 
1112     return TRUE;
1113 }
1114 
1115 /* Routine that pops up a file chooser dialog
1116  *
1117  * Note: this dialog is used when dbi is not enabled
1118  *       so the paths used in here are always file
1119  *       paths, never db uris.
1120  */
1121 gboolean
gnc_file_open(GtkWindow * parent)1122 gnc_file_open (GtkWindow *parent)
1123 {
1124     const gchar * newfile;
1125     gchar *last = NULL;
1126     gchar *default_dir = NULL;
1127     gboolean result;
1128 
1129     if (!gnc_file_query_save (parent, TRUE))
1130         return FALSE;
1131 
1132     if ( last && gnc_uri_targets_local_fs (last))
1133     {
1134         gchar *filepath = gnc_uri_get_path ( last );
1135         default_dir = g_path_get_dirname( filepath );
1136         g_free ( filepath );
1137     }
1138     else
1139         default_dir = gnc_get_default_directory(GNC_PREFS_GROUP_OPEN_SAVE);
1140 
1141     newfile = gnc_file_dialog (parent, _("Open"), NULL, default_dir, GNC_FILE_DIALOG_OPEN);
1142     g_free ( last );
1143     g_free ( default_dir );
1144 
1145     result = gnc_post_file_open (parent, newfile, /*is_readonly*/ FALSE );
1146 
1147     /* This dialogue can show up early in the startup process. If the
1148      * user fails to pick a file (by e.g. hitting the cancel button), we
1149      * might be left with a null topgroup, which leads to nastiness when
1150      * user goes to create their very first account. So create one. */
1151     gnc_get_current_session ();
1152 
1153     return result;
1154 }
1155 
1156 gboolean
gnc_file_open_file(GtkWindow * parent,const char * newfile,gboolean open_readonly)1157 gnc_file_open_file (GtkWindow *parent, const char * newfile, gboolean open_readonly)
1158 {
1159     if (!newfile) return FALSE;
1160 
1161     if (!gnc_file_query_save (parent, TRUE))
1162         return FALSE;
1163 
1164     /* Reset the flag that indicates the conversion of the bayes KVP
1165      * entries has been run */
1166     gnc_account_reset_convert_bayes_to_flat ();
1167 
1168     return gnc_post_file_open (parent, newfile, open_readonly);
1169 }
1170 
1171 /* Note: this dialog will only be used when dbi is not enabled
1172  *       paths used in it always refer to files and are
1173  *       never db uris
1174  */
1175 void
gnc_file_export(GtkWindow * parent)1176 gnc_file_export (GtkWindow *parent)
1177 {
1178     const char *filename;
1179     char *default_dir = NULL;        /* Default to last open */
1180     char *last;
1181 
1182     ENTER(" ");
1183 
1184     last = gnc_history_get_last();
1185     if ( last && gnc_uri_targets_local_fs (last))
1186     {
1187         gchar *filepath = gnc_uri_get_path ( last );
1188         default_dir = g_path_get_dirname( filepath );
1189         g_free ( filepath );
1190     }
1191     else
1192         default_dir = gnc_get_default_directory(GNC_PREFS_GROUP_EXPORT);
1193 
1194     filename = gnc_file_dialog (parent,
1195                                 _("Save"), NULL, default_dir,
1196                                 GNC_FILE_DIALOG_SAVE);
1197     g_free ( last );
1198     g_free ( default_dir );
1199     if (!filename) return;
1200 
1201     gnc_file_do_export (parent, filename);
1202 
1203     LEAVE (" ");
1204 }
1205 
1206 /* Prevent the user from storing or exporting data files into the settings
1207  * directory.
1208  */
1209 static gboolean
check_file_path(const char * path)1210 check_file_path (const char *path)
1211 {
1212     /* Remember the directory as the default. */
1213      gchar *dir = g_path_get_dirname(path);
1214      const gchar *dotgnucash = gnc_userdata_dir();
1215      char *dirpath = dir;
1216 
1217      /* Prevent user from storing file in GnuCash' private configuration
1218       * directory (~/.gnucash by default in linux, but can be overridden)
1219       */
1220      while (strcmp(dir = g_path_get_dirname(dirpath), dirpath) != 0)
1221      {
1222          if (strcmp(dirpath, dotgnucash) == 0)
1223          {
1224              g_free (dir);
1225              g_free (dirpath);
1226              return TRUE;
1227          }
1228          g_free (dirpath);
1229          dirpath = dir;
1230      }
1231      g_free (dirpath);
1232      g_free(dir);
1233      return FALSE;
1234 }
1235 
1236 
1237 void
gnc_file_do_export(GtkWindow * parent,const char * filename)1238 gnc_file_do_export(GtkWindow *parent, const char * filename)
1239 {
1240     QofSession *current_session, *new_session;
1241     gboolean ok;
1242     QofBackendError io_err = ERR_BACKEND_NO_ERR;
1243     gchar *norm_file;
1244     gchar *newfile;
1245     const gchar *oldfile;
1246 
1247     gchar *scheme   = NULL;
1248     gchar *hostname = NULL;
1249     gchar *username = NULL;
1250     gchar *password = NULL;
1251     gchar *path = NULL;
1252     gint32 port = 0;
1253 
1254     ENTER(" ");
1255 
1256     /* Convert user input into a normalized uri
1257      * Note that the normalized uri for internal use can have a password */
1258     norm_file = gnc_uri_normalize_uri ( filename, TRUE );
1259     if (!norm_file)
1260     {
1261         show_session_error (parent, ERR_FILEIO_FILE_NOT_FOUND, filename,
1262                             GNC_FILE_DIALOG_EXPORT);
1263         return;
1264     }
1265 
1266     newfile = gnc_uri_add_extension (norm_file, GNC_DATAFILE_EXT);
1267     g_free (norm_file);
1268     gnc_uri_get_components (newfile, &scheme, &hostname,
1269                             &port, &username, &password, &path);
1270 
1271     /* Save As can't use the generic 'file' protocol. If the user didn't set
1272      * a specific protocol, assume the default 'xml'.
1273      */
1274     if (g_strcmp0 (scheme, "file") == 0)
1275     {
1276         g_free (scheme);
1277         scheme = g_strdup ("xml");
1278         norm_file = gnc_uri_create_uri (scheme, hostname, port,
1279                                         username, password, path);
1280         g_free (newfile);
1281         newfile = norm_file;
1282     }
1283 
1284     /* Some extra steps for file based uri's only
1285      * Note newfile is normalized uri so we can safely call
1286      * gnc_uri_is_file_scheme on it. */
1287     if (gnc_uri_is_file_scheme (scheme))
1288     {
1289         if (check_file_path (path))
1290         {
1291             show_session_error (parent, ERR_FILEIO_RESERVED_WRITE, newfile,
1292                     GNC_FILE_DIALOG_SAVE);
1293             return;
1294         }
1295         gnc_set_default_directory (GNC_PREFS_GROUP_OPEN_SAVE,
1296                        g_path_get_dirname(path));
1297     }
1298     /* Check to see if the user specified the same file as the current
1299      * file. If so, prevent the export from happening to avoid killing this file */
1300     current_session = gnc_get_current_session ();
1301     oldfile = qof_session_get_url(current_session);
1302     if (strlen (oldfile) && (strcmp(oldfile, newfile) == 0))
1303     {
1304         g_free (newfile);
1305         show_session_error (parent, ERR_FILEIO_WRITE_ERROR, filename,
1306                             GNC_FILE_DIALOG_EXPORT);
1307         return;
1308     }
1309 
1310     qof_event_suspend();
1311 
1312     /* -- this session code is NOT identical in FileOpen and FileSaveAs -- */
1313 
1314     new_session = qof_session_new (NULL);
1315     qof_session_begin (new_session, newfile, SESSION_NEW_STORE);
1316 
1317     io_err = qof_session_get_error (new_session);
1318     /* If the file exists and would be clobbered, ask the user */
1319     if (ERR_BACKEND_STORE_EXISTS == io_err)
1320     {
1321         const char *format = _("The file %s already exists. "
1322                                "Are you sure you want to overwrite it?");
1323 
1324         const char *name;
1325         if ( gnc_uri_is_file_uri ( newfile ) )
1326             name = gnc_uri_get_path ( newfile );
1327         else
1328             name = gnc_uri_normalize_uri ( newfile, FALSE );
1329         /* if user says cancel, we should break out */
1330         if (!gnc_verify_dialog (parent, FALSE, format, name))
1331         {
1332             return;
1333         }
1334         qof_session_begin (new_session, newfile, SESSION_NEW_OVERWRITE);
1335     }
1336     /* if file appears to be locked, ask the user ... */
1337     if (ERR_BACKEND_LOCKED == io_err || ERR_BACKEND_READONLY == io_err)
1338     {
1339         if (!show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_EXPORT))
1340         {
1341             /* user told us to ignore locks. So ignore them. */
1342             qof_session_begin (new_session, newfile, SESSION_BREAK_LOCK);
1343         }
1344     }
1345 
1346     /* --------------- END CORE SESSION CODE -------------- */
1347 
1348     /* use the current session to save to file */
1349     gnc_set_busy_cursor (NULL, TRUE);
1350     gnc_window_show_progress(_("Exporting file..."), 0.0);
1351     ok = qof_session_export (new_session, current_session,
1352                              gnc_window_show_progress);
1353     gnc_window_show_progress(NULL, -1.0);
1354     gnc_unset_busy_cursor (NULL);
1355     xaccLogDisable();
1356     qof_session_destroy (new_session);
1357     xaccLogEnable();
1358     qof_event_resume();
1359 
1360     if (!ok)
1361     {
1362         /* %s is the strerror(3) error string of the error that occurred. */
1363         const char *format = _("There was an error saving the file.\n\n%s");
1364 
1365         gnc_error_dialog (parent, format, strerror(errno));
1366         return;
1367     }
1368 }
1369 
1370 static gboolean been_here_before = FALSE;
1371 
1372 void
gnc_file_save(GtkWindow * parent)1373 gnc_file_save (GtkWindow *parent)
1374 {
1375     QofBackendError io_err;
1376     const char * newfile;
1377     QofSession *session;
1378     ENTER (" ");
1379 
1380     if (!gnc_current_session_exist ())
1381         return; //No session means nothing to save.
1382 
1383     /* hack alert -- Somehow make sure all in-progress edits get committed! */
1384 
1385     /* If we don't have a filename/path to save to get one. */
1386     session = gnc_get_current_session ();
1387 
1388     if (!strlen (qof_session_get_url (session)))
1389     {
1390         gnc_file_save_as (parent);
1391         return;
1392     }
1393 
1394     if (qof_book_is_readonly(qof_session_get_book(session)))
1395     {
1396         gint response = gnc_ok_cancel_dialog(parent,
1397                                              GTK_RESPONSE_CANCEL,
1398                                              _("The database was opened read-only. "
1399                                                "Do you want to save it to a different place?"));
1400         if (response == GTK_RESPONSE_OK)
1401         {
1402             gnc_file_save_as (parent);
1403         }
1404         return;
1405     }
1406 
1407     /* use the current session to save to file */
1408     save_in_progress++;
1409     gnc_set_busy_cursor (NULL, TRUE);
1410     gnc_window_show_progress(_("Writing file..."), 0.0);
1411     qof_session_save (session, gnc_window_show_progress);
1412     gnc_window_show_progress(NULL, -1.0);
1413     gnc_unset_busy_cursor (NULL);
1414     save_in_progress--;
1415 
1416     /* Make sure everything's OK - disk could be full, file could have
1417        become read-only etc. */
1418     io_err = qof_session_get_error (session);
1419     if (ERR_BACKEND_NO_ERR != io_err)
1420     {
1421         newfile = qof_session_get_url(session);
1422         show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_SAVE);
1423 
1424         if (been_here_before) return;
1425         been_here_before = TRUE;
1426         gnc_file_save_as (parent);   /* been_here prevents infinite recursion */
1427         been_here_before = FALSE;
1428         return;
1429     }
1430 
1431     xaccReopenLog();
1432     gnc_add_history (session);
1433     gnc_hook_run(HOOK_BOOK_SAVED, session);
1434     LEAVE (" ");
1435 }
1436 
1437 /* Note: this dialog will only be used when dbi is not enabled
1438  *       paths used in it always refer to files and are
1439  *       never db uris. See gnc_file_do_save_as for that.
1440  */
1441 void
gnc_file_save_as(GtkWindow * parent)1442 gnc_file_save_as (GtkWindow *parent)
1443 {
1444     const gchar *filename;
1445     gchar *default_dir = NULL;        /* Default to last open */
1446     gchar *last;
1447 
1448     ENTER(" ");
1449 
1450     if (!gnc_current_session_exist ())
1451     {
1452         LEAVE("No Session.");
1453         return;
1454     }
1455 
1456     last = gnc_history_get_last();
1457     if ( last && gnc_uri_targets_local_fs (last))
1458     {
1459         gchar *filepath = gnc_uri_get_path ( last );
1460         default_dir = g_path_get_dirname( filepath );
1461         g_free ( filepath );
1462     }
1463     else
1464         default_dir = gnc_get_default_directory(GNC_PREFS_GROUP_OPEN_SAVE);
1465 
1466     filename = gnc_file_dialog (parent,
1467                                 _("Save"), NULL, default_dir,
1468                                 GNC_FILE_DIALOG_SAVE);
1469     g_free ( last );
1470     g_free ( default_dir );
1471     if (!filename) return;
1472 
1473     gnc_file_do_save_as (parent, filename);
1474 
1475     LEAVE (" ");
1476 }
1477 
1478 void
gnc_file_do_save_as(GtkWindow * parent,const char * filename)1479 gnc_file_do_save_as (GtkWindow *parent, const char* filename)
1480 {
1481     QofSession *new_session;
1482     QofSession *session;
1483     gchar *norm_file;
1484     gchar *newfile;
1485     const gchar *oldfile;
1486 
1487     gchar *scheme   = NULL;
1488     gchar *hostname = NULL;
1489     gchar *username = NULL;
1490     gchar *password = NULL;
1491     gchar *path = NULL;
1492     gint32 port = 0;
1493 
1494 
1495     QofBackendError io_err = ERR_BACKEND_NO_ERR;
1496 
1497     ENTER(" ");
1498 
1499     /* Convert user input into a normalized uri
1500      * Note that the normalized uri for internal use can have a password */
1501     norm_file = gnc_uri_normalize_uri ( filename, TRUE );
1502     if (!norm_file)
1503     {
1504         show_session_error (parent, ERR_FILEIO_FILE_NOT_FOUND, filename,
1505                             GNC_FILE_DIALOG_SAVE);
1506         return;
1507     }
1508 
1509     newfile = gnc_uri_add_extension (norm_file, GNC_DATAFILE_EXT);
1510     g_free (norm_file);
1511     gnc_uri_get_components (newfile, &scheme, &hostname,
1512                             &port, &username, &password, &path);
1513 
1514     /* Save As can't use the generic 'file' protocol. If the user didn't set
1515      * a specific protocol, assume the default 'xml'.
1516      */
1517     if (g_strcmp0 (scheme, "file") == 0)
1518     {
1519         g_free (scheme);
1520         scheme = g_strdup ("xml");
1521         norm_file = gnc_uri_create_uri (scheme, hostname, port,
1522                                         username, password, path);
1523         g_free (newfile);
1524         newfile = norm_file;
1525     }
1526 
1527     /* Some extra steps for file based uri's only
1528      * Note newfile is normalized uri so we can safely call
1529      * gnc_uri_is_file_scheme on it. */
1530     if (gnc_uri_is_file_scheme (scheme))
1531     {
1532         if (check_file_path (path))
1533         {
1534             show_session_error (parent, ERR_FILEIO_RESERVED_WRITE, newfile,
1535                     GNC_FILE_DIALOG_SAVE);
1536             return;
1537         }
1538         gnc_set_default_directory (GNC_PREFS_GROUP_OPEN_SAVE,
1539                        g_path_get_dirname (path));
1540     }
1541 
1542     /* Check to see if the user specified the same file as the current
1543      * file. If so, then just do a simple save, instead of a full save as */
1544     session = gnc_get_current_session ();
1545     oldfile = qof_session_get_url(session);
1546     if (strlen (oldfile) && (strcmp(oldfile, newfile) == 0))
1547     {
1548         g_free (newfile);
1549         gnc_file_save (parent);
1550         return;
1551     }
1552 
1553     /* Make sure all of the data from the old file is loaded */
1554     qof_session_ensure_all_data_loaded(session);
1555 
1556     /* -- this session code is NOT identical in FileOpen and FileSaveAs -- */
1557 
1558     save_in_progress++;
1559 
1560     new_session = qof_session_new (NULL);
1561     qof_session_begin (new_session, newfile, SESSION_NEW_STORE);
1562 
1563     io_err = qof_session_get_error (new_session);
1564 
1565     /* If the file exists and would be clobbered, ask the user */
1566     if (ERR_BACKEND_STORE_EXISTS == io_err)
1567     {
1568         const char *format = _("The file %s already exists. "
1569                                "Are you sure you want to overwrite it?");
1570 
1571         const char *name;
1572         if ( gnc_uri_is_file_uri ( newfile ) )
1573             name = gnc_uri_get_path ( newfile );
1574         else
1575             name = gnc_uri_normalize_uri ( newfile, FALSE );
1576 
1577         /* if user says cancel, we should break out */
1578         if (!gnc_verify_dialog (parent, FALSE, format, name ))
1579         {
1580             xaccLogDisable();
1581             qof_session_destroy (new_session);
1582             xaccLogEnable();
1583             g_free (newfile);
1584             save_in_progress--;
1585             return;
1586         }
1587         qof_session_begin (new_session, newfile, SESSION_NEW_OVERWRITE);
1588     }
1589     /* if file appears to be locked, ask the user ... */
1590     else if (ERR_BACKEND_LOCKED == io_err || ERR_BACKEND_READONLY == io_err)
1591     {
1592         if (!show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_SAVE))
1593         {
1594             // User wants to replace the file.
1595             qof_session_begin (new_session, newfile, SESSION_BREAK_LOCK);
1596         }
1597     }
1598 
1599     /* if the database doesn't exist, ask the user ... */
1600     else if ((ERR_FILEIO_FILE_NOT_FOUND == io_err) ||
1601              (ERR_BACKEND_NO_SUCH_DB == io_err) ||
1602              (ERR_SQL_DB_TOO_OLD == io_err))
1603     {
1604         if (!show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_SAVE))
1605         {
1606             /* user told us to create a new database. Do it. */
1607             qof_session_begin (new_session, newfile, SESSION_NEW_STORE);
1608         }
1609     }
1610 
1611     /* check again for session errors (since above dialog may have
1612      * cleared a file lock & moved things forward some more)
1613      * This time, errors will be fatal.
1614      */
1615     io_err = qof_session_get_error (new_session);
1616     if (ERR_BACKEND_NO_ERR != io_err)
1617     {
1618         show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_SAVE);
1619         xaccLogDisable();
1620         qof_session_destroy (new_session);
1621         xaccLogEnable();
1622         g_free (newfile);
1623         save_in_progress--;
1624         return;
1625     }
1626 
1627     /* If the new "file" is a database, attempt to store the password
1628      * in a keyring. GnuCash itself will not save it.
1629      */
1630     if ( !gnc_uri_is_file_scheme (scheme))
1631         gnc_keyring_set_password ( scheme, hostname, port,
1632                                    path, username, password );
1633 
1634     /* Prevent race condition between swapping the contents of the two
1635      * sessions, and actually installing the new session as the current
1636      * one. Any event callbacks that occur in this interval will have
1637      * problems if they check for the current book. */
1638     qof_event_suspend();
1639 
1640     /* if we got to here, then we've successfully gotten a new session */
1641     /* close up the old file session (if any) */
1642     qof_session_swap_data (session, new_session);
1643     qof_book_mark_session_dirty (qof_session_get_book (new_session));
1644 
1645     qof_event_resume();
1646 
1647 
1648     gnc_set_busy_cursor (NULL, TRUE);
1649     gnc_window_show_progress(_("Writing file..."), 0.0);
1650     qof_session_save (new_session, gnc_window_show_progress);
1651     gnc_window_show_progress(NULL, -1.0);
1652     gnc_unset_busy_cursor (NULL);
1653 
1654     io_err = qof_session_get_error( new_session );
1655     if ( ERR_BACKEND_NO_ERR != io_err )
1656     {
1657         /* Well, poop. The save failed, so the new session is invalid and we
1658          * need to restore the old one.
1659          */
1660         show_session_error (parent, io_err, newfile, GNC_FILE_DIALOG_SAVE);
1661         qof_event_suspend();
1662         qof_session_swap_data( new_session, session );
1663         qof_session_destroy( new_session );
1664         new_session = NULL;
1665         qof_event_resume();
1666     }
1667     else
1668     {
1669         /* Yay! Save was successful, we can dump the old session */
1670         qof_event_suspend();
1671         gnc_clear_current_session();
1672         gnc_set_current_session( new_session );
1673         qof_event_resume();
1674         session = NULL;
1675 
1676         xaccReopenLog();
1677         gnc_add_history (new_session);
1678         gnc_hook_run(HOOK_BOOK_SAVED, new_session);
1679     }
1680     /* --------------- END CORE SESSION CODE -------------- */
1681 
1682     save_in_progress--;
1683 
1684     g_free (newfile);
1685     LEAVE (" ");
1686 }
1687 
1688 void
gnc_file_revert(GtkWindow * parent)1689 gnc_file_revert (GtkWindow *parent)
1690 {
1691     QofSession *session;
1692     const gchar *fileurl, *filename, *tmp;
1693     const gchar *title = _("Reverting will discard all unsaved changes to %s. Are you sure you want to proceed?");
1694 
1695     if (!gnc_main_window_all_finish_pending())
1696         return;
1697 
1698     session = gnc_get_current_session();
1699     fileurl = qof_session_get_url(session);
1700     if (!strlen (fileurl))
1701         fileurl = _("<unknown>");
1702     if ((tmp = strrchr(fileurl, '/')) != NULL)
1703         filename = tmp + 1;
1704     else
1705         filename = fileurl;
1706 
1707     if (!gnc_verify_dialog (parent, FALSE, title, filename))
1708         return;
1709 
1710     qof_book_mark_session_saved (qof_session_get_book (session));
1711     gnc_file_open_file (parent, fileurl, qof_book_is_readonly(gnc_get_current_book()));}
1712 
1713 void
gnc_file_quit(void)1714 gnc_file_quit (void)
1715 {
1716     QofSession *session;
1717 
1718     if (!gnc_current_session_exist ())
1719         return;
1720     gnc_set_busy_cursor (NULL, TRUE);
1721     session = gnc_get_current_session ();
1722 
1723     /* disable events; otherwise the mass deletion of accounts and
1724      * transactions during shutdown would cause massive redraws */
1725     qof_event_suspend ();
1726 
1727     gnc_hook_run(HOOK_BOOK_CLOSED, session);
1728     gnc_close_gui_component_by_session (session);
1729     gnc_state_save (session);
1730     gnc_clear_current_session();
1731 
1732     qof_event_resume ();
1733     gnc_unset_busy_cursor (NULL);
1734 }
1735 
1736 void
gnc_file_set_shutdown_callback(GNCShutdownCB cb)1737 gnc_file_set_shutdown_callback (GNCShutdownCB cb)
1738 {
1739     shutdown_cb = cb;
1740 }
1741 
1742 gboolean
gnc_file_save_in_progress(void)1743 gnc_file_save_in_progress (void)
1744 {
1745     if (gnc_current_session_exist())
1746     {
1747         QofSession *session = gnc_get_current_session();
1748         return (qof_session_save_in_progress(session) || save_in_progress > 0);
1749     }
1750     return FALSE;
1751 }
1752