1 /********************************************************************\
2  * dialog-doclink-utils.c -- Document link dialog Utils             *
3  * Copyright (C) 2020 Robert Fewell                                 *
4  *                                                                  *
5  * This program is free software; you can redistribute it and/or    *
6  * modify it under the terms of the GNU General Public License as   *
7  * published by the Free Software Foundation; either version 2 of   *
8  * the License, or (at your option) any later version.              *
9  *                                                                  *
10  * This program is distributed in the hope that it will be useful,  *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
13  * GNU General Public License for more details.                     *
14  *                                                                  *
15  * You should have received a copy of the GNU General Public License*
16  * along with this program; if not, contact:                        *
17  *                                                                  *
18  * Free Software Foundation           Voice:  +1-617-542-5942       *
19  * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
20  * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
21 \********************************************************************/
22 
23 #include <config.h>
24 
25 #include <gtk/gtk.h>
26 #include <glib/gi18n.h>
27 
28 #include "dialog-doclink-utils.h"
29 
30 #include "dialog-utils.h"
31 #include "Transaction.h"
32 #include "gncInvoice.h"
33 
34 #include "gnc-prefs.h"
35 #include "gnc-ui.h"
36 #include "gnc-ui-util.h"
37 #include "gnc-gnome-utils.h"
38 #include "gnc-uri-utils.h"
39 #include "gnc-filepath-utils.h"
40 #include "Account.h"
41 
42 /* This static indicates the debugging module that this .o belongs to. */
43 static QofLogModule log_module = GNC_MOD_GUI;
44 
45 /* =================================================================== */
46 
47 static gchar *
convert_uri_to_abs_path(const gchar * path_head,const gchar * uri,gchar * uri_scheme,gboolean return_uri)48 convert_uri_to_abs_path (const gchar *path_head, const gchar *uri,
49                          gchar *uri_scheme, gboolean return_uri)
50 {
51     gchar *ret_value = NULL;
52 
53     if (!uri_scheme) // relative path
54     {
55         gchar *path = gnc_uri_get_path (path_head);
56         gchar *file_path = gnc_file_path_absolute (path, uri);
57 
58         if (return_uri)
59             ret_value = gnc_uri_create_uri ("file", NULL, 0, NULL, NULL, file_path);
60         else
61             ret_value = g_strdup (file_path);
62 
63         g_free (path);
64         g_free (file_path);
65     }
66 
67     if (g_strcmp0 (uri_scheme, "file") == 0) // absolute path
68     {
69         if (return_uri)
70             ret_value = g_strdup (uri);
71         else
72             ret_value = gnc_uri_get_path (uri);
73     }
74     return ret_value;
75 }
76 
77 gchar *
gnc_doclink_get_unescape_uri(const gchar * path_head,const gchar * uri,gchar * uri_scheme)78 gnc_doclink_get_unescape_uri (const gchar *path_head, const gchar *uri, gchar *uri_scheme)
79 {
80     gchar *display_str = NULL;
81 
82     if (uri && *uri)
83     {
84         // if scheme is null or 'file' we should get a file path
85         gchar *file_path = convert_uri_to_abs_path (path_head, uri, uri_scheme, FALSE);
86 
87         if (file_path)
88             display_str = g_uri_unescape_string (file_path, NULL);
89         else
90             display_str = g_uri_unescape_string (uri, NULL);
91 
92         g_free (file_path);
93 
94 #ifdef G_OS_WIN32 // make path look like a traditional windows path
95         g_strdelimit (display_str, "/", '\\');
96 #endif
97     }
98     DEBUG("Return display string is '%s'", display_str);
99     return display_str;
100 }
101 
102 gchar *
gnc_doclink_get_use_uri(const gchar * path_head,const gchar * uri,gchar * uri_scheme)103 gnc_doclink_get_use_uri (const gchar *path_head, const gchar *uri, gchar *uri_scheme)
104 {
105     gchar *use_str = NULL;
106 
107     if (uri && *uri)
108     {
109         // if scheme is null or 'file' we should get a file path
110         gchar *file_path = convert_uri_to_abs_path (path_head, uri, uri_scheme, TRUE);
111 
112         if (file_path)
113             use_str = g_strdup (file_path);
114         else
115             use_str = g_strdup (uri);
116 
117         g_free (file_path);
118     }
119     DEBUG("Return use string is '%s'", use_str);
120     return use_str;
121 }
122 
123 gchar *
gnc_doclink_get_unescaped_just_uri(const gchar * uri)124 gnc_doclink_get_unescaped_just_uri (const gchar *uri)
125 {
126     gchar *path_head = gnc_doclink_get_path_head ();
127     gchar *uri_scheme = gnc_uri_get_scheme (uri);
128     gchar *ret_uri = gnc_doclink_get_unescape_uri (path_head, uri, uri_scheme);
129 
130     g_free (path_head);
131     g_free (uri_scheme);
132     return ret_uri;
133 }
134 
135 gchar *
gnc_doclink_convert_trans_link_uri(gpointer trans,gboolean book_ro)136 gnc_doclink_convert_trans_link_uri (gpointer trans, gboolean book_ro)
137 {
138     const gchar *uri = xaccTransGetDocLink (trans); // get the existing uri
139     const gchar *part = NULL;
140 
141     if (!uri)
142         return NULL;
143 
144     if (g_str_has_prefix (uri, "file:") && !g_str_has_prefix (uri,"file://"))
145     {
146         /* fix an earlier error when storing relative paths before version 3.5
147          * they were stored starting as 'file:' or 'file:/' depending on OS
148          * relative paths are stored without a leading "/" and in native form
149          */
150         if (g_str_has_prefix (uri,"file:/"))
151             part = uri + strlen ("file:/");
152         else if (g_str_has_prefix (uri,"file:"))
153             part = uri + strlen ("file:");
154 
155         if (!xaccTransGetReadOnly (trans) && !book_ro)
156             xaccTransSetDocLink (trans, part);
157 
158         return g_strdup (part);
159     }
160     return g_strdup (uri);
161 }
162 
163 /* =================================================================== */
164 
165 static gchar *
doclink_get_path_head_and_set(gboolean * path_head_set)166 doclink_get_path_head_and_set (gboolean *path_head_set)
167 {
168     gchar *ret_path = NULL;
169     gchar *path_head = gnc_prefs_get_string (GNC_PREFS_GROUP_GENERAL, GNC_DOC_LINK_PATH_HEAD);
170     *path_head_set = FALSE;
171 
172     if (path_head && *path_head) // not default entry
173     {
174         *path_head_set = TRUE;
175         ret_path = g_strdup (path_head);
176     }
177     else
178     {
179         const gchar *doc = g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS);
180 
181         if (doc)
182             ret_path = gnc_uri_create_uri ("file", NULL, 0, NULL, NULL, doc);
183         else
184             ret_path = gnc_uri_create_uri ("file", NULL, 0, NULL, NULL, gnc_userdata_dir ());
185     }
186     // make sure there is a trailing '/'
187     if (!g_str_has_suffix (ret_path, "/"))
188     {
189         gchar *folder_with_slash = g_strconcat (ret_path, "/", NULL);
190         g_free (ret_path);
191         ret_path = g_strdup (folder_with_slash);
192         g_free (folder_with_slash);
193 
194         if (*path_head_set) // prior to 3.5, assoc-head could be with or without a trailing '/'
195         {
196             if (!gnc_prefs_set_string (GNC_PREFS_GROUP_GENERAL, GNC_DOC_LINK_PATH_HEAD, ret_path))
197                 PINFO ("Failed to save preference at %s, %s with %s",
198                        GNC_PREFS_GROUP_GENERAL, GNC_DOC_LINK_PATH_HEAD, ret_path);
199         }
200     }
201     g_free (path_head);
202     return ret_path;
203 }
204 
205 gchar *
gnc_doclink_get_path_head(void)206 gnc_doclink_get_path_head (void)
207 {
208     gboolean path_head_set = FALSE;
209 
210     return doclink_get_path_head_and_set (&path_head_set);
211 }
212 
213 void
gnc_doclink_set_path_head_label(GtkWidget * path_head_label,const gchar * incoming_path_head,const gchar * prefix)214 gnc_doclink_set_path_head_label (GtkWidget *path_head_label, const gchar *incoming_path_head, const gchar *prefix)
215 {
216     gboolean path_head_set = FALSE;
217     gchar *path_head = NULL;
218     gchar *scheme;
219     gchar *path_head_str;
220     gchar *path_head_text;
221 
222     if (incoming_path_head)
223     {
224          path_head = g_strdup (incoming_path_head);
225          path_head_set = TRUE;
226     }
227     else
228         path_head = doclink_get_path_head_and_set (&path_head_set);
229 
230     scheme = gnc_uri_get_scheme (path_head);
231     path_head_str = gnc_doclink_get_unescape_uri (NULL, path_head, scheme);
232 
233     if (path_head_set)
234     {
235         // test for current folder being present
236         if (g_file_test (path_head_str, G_FILE_TEST_IS_DIR))
237             path_head_text = g_strdup_printf ("%s '%s'", _("Path head for files is,"), path_head_str);
238         else
239             path_head_text = g_strdup_printf ("%s '%s'", _("Path head does not exist,"), path_head_str);
240     }
241     else
242         path_head_text = g_strdup_printf (_("Path head not set, using '%s' for relative paths"), path_head_str);
243 
244     if (prefix)
245     {
246         gchar *tmp = g_strdup (path_head_text);
247         g_free (path_head_text);
248 
249         path_head_text = g_strdup_printf ("%s %s", prefix, tmp);
250 
251         g_free (tmp);
252     }
253 
254     gtk_label_set_text (GTK_LABEL(path_head_label), path_head_text);
255 
256     // Set the style context for this label so it can be easily manipulated with css
257     gnc_widget_style_context_add_class (GTK_WIDGET(path_head_label), "gnc-class-highlight");
258 
259     g_free (scheme);
260     g_free (path_head_str);
261     g_free (path_head_text);
262     g_free (path_head);
263 }
264 
265 /* =================================================================== */
266 
267 typedef struct
268 {
269     const gchar *old_path_head_uri;
270     gboolean     change_old;
271     const gchar *new_path_head_uri;
272     gboolean     change_new;
273     gboolean     book_ro;
274 }DoclinkUpdate;
275 
276 static void
update_invoice_uri(QofInstance * data,gpointer user_data)277 update_invoice_uri (QofInstance* data, gpointer user_data)
278 {
279     DoclinkUpdate *doclink_update = user_data;
280     GncInvoice *invoice = GNC_INVOICE(data);
281     const gchar* uri = gncInvoiceGetDocLink (invoice);
282 
283     if (uri && *uri)
284     {
285         gboolean rel = FALSE;
286         gchar *scheme = gnc_uri_get_scheme (uri);
287 
288         if (!scheme) // path is relative
289             rel = TRUE;
290 
291         // check for relative and we want to change them
292         if (rel && doclink_update->change_old)
293         {
294             gchar *new_uri = gnc_doclink_get_use_uri (doclink_update->old_path_head_uri, uri, scheme);
295             gncInvoiceSetDocLink (invoice, new_uri);
296             g_free (new_uri);
297         }
298         g_free (scheme);
299 
300         // check for not relative and we want to change them
301         if (!rel && doclink_update->change_new && g_str_has_prefix (uri, doclink_update->new_path_head_uri))
302         {
303             // relative paths do not start with a '/'
304             const gchar *part = uri + strlen (doclink_update->new_path_head_uri);
305             gchar *new_uri = g_strdup (part);
306 
307             gncInvoiceSetDocLink (invoice, new_uri);
308             g_free (new_uri);
309         }
310     }
311 }
312 
313 static void
update_trans_uri(QofInstance * data,gpointer user_data)314 update_trans_uri (QofInstance* data, gpointer user_data)
315 {
316     DoclinkUpdate *doclink_update = user_data;
317     Transaction *trans = GNC_TRANSACTION(data);
318     gchar *uri;
319 
320     // fix an earlier error when storing relative paths before version 3.5
321     uri = gnc_doclink_convert_trans_link_uri (trans, doclink_update->book_ro);
322 
323     if (uri && *uri)
324     {
325         gboolean rel = FALSE;
326         gchar *scheme = gnc_uri_get_scheme (uri);
327 
328         if (!scheme) // path is relative
329             rel = TRUE;
330 
331         // check for relative and we want to change them
332         if (rel && doclink_update->change_old)
333         {
334             gchar *new_uri = gnc_doclink_get_use_uri (doclink_update->old_path_head_uri, uri, scheme);
335 
336             if (!xaccTransGetReadOnly (trans))
337                 xaccTransSetDocLink (trans, new_uri);
338 
339             g_free (new_uri);
340         }
341         g_free (scheme);
342 
343         // check for not relative and we want to change them
344         if (!rel && doclink_update->change_new && g_str_has_prefix (uri, doclink_update->new_path_head_uri))
345         {
346             // relative paths do not start with a '/'
347             const gchar *part = uri + strlen (doclink_update->new_path_head_uri);
348             gchar *new_uri = g_strdup (part);
349 
350             if (!xaccTransGetReadOnly (trans))
351                 xaccTransSetDocLink (trans, new_uri);
352 
353             g_free (new_uri);
354         }
355     }
356     g_free (uri);
357 }
358 
359 static void
change_relative_and_absolute_uri_paths(const gchar * old_path_head_uri,gboolean change_old,const gchar * new_path_head_uri,gboolean change_new)360 change_relative_and_absolute_uri_paths (const gchar *old_path_head_uri, gboolean change_old,
361                                         const gchar *new_path_head_uri, gboolean change_new)
362 {
363     QofBook      *book = gnc_get_current_book();
364     gboolean      book_ro = qof_book_is_readonly (book);
365     DoclinkUpdate  *doclink_update;
366 
367     /* if book is read only, nothing to do */
368     if (book_ro)
369         return;
370 
371     doclink_update = g_new0 (DoclinkUpdate, 1);
372 
373     doclink_update->old_path_head_uri = old_path_head_uri;
374     doclink_update->new_path_head_uri = new_path_head_uri;
375     doclink_update->change_old = change_old;
376     doclink_update->change_new = change_new;
377     doclink_update->book_ro = book_ro;
378 
379     /* Loop through the transactions */
380     qof_collection_foreach (qof_book_get_collection (book, GNC_ID_TRANS),
381                             update_trans_uri, doclink_update);
382 
383     /* Loop through the invoices */
384     qof_collection_foreach (qof_book_get_collection (book, GNC_ID_INVOICE),
385                             update_invoice_uri, doclink_update);
386 
387     g_free (doclink_update);
388 }
389 
390 void
gnc_doclink_pref_path_head_changed(GtkWindow * parent,const gchar * old_path_head_uri)391 gnc_doclink_pref_path_head_changed (GtkWindow *parent, const gchar *old_path_head_uri)
392 {
393     GtkWidget  *dialog;
394     GtkBuilder *builder;
395     GtkWidget  *use_old_path_head, *use_new_path_head;
396     GtkWidget  *old_head_label, *new_head_label;
397     GtkWidget  *old_hbox, *new_hbox;
398     gint        result;
399     gchar      *new_path_head_uri = gnc_doclink_get_path_head ();
400 
401     if (g_strcmp0 (old_path_head_uri, new_path_head_uri) == 0)
402     {
403         g_free (new_path_head_uri);
404         return;
405     }
406 
407     /* Create the dialog box */
408     builder = gtk_builder_new();
409     gnc_builder_add_from_file (builder, "dialog-doclink.glade", "link_path_head_changed_dialog");
410     dialog = GTK_WIDGET(gtk_builder_get_object (builder, "link_path_head_changed_dialog"));
411 
412     if (parent != NULL)
413         gtk_window_set_transient_for (GTK_WINDOW(dialog), GTK_WINDOW(parent));
414 
415     // Set the name and style context for this widget so it can be easily manipulated with css
416     gtk_widget_set_name (GTK_WIDGET(dialog), "gnc-id-doclink-change");
417     gnc_widget_style_context_add_class (GTK_WIDGET(dialog), "gnc-class-doclink");
418 
419     old_head_label = GTK_WIDGET(gtk_builder_get_object (builder, "existing_path_head"));
420     new_head_label = GTK_WIDGET(gtk_builder_get_object (builder, "new_path_head"));
421 
422     use_old_path_head = GTK_WIDGET(gtk_builder_get_object (builder, "use_old_path_head"));
423     use_new_path_head = GTK_WIDGET(gtk_builder_get_object (builder, "use_new_path_head"));
424 
425     // display path head text and test if present
426     gnc_doclink_set_path_head_label (old_head_label, old_path_head_uri, _("Existing"));
427     gnc_doclink_set_path_head_label (new_head_label, new_path_head_uri, _("New"));
428 
429     gtk_widget_show (dialog);
430     g_object_unref (G_OBJECT(builder));
431 
432     // run the dialog
433     result = gtk_dialog_run (GTK_DIALOG(dialog));
434     if (result == GTK_RESPONSE_OK)
435     {
436         gboolean use_old = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(use_old_path_head));
437         gboolean use_new = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(use_new_path_head));
438 
439         if (use_old || use_new)
440             change_relative_and_absolute_uri_paths (old_path_head_uri, use_old,
441                                                     new_path_head_uri, use_new);
442     }
443     g_free (new_path_head_uri);
444     gtk_widget_destroy (dialog);
445 }
446