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