1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 2016 The Claws Mail Team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (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, see <http://www.gnu.org/licenses/>.
17  *
18  */
19 
20 #ifdef HAVE_CONFIG_H
21 #  include "config.h"
22 #endif
23 
24 #define UNICODE
25 #define _UNICODE
26 
27 #include <glib.h>
28 #include <glib/gi18n.h>
29 #include <gdk/gdkwin32.h>
30 #include <pthread.h>
31 
32 #include <windows.h>
33 #include <shlobj.h>
34 
35 #include "claws.h"
36 #include "alertpanel.h"
37 #include "manage_window.h"
38 #include "utils.h"
39 
40 static OPENFILENAME o;
41 static BROWSEINFO b;
42 
43 /* Since running the native dialogs in the same thread stops GTK+
44  * loop from redrawing other windows on the background, we need
45  * to run the dialogs in a separate thread. */
46 
47 /* TODO: There's a lot of code repeat in this file, it could be
48  * refactored to be neater. */
49 
50 struct _WinChooserCtx {
51 	void *data;
52 	gboolean return_value;
53 	PIDLIST_ABSOLUTE return_value_pidl;
54 	gboolean done;
55 };
56 
57 typedef struct _WinChooserCtx WinChooserCtx;
58 
threaded_GetOpenFileName(void * arg)59 static void *threaded_GetOpenFileName(void *arg)
60 {
61 	WinChooserCtx *ctx = (WinChooserCtx *)arg;
62 
63 	g_return_val_if_fail(ctx != NULL, NULL);
64 	g_return_val_if_fail(ctx->data != NULL, NULL);
65 
66 	ctx->return_value = GetOpenFileName(ctx->data);
67 	ctx->done = TRUE;
68 
69 	return NULL;
70 }
71 
threaded_GetSaveFileName(void * arg)72 static void *threaded_GetSaveFileName(void *arg)
73 {
74 	WinChooserCtx *ctx = (WinChooserCtx *)arg;
75 
76 	g_return_val_if_fail(ctx != NULL, NULL);
77 	g_return_val_if_fail(ctx->data != NULL, NULL);
78 
79 	ctx->return_value = GetSaveFileName(ctx->data);
80 	ctx->done = TRUE;
81 
82 	return NULL;
83 }
84 
threaded_SHBrowseForFolder(void * arg)85 static void *threaded_SHBrowseForFolder(void *arg)
86 {
87 	WinChooserCtx *ctx = (WinChooserCtx *)arg;
88 
89 	g_return_val_if_fail(ctx != NULL, NULL);
90 	g_return_val_if_fail(ctx->data != NULL, NULL);
91 
92 	ctx->return_value_pidl = SHBrowseForFolder(ctx->data);
93 
94 	ctx->done = TRUE;
95 
96 	return NULL;
97 }
98 
99 /* This function handles calling GetOpenFilename(), using
100  * global static variable o.
101  * It expects o.lpstrFile to point to an already allocated buffer,
102  * of size at least MAXPATHLEN. */
_file_open_dialog(const gchar * path,const gchar * title,const gchar * filter,const gboolean multi)103 static const gboolean _file_open_dialog(const gchar *path, const gchar *title,
104 		const gchar *filter, const gboolean multi)
105 {
106 	gboolean ret;
107 	gunichar2 *path16 = NULL;
108 	gunichar2 *title16 = NULL;
109 	gunichar2 *filter16 = NULL;
110 	gunichar2 *win_filter16 = NULL;
111 	glong conv_items, sz;
112 	GError *error = NULL;
113 	WinChooserCtx *ctx;
114 #ifdef USE_PTHREAD
115 	pthread_t pt;
116 #endif
117 
118 	/* Path needs to be converted to UTF-16, so that the native chooser
119 	 * can understand it. */
120 	path16 = g_utf8_to_utf16(path ? path : "",
121 			-1, NULL, NULL, &error);
122 	if (error != NULL) {
123 		alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
124 				error->message);
125 		debug_print("file path '%s' conversion to UTF-16 failed\n", path);
126 		g_error_free(error);
127 		error = NULL;
128 		return FALSE;
129 	}
130 
131 	/* Chooser dialog title needs to be UTF-16 as well. */
132 	title16 = g_utf8_to_utf16(title ? title : "",
133 			-1, NULL, NULL, &error);
134 	if (error != NULL) {
135 		debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
136 		g_error_free(error);
137 		error = NULL;
138 	}
139 
140 	o.lStructSize = sizeof(OPENFILENAME);
141 	if (focus_window != NULL)
142 		o.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
143 	else
144 		o.hwndOwner = NULL;
145 	o.hInstance = NULL;
146 	o.lpstrFilter = NULL;
147 	o.lpstrCustomFilter = NULL;
148 	o.nFilterIndex = 0;
149 	o.nMaxFile = MAXPATHLEN;
150 	o.lpstrFileTitle = NULL;
151 	o.lpstrInitialDir = path16;
152 	o.lpstrTitle = title16;
153 	if (multi)
154 		o.Flags = OFN_LONGNAMES | OFN_EXPLORER | OFN_ALLOWMULTISELECT;
155 	else
156 		o.Flags = OFN_LONGNAMES | OFN_EXPLORER;
157 
158 	if (filter != NULL && strlen(filter) > 0) {
159 		debug_print("Setting filter '%s'\n", filter);
160 		filter16 = g_utf8_to_utf16(filter, -1, NULL, &conv_items, &error);
161 		/* We're creating a UTF16 (2 bytes for each character) string:
162 		 * "filter\0filter\0\0"
163 		 * As g_utf8_to_utf16() will stop on first null byte, even if
164 		 * we pass string length in its second argument, we have to
165 		 * construct this string manually.
166 		 * conv_items contains number of UTF16 characters of our filter.
167 		 * Therefore we need enough bytes to store the filter string twice
168 		 * and three null chars. */
169 		sz = sizeof(gunichar2);
170 		win_filter16 = g_malloc0(conv_items*sz*2 + sz*3);
171 		memcpy(win_filter16, filter16, conv_items*sz);
172 		memcpy(win_filter16 + conv_items + 1, filter16, conv_items*sz);
173 		g_free(filter16);
174 
175 		if (error != NULL) {
176 			debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
177 			g_error_free(error);
178 			error = NULL;
179 		}
180 		o.lpstrFilter = (LPCTSTR)win_filter16;
181 		o.nFilterIndex = 1;
182 	}
183 
184 	ctx = g_new0(WinChooserCtx, 1);
185 	ctx->data = &o;
186 	ctx->done = FALSE;
187 
188 #ifdef USE_PTHREAD
189 	if (pthread_create(&pt, NULL, threaded_GetOpenFileName,
190 				(void *)ctx) != 0) {
191 		debug_print("Couldn't run in a thread, continuing unthreaded.\n");
192 		threaded_GetOpenFileName(ctx);
193 	} else {
194 		while (!ctx->done) {
195 			claws_do_idle();
196 		}
197 		pthread_join(pt, NULL);
198 	}
199 	ret = ctx->return_value;
200 #else
201 	debug_print("No threads available, continuing unthreaded.\n");
202 	ret = GetOpenFileName(&o);
203 #endif
204 
205 	g_free(win_filter16);
206 	if (path16 != NULL) {
207 		g_free(path16);
208 	}
209 	g_free(title16);
210 	g_free(ctx);
211 
212 	return ret;
213 }
214 
filesel_select_file_open_with_filter(const gchar * title,const gchar * path,const gchar * filter)215 gchar *filesel_select_file_open_with_filter(const gchar *title, const gchar *path,
216 		              const gchar *filter)
217 {
218 	gchar *str = NULL;
219 	GError *error = NULL;
220 
221 	o.lpstrFile = g_malloc0(MAXPATHLEN);
222 	if (!_file_open_dialog(path, title, filter, FALSE)) {
223 		g_free(o.lpstrFile);
224 		return NULL;
225 	}
226 
227 	/* Now convert the returned file path back from UTF-16. */
228 	str = g_utf16_to_utf8(o.lpstrFile, o.nMaxFile, NULL, NULL, &error);
229 	if (error != NULL) {
230 		alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
231 				error->message);
232 		debug_print("returned file path conversion to UTF-8 failed\n");
233 		g_error_free(error);
234 	}
235 
236 	g_free(o.lpstrFile);
237 	return str;
238 }
239 
filesel_select_multiple_files_open_with_filter(const gchar * title,const gchar * path,const gchar * filter)240 GList *filesel_select_multiple_files_open_with_filter(const gchar *title,
241 		const gchar *path, const gchar *filter)
242 {
243 	GList *file_list = NULL;
244 	gchar *str = NULL;
245 	gchar *dir = NULL;
246 	gunichar2 *f;
247 	GError *error = NULL;
248 	glong n, items_read;
249 
250 	o.lpstrFile = g_malloc0(MAXPATHLEN);
251 	if (!_file_open_dialog(path, title, filter, TRUE)) {
252 		g_free(o.lpstrFile);
253 		return NULL;
254 	}
255 
256 	/* Now convert the returned directory and file names back from UTF-16.
257 	 * The content of o.lpstrFile is:
258 	 * "directory0file0file0...0file00" for multiple files selected,
259 	 * "fullfilepath0" for single file. */
260 	str = g_utf16_to_utf8(o.lpstrFile, -1, &items_read, NULL, &error);
261 
262 	if (error != NULL) {
263 		alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
264 				error->message);
265 		debug_print("returned file path conversion to UTF-8 failed\n");
266 		g_error_free(error);
267 		return NULL;
268 	}
269 
270 	/* The part before the first null char is always a full path. If it is
271 	 * a path to a file, then only this one file has been selected,
272 	 * and we can bail out early. */
273 	if (g_file_test(str, G_FILE_TEST_IS_REGULAR)) {
274 		debug_print("Selected one file: '%s'\n", str);
275 		file_list = g_list_append(file_list, g_strdup(str));
276 		g_free(str);
277 		return file_list;
278 	}
279 
280 	/* So the path was to a directory. We need to parse more after
281 	 * the fist null char, until we get to two null chars in a row. */
282 	dir = g_strdup(str);
283 	g_free(str);
284 	debug_print("Selected multiple files in dir '%s'\n", dir);
285 
286 	n = items_read + 1;
287 	f = &o.lpstrFile[n];
288 	while (items_read > 0) {
289 		str = g_utf16_to_utf8(f, -1, &items_read, NULL, &error);
290 		if (error != NULL) {
291 			alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
292 					error->message);
293 			debug_print("returned file path conversion to UTF-8 failed\n");
294 			g_error_free(error);
295 			g_free(o.lpstrFile);
296 			return NULL;
297 		}
298 
299 		if (items_read > 0) {
300 			debug_print("selected file '%s'\n", str);
301 			file_list = g_list_append(file_list,
302 					g_strconcat(dir, G_DIR_SEPARATOR_S, str, NULL));
303 		}
304 		g_free(str);
305 
306 		n += items_read + 1;
307 		f = &o.lpstrFile[n];
308 	}
309 
310 	g_free(o.lpstrFile);
311 	return file_list;
312 }
313 
filesel_select_file_open(const gchar * title,const gchar * path)314 gchar *filesel_select_file_open(const gchar *title, const gchar *path)
315 {
316 	return filesel_select_file_open_with_filter(title, path, NULL);
317 }
318 
filesel_select_multiple_files_open(const gchar * title,const gchar * path)319 GList *filesel_select_multiple_files_open(const gchar *title, const gchar *path)
320 {
321 	return filesel_select_multiple_files_open_with_filter(title, path, NULL);
322 }
323 
filesel_select_file_save(const gchar * title,const gchar * path)324 gchar *filesel_select_file_save(const gchar *title, const gchar *path)
325 {
326 	gboolean ret;
327 	gchar *str, *filename = NULL;
328 	gunichar2 *filename16, *path16, *title16;
329 	glong conv_items;
330 	GError *error = NULL;
331 	WinChooserCtx *ctx;
332 #ifdef USE_PTHREAD
333 	pthread_t pt;
334 #endif
335 
336 	/* Find the filename part, if any */
337 	if (path == NULL || path[strlen(path)-1] == G_DIR_SEPARATOR) {
338 		filename = "";
339 	} else if ((filename = strrchr(path, G_DIR_SEPARATOR)) != NULL) {
340 		filename++;
341 	} else {
342 		filename = (char *) path;
343 	}
344 
345 	/* Convert it to UTF-16. */
346 	filename16 = g_utf8_to_utf16(filename, -1, NULL, &conv_items, &error);
347 	if (error != NULL) {
348 		alertpanel_error(_("Could not convert attachment name to UTF-16:\n\n%s"),
349 				error->message);
350 		debug_print("filename '%s' conversion to UTF-16 failed\n", filename);
351 		g_error_free(error);
352 		return NULL;
353 	}
354 
355 	/* Path needs to be converted to UTF-16, so that the native chooser
356 	 * can understand it. */
357 	path16 = g_utf8_to_utf16(path, -1, NULL, NULL, &error);
358 	if (error != NULL) {
359 		alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
360 				error->message);
361 		debug_print("file path '%s' conversion to UTF-16 failed\n", path);
362 		g_error_free(error);
363 		g_free(filename16);
364 		return NULL;
365 	}
366 
367 	/* Chooser dialog title needs to be UTF-16 as well. */
368 	title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
369 	if (error != NULL) {
370 		debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
371 		g_error_free(error);
372 	}
373 
374 	o.lStructSize = sizeof(OPENFILENAME);
375 	if (focus_window != NULL)
376 		o.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
377 	else
378 		o.hwndOwner = NULL;
379 	o.lpstrFilter = NULL;
380 	o.lpstrCustomFilter = NULL;
381 	o.lpstrFile = g_malloc0(MAXPATHLEN);
382 	if (path16 != NULL)
383 		memcpy(o.lpstrFile, filename16, conv_items * sizeof(gunichar2));
384 	o.nMaxFile = MAXPATHLEN;
385 	o.lpstrFileTitle = NULL;
386 	o.lpstrInitialDir = path16;
387 	o.lpstrTitle = title16;
388 	o.Flags = OFN_LONGNAMES | OFN_EXPLORER;
389 
390 	ctx = g_new0(WinChooserCtx, 1);
391 	ctx->data = &o;
392 	ctx->return_value = FALSE;
393 	ctx->done = FALSE;
394 
395 #ifdef USE_PTHREAD
396 	if (pthread_create(&pt, NULL, threaded_GetSaveFileName,
397 				(void *)ctx) != 0) {
398 		debug_print("Couldn't run in a thread, continuing unthreaded.\n");
399 		threaded_GetSaveFileName(ctx);
400 	} else {
401 		while (!ctx->done) {
402 			claws_do_idle();
403 		}
404 		pthread_join(pt, NULL);
405 	}
406 	ret = ctx->return_value;
407 #else
408 	debug_print("No threads available, continuing unthreaded.\n");
409 	ret = GetSaveFileName(&o);
410 #endif
411 
412 	g_free(filename16);
413 	g_free(path16);
414 	g_free(title16);
415 	g_free(ctx);
416 
417 	if (!ret) {
418 		g_free(o.lpstrFile);
419 		return NULL;
420 	}
421 
422 	/* Now convert the returned file path back from UTF-16. */
423 	str = g_utf16_to_utf8(o.lpstrFile, o.nMaxFile, NULL, NULL, &error);
424 	if (error != NULL) {
425 		alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
426 				error->message);
427 		debug_print("returned file path conversion to UTF-8 failed\n");
428 		g_error_free(error);
429 	}
430 
431 	g_free(o.lpstrFile);
432 	return str;
433 }
434 
435 /* This callback function is used to set the folder browse dialog
436  * selection from filesel_select_file_open_folder() to set
437  * chosen starting folder ("path" argument to that function. */
_open_folder_callback(HWND hwnd,UINT uMsg,LPARAM lParam,LPARAM lpData)438 static int CALLBACK _open_folder_callback(HWND hwnd, UINT uMsg,
439 		LPARAM lParam, LPARAM lpData)
440 {
441 	if (uMsg != BFFM_INITIALIZED)
442 		return 0;
443 
444 	SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
445 	return 0;
446 }
447 
filesel_select_file_open_folder(const gchar * title,const gchar * path)448 gchar *filesel_select_file_open_folder(const gchar *title, const gchar *path)
449 {
450 	PIDLIST_ABSOLUTE pidl;
451 	gchar *str;
452 	gunichar2 *path16, *title16;
453 	glong conv_items;
454 	GError *error = NULL;
455 	WinChooserCtx *ctx;
456 #ifdef USE_PTHREAD
457 	pthread_t pt;
458 #endif
459 
460 	/* Path needs to be converted to UTF-16, so that the native chooser
461 	 * can understand it. */
462 	path16 = g_utf8_to_utf16(path ? path : "",
463 			-1, NULL, &conv_items, &error);
464 	if (error != NULL) {
465 		alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
466 				error->message);
467 		debug_print("file path '%s' conversion to UTF-16 failed\n", path);
468 		g_error_free(error);
469 		return NULL;
470 	}
471 
472 	/* Chooser dialog title needs to be UTF-16 as well. */
473 	title16 = g_utf8_to_utf16(title ? title : "",
474 			-1, NULL, NULL, &error);
475 	if (error != NULL) {
476 		debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
477 		g_error_free(error);
478 	}
479 
480 	if (focus_window != NULL)
481 		b.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
482 	else
483 		b.hwndOwner = NULL;
484 	b.pszDisplayName = g_malloc(MAXPATHLEN);
485 	b.lpszTitle = title16;
486 	b.ulFlags = 0;
487 	b.pidlRoot = NULL;
488 	b.lpfn = _open_folder_callback;
489 	b.lParam = (LPARAM)path16;
490 
491 	CoInitialize(NULL);
492 
493 	ctx = g_new0(WinChooserCtx, 1);
494 	ctx->data = &b;
495 	ctx->done = FALSE;
496 
497 #ifdef USE_PTHREAD
498 	if (pthread_create(&pt, NULL, threaded_SHBrowseForFolder,
499 				(void *)ctx) != 0) {
500 		debug_print("Couldn't run in a thread, continuing unthreaded.\n");
501 		threaded_SHBrowseForFolder(ctx);
502 	} else {
503 		while (!ctx->done) {
504 			claws_do_idle();
505 		}
506 		pthread_join(pt, NULL);
507 	}
508 	pidl = ctx->return_value_pidl;
509 #else
510 	debug_print("No threads available, continuing unthreaded.\n");
511 	pidl = SHBrowseForFolder(&b);
512 #endif
513 
514 	g_free(b.pszDisplayName);
515 	g_free(title16);
516 	g_free(path16);
517 
518 	if (pidl == NULL) {
519 		CoUninitialize();
520 		g_free(ctx);
521 		return NULL;
522 	}
523 
524 	path16 = malloc(MAX_PATH);
525 	if (!SHGetPathFromIDList(pidl, path16)) {
526 		CoTaskMemFree(pidl);
527 		CoUninitialize();
528 		g_free(path16);
529 		g_free(ctx);
530 		return NULL;
531 	}
532 
533 	/* Now convert the returned file path back from UTF-16. */
534 	/* Unfortunately, there is no field in BROWSEINFO struct to indicate
535 	 * actual length of string in pszDisplayName, so we have to assume
536 	 * the string is null-terminated. */
537 	str = g_utf16_to_utf8(path16, -1, NULL, NULL, &error);
538 	if (error != NULL) {
539 		alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
540 				error->message);
541 		debug_print("returned file path conversion to UTF-8 failed\n");
542 		g_error_free(error);
543 	}
544 	CoTaskMemFree(pidl);
545 	CoUninitialize();
546 	g_free(ctx);
547 	g_free(path16);
548 
549 	return str;
550 }
551 
filesel_select_file_save_folder(const gchar * title,const gchar * path)552 gchar *filesel_select_file_save_folder(const gchar *title, const gchar *path)
553 {
554 	return filesel_select_file_open_folder(title, path);
555 }
556