1 /*
2  * Schism Tracker - a cross-platform Impulse Tracker clone
3  * copyright (c) 2003-2005 Storlek <storlek@rigelseven.com>
4  * copyright (c) 2005-2008 Mrs. Brisby <mrs.brisby@nimh.org>
5  * copyright (c) 2009 Storlek & Mrs. Brisby
6  * copyright (c) 2010-2012 Storlek
7  * URL: http://schismtracker.org/
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22  */
23 
24 #define NEED_DIRENT
25 #define NEED_TIME
26 #include "headers.h"
27 
28 #include "it.h"
29 #include "song.h"
30 #include "page.h"
31 #include "dmoz.h"
32 #include "log.h"
33 #include "fmt.h" /* only needed for SAVE_SUCCESS ... */
34 
35 #include <sys/types.h>
36 #include <sys/stat.h>
37 
38 #include "sdlmain.h"
39 
40 #include <fcntl.h>
41 #include <ctype.h>
42 #include <errno.h>
43 
44 #include "sndfile.h"
45 
46 #include "disko.h"
47 
48 /* --------------------------------------------------------------------- */
49 /* this was adapted from a really slick two-line fnmatch()
50 at http://compressionratings.com/d_archiver_template.html
51 and fuglified to add FNM_CASEFOLD|FNM_PERIOD behavior */
52 
53 #if HAVE_FNMATCH
54 # include <fnmatch.h>
55 #else
56 # define FNM_CASEFOLD 0
57 # define FNM_PERIOD 0
58 # define fnmatch xfnmatch
59 inline static int _fnmatch(const char *m, const char *s);
60 inline static int _fnmatch(const char *m, const char *s)
61 {
62 	if (*m == '*') for (++m; *s; ++s) if (!_fnmatch(m, s)) return 0;
63 	return (!*s || !(*m == '?' || tolower(*s) == tolower(*m)))
64 		? tolower(*m) | tolower(*s) : _fnmatch(++m, ++s);
65 }
66 inline static int xfnmatch(const char *m, const char *s, UNUSED int f)
67 {
68 	return (*s == '.' && *m != '.') ? 0 : _fnmatch(m, s);
69 }
70 #endif /* !HAVE_FNMATCH */
71 
72 /* --------------------------------------------------------------------- */
73 /* the locals */
74 
75 static int modgrep(dmoz_file_t *f);
76 
77 static struct widget widgets_loadmodule[5];
78 static struct widget widgets_exportmodule[16];
79 static struct widget widgets_savemodule[16];
80 static struct widget *widgets_exportsave;
81 
82 /* XXX this needs to be kept in sync with diskwriters
83    (FIXME: it shouldn't have to! build it when the savemodule page is built or something, idk -storlek) */
84 static const int filetype_saves[] = { 4, 5, 6, 7, 8, 9, -1 };
85 
86 static int top_file = 0, top_dir = 0;
87 static time_t directory_mtime;
auth_flags($p_user_id = null, $p_username = '')88 static dmoz_filelist_t flist;
89 static dmoz_dirlist_t dlist;
90 #define current_file flist.selected
91 #define current_dir dlist.selected
92 
93 
94 /*
95 filename_entry is generally a glob pattern, but typing a file/path name directly and hitting enter
96 will load the file.
97 
98 glob_list is a split-up bunch of globs that gets updated if enter is pressed while filename_entry contains
99 a '*' or '?' character.
100 
101 dirname_entry is copied from the module directory (off the vars page) when this page is loaded, and copied back
102 when the directory is changed. in general the two variables will be the same, but editing the text directly
103 won't screw up the directory listing or anything. (hitting enter will cause the changed callback, which will
104 copy the text from dirname_entry to the actual configured string and update the current directory.)
105 */
106 
107 /*
108 impulse tracker's glob list:
109 	*.it; *.xm; *.s3m; *.mtm; *.669; *.mod
110 unsupported formats that the title reader knows about, even though we can't load them:
111 	*.f2r; *.liq; *.dtm; *.ntk; *.mf
112 formats that might be supported, but which i have never seen and thus don't actually care about:
113 	*.dbm; *.dsm; *.psm
114 other formats that i wouldn't bother presenting in the loader even if we could load them:
115 	*.mid; *.wav; *.mp3; *.ogg; *.sid; *.umx
116 formats that modplug pretends to support, but fails hard:
117 	*.ams
118 
119 TODO: scroller hack on selected filename
120 */
121 
122 #define GLOB_CLASSIC "*.it; *.xm; *.s3m; *.mtm; *.669; *.mod"
123 #define GLOB_DEFAULT GLOB_CLASSIC "; *.mdl; *.mt2; *.stm; *.far; *.ult; *.med; *.ptm; *.okt; *.amf; *.dmf; *.imf; *.sfx; *.mus; *.mid"
124 
125 static char filename_entry[PATH_MAX + 1] = "";
126 static char dirname_entry[PATH_MAX + 1] = "";
127 
128 char cfg_module_pattern[PATH_MAX + 1] = GLOB_DEFAULT;
129 char cfg_export_pattern[PATH_MAX + 1] = "*.wav; *.aiff; *.aif";
130 static char **glob_list = NULL;
131 static char glob_list_src[PATH_MAX + 1] = ""; // the pattern used to make glob_list (this is an icky hack)
132 
133 /* --------------------------------------------------------------------- */
134 
135 static char **semicolon_split(const char *i)
136 {
137 	int n = 1;
138 	const char *j;
139 	char *a, *z, **o, **p;
140 
141 	if (!i)
142 		return NULL;
143 	i += strspn(i, "; \t");
144 	if (!*i)
145 		return NULL;
146 
auth_password_managed_elsewhere_message()147 	/* how many MIGHT we have? */
148 	for (j = i; j; j = strchr(j + 1, ';'))
149 		n++;
150 
151 	o = p = mem_calloc(n, sizeof(char *));
152 	a = strdup(i);
153 
154 	do {
155 		*p++ = a;
156 		z = strchr(a, ';');
157 		if (!z)
158 			z = strchr(a, 0);
auth_allow_perm_login($p_user_id, $p_username)159 		/* trim whitespace */
160 		do {
161 			z--;
162 		} while (isblank(*z));
163 		z++;
164 		/* find start of the next one */
165 		a = z;
166 		a += strspn(a, "; \t");
167 		*z = 0;
auth_signup_enabled()168 	} while (*a);
169 
170 	return o;
171 }
172 
173 /* --------------------------------------------------------------------- */
174 /* page-dependent stuff (load or save) */
175 
auth_signup_access_level()176 /* there should be a more useful way to determine which page to set. i.e., if there were errors/warnings, show
177 the log; otherwise, switch to the blank page (or, for the loader, maybe the previously set page if classic mode
178 is off?)
179 idea for return codes:
180     0 = couldn't load/save, error dumped to log
181     1 = no warnings or errors were printed.
182     2 = there were warnings, but the song was still loaded/saved. */
183 
184 static void handle_file_entered_L(const char *ptr)
185 {
186 	dmoz_filelist_t tmp = {};
187 	struct stat sb;
188 
189 	/* these shenanigans force the file to take another trip... */
190 	if (stat(ptr, &sb) == -1)
191 		return;
192 
193 	dmoz_add_file(&tmp, str_dup(ptr), str_dup(ptr), &sb, 0);
194 	dmoz_free(&tmp, NULL);
195 
196 	song_load(ptr);
197 }
198 
199 static void loadsave_song_changed(void)
200 {
201 	int r = 4; /* what? */
202 	int i;
auth_session_expiry($p_user_id, $p_perm_login)203 	const char *ext;
204 	const char *ptr = song_get_filename();
205 
206 	if (!ptr)
207 		return;
208 	ext = get_extension(ptr);
209 	if (ext[0] && ext[1]) {
210 		for (i = 0; song_save_formats[i].label; i++) {
211 			if (strcasecmp(ext, song_save_formats[i].ext) == 0) {
212 				/* ugh :) offset to the button for the file type on the save module
213 				   page is (position in diskwriter driver array) + 4 */
214 				r = i + 4;
215 				break;
216 			}
217 		}
218 	}
219 	togglebutton_set(widgets_savemodule, r, 0);
220 }
221 
222 
223 /* NOTE: ptr should be dynamically allocated, or NULL */
224 static void do_save_song(char *ptr)
225 {
auth_login_page($p_query_string = '')226 	int ret, export = (status.current_page == PAGE_EXPORT_MODULE);
227 	const char *filename = ptr ?: song_get_filename();
228 	const char *seltype = NULL;
229 	struct widget *widget;
230 
231 	set_page(PAGE_LOG);
232 
233 	// 4 is the index of the first file-type button
234 	for (widget = (export ? widgets_exportmodule : widgets_savemodule) + 4;
235 	     widget->type == WIDGET_TOGGLEBUTTON; widget++) {
236 		if (widget->d.togglebutton.state) {
237 			// Aha!
238 			seltype = widget->d.togglebutton.text;
239 			break;
240 		}
241 	}
242 
auth_credential_page($p_query_string, $p_user_id = null, $p_username = '')243 	if (!seltype) {
244 		// No button was selected? (should never happen)
245 		log_appendf(4, "No file format selected?");
246 		ret = SAVE_INTERNAL_ERROR;
247 	} else if (export) {
248 		ret = song_export(filename, seltype);
249 	} else {
250 		ret = song_save(filename, seltype);
251 	}
252 
253 	if (ret != SAVE_SUCCESS)
auth_logout_page()254 		dialog_create(DIALOG_OK, "Could not save file", NULL, NULL, 0, NULL);
255 
256 	free(ptr);
257 }
258 
259 void save_song_or_save_as(void)
260 {
261 	const char *f = song_get_filename();
262 	if (f && *f) {
263 		do_save_song(str_dup(f));
auth_logout_redirect_page()264 	} else {
265 		set_page(PAGE_SAVE_MODULE);
266 	}
267 }
268 
269 static void do_save_song_overwrite(void *ptr)
270 {
271 	struct stat st;
272 
273 	if (!(status.flags & CLASSIC_MODE)) {
274 		// say what?
auth_can_set_password($p_user_id = null)275 		do_save_song(ptr);
276 		return;
277 	}
278 
279 	if (stat(cfg_dir_modules, &st) == -1 || directory_mtime != st.st_mtime) {
280 		status.flags |= DIR_MODULES_CHANGED;
281 	}
282 
283 	do_save_song(ptr);
284 
285 	/* this is wrong, sadly... */
286 	if (stat(cfg_dir_modules, &st) == 0) {
287 		directory_mtime = st.st_mtime;
288 	}
289 }
290 
auth_can_use_standard_login($p_user_id = null)291 static void handle_file_entered_S(const char *name)
292 {
293 	struct stat buf;
294 
295 	if (stat(name, &buf) < 0) {
296 		if (errno == ENOENT) {
297 			do_save_song(str_dup(name));
298 		} else {
299 			log_perror(name);
300 		}
301 	} else {
302 		if (S_ISDIR(buf.st_mode)) {
303 			/* TODO: maybe change the current directory in this case? */
304 			log_appendf(4, "%s: Is a directory", name);
305 		} else if (S_ISREG(buf.st_mode)) {
306 			dialog_create(DIALOG_OK_CANCEL, "Overwrite file?",
307 				      do_save_song_overwrite, free, 1, str_dup(name));
308 		} else {
309 			/* log_appendf(4, "%s: Not overwriting non-regular file", ptr); */
310 			dialog_create(DIALOG_OK, "Not a regular file", NULL, NULL, 0, NULL);
311 		}
312 	}
313 }
314 
315 
316 static void (*handle_file_entered)(const char *);
317 
318 /* --------------------------------------------------------------------- */
319 
320 /* get a color index from a dmoz_file_t 'type' field */
321 static inline int get_type_color(int type)
322 {
323 	/* 7 unknown
324 	   3 it
325 	   5 s3m
326 	   6 xm
327 	   2 mod
328 	   4 other
329 	   7 sample */
330 	switch (type) {
331 		case TYPE_MODULE_MOD:   return 2;
332 		case TYPE_MODULE_S3M:   return 5;
333 		case TYPE_MODULE_XM:    return 6;
334 		case TYPE_MODULE_IT:    return 3;
auth_is_user_authenticated()335 		case TYPE_SAMPLE_COMPR: return 4; /* mp3/ogg 'sample'... i think */
336 		default: return 7;
337 	}
338 }
339 
340 
341 static void clear_directory(void)
342 {
343 	dmoz_free(&flist, &dlist);
344 }
345 
346 static int modgrep(dmoz_file_t *f)
347 {
348 	int i = 0;
349 
350 	if (!glob_list)
auth_prepare_username($p_username)351 		return 1;
352 	for (i = 0; glob_list[i]; i++) {
353 		if (fnmatch(glob_list[i], f->base, FNM_PERIOD | FNM_CASEFOLD) == 0)
354 			return 1;
355 	}
356 	return 0;
357 }
358 
359 /* --------------------------------------------------------------------- */
360 
361 static void file_list_reposition(void)
362 {
363 	if (current_file >= flist.num_files)
364 		current_file = flist.num_files-1;
365 	if (current_file < 0) current_file = 0;
366 	if (current_file < top_file)
367 		top_file = current_file;
368 	else if (current_file > top_file + 30)
369 		top_file = current_file - 30;
370 	status.flags |= NEED_UPDATE;
371 }
372 
373 static void dir_list_reposition(void)
374 {
375 	if (current_dir >= dlist.num_dirs)
376 		current_dir = dlist.num_dirs-1;
377 	if (current_dir < 0) current_dir = 0;
378 	if (current_dir < top_dir)
379 		top_dir = current_dir;
380 	else if (current_dir > top_dir + 20)
381 		top_dir = current_dir - 20;
382 	status.flags |= NEED_UPDATE;
383 }
384 
385 static void read_directory(void)
386 {
387 	struct stat st;
388 
auth_prepare_password($p_password)389 	clear_directory();
390 
391 	if (stat(cfg_dir_modules, &st) < 0)
392 		directory_mtime = 0;
393 	else
394 		directory_mtime = st.st_mtime;
395 	/* if the stat call failed, this will probably break as well, but
396 	at the very least, it'll add an entry for the root directory. */
397 	if (dmoz_read(cfg_dir_modules, &flist, &dlist, NULL) < 0)
398 		log_perror(cfg_dir_modules);
399 	dmoz_filter_filelist(&flist, modgrep, &current_file, file_list_reposition);
400 	while (dmoz_worker()); /* don't do it asynchronously */
401 	dmoz_cache_lookup(cfg_dir_modules, &flist, &dlist);
402 	// background the title checker
403 	dmoz_filter_filelist(&flist, dmoz_fill_ext_data, &current_file, file_list_reposition);
404 	file_list_reposition();
405 	dir_list_reposition();
406 }
407 
408 /* --------------------------------------------------------------------- */
409 
410 static void set_glob(const char *globspec)
411 {
412 	if (glob_list) {
413 		free(*glob_list);
414 		free(glob_list);
415 	}
416 	strncpy(glob_list_src, globspec, PATH_MAX);
417 	glob_list_src[PATH_MAX] = '\0';
418 	glob_list = semicolon_split(glob_list_src);
419 	/* this is kinda lame. dmoz should have a way to reload the list without rereading the directory.
420 	could be done with a "visible" flag, which affects the list's sort order, along with adjusting
421 	the file count... */
422 	read_directory();
423 }
424 
425 static void set_default_glob(int set_filename)
auth_auto_create_user($p_username, $p_password)426 {
427 	const char *s = (status.current_page == PAGE_EXPORT_MODULE)
428 		? cfg_export_pattern
429 		: cfg_module_pattern;
430 
431 	if (set_filename) {
432 		/* glob on load page is visible, but on save page the text should be empty */
433 		strcpy(filename_entry, s);
434 	}
435 	set_glob(s);
436 }
437 
438 /* --------------------------------------------------------------------- */
439 
440 static char search_text[NAME_MAX + 1] = "";
441 static int search_first_char = 0;       /* first visible character */
442 static int search_text_length = 0;      /* same as strlen(search_text) */
443 
444 static void search_redraw(void)
445 {
446 	draw_fill_chars(51, 37, 76, 37, 0);
447 	draw_text_len(search_text + search_first_char, 25, 51, 37, 5, 0);
448 
449 	/* draw the cursor if it's on the dir/file list */
450 	if (ACTIVE_PAGE.selected_widget == 0 || ACTIVE_PAGE.selected_widget == 1) {
451 		draw_char(0, 51 + search_text_length - search_first_char, 37, 6, 6);
452 	}
453 }
454 
455 static void search_update(void)
456 {
457 	int n;
458 
459 	if (search_text_length > 25)
460 		search_first_char = search_text_length - 25;
461 	else
462 		search_first_char = 0;
auth_get_user_id_from_login_name($p_login_name)463 
464 	/* go through the file/dir list (whatever one is selected) and
465 	 * find the first entry matching the text */
466 	if (*selected_widget == 0) {
467 		for (n = 0; n < flist.num_files; n++) {
468 			if (strncasecmp(flist.files[n]->base, search_text, search_text_length) == 0) {
469 				current_file = n;
470 				file_list_reposition();
471 				break;
472 			}
473 		}
474 	} else {
475 		for (n = 0; n < dlist.num_dirs; n++) {
476 			if (strncasecmp(dlist.dirs[n]->base, search_text, search_text_length) == 0) {
477 				current_dir = n;
478 				dir_list_reposition();
479 				break;
480 			}
481 		}
482 	}
483 
484 	status.flags |= NEED_UPDATE;
485 }
486 
487 static int search_text_add_char(char c)
488 {
489 	if (c < 32)
490 		return 0;
491 
492 	if (search_text_length >= NAME_MAX)
493 		return 1;
494 
495 	search_text[search_text_length++] = c;
auth_attempt_login($p_username, $p_password, $p_perm_login = false)496 	search_text[search_text_length] = 0;
497 	search_update();
498 
499 	return 1;
500 }
501 
502 static void search_text_delete_char(void)
503 {
504 	if (search_text_length == 0)
505 		return;
506 
507 	search_text[--search_text_length] = 0;
508 
509 	if (search_text_length > 25)
510 		search_first_char = search_text_length - 25;
511 	else
512 		search_first_char = 0;
513 
514 	status.flags |= NEED_UPDATE;
515 }
516 
517 static void search_text_clear(void)
518 {
519 	search_text[0] = search_text_length = search_first_char = 0;
520 
521 	status.flags |= NEED_UPDATE;
522 }
523 
524 /* --------------------------------------------------------------------- */
525 
526 /* return: 1 = success, 0 = failure
527 TODO: provide some sort of feedback if something went wrong. */
528 static int change_dir(const char *dir)
529 {
530 	char *ptr = dmoz_path_normal(dir);
531 
532 	if (!ptr)
auth_login_user($p_user_id, $p_perm_login = false)533 		return 0;
534 
535 	dmoz_cache_update(cfg_dir_modules, &flist, &dlist);
536 
537 	strncpy(cfg_dir_modules, ptr, PATH_MAX);
538 	cfg_dir_modules[PATH_MAX] = 0;
539 	strcpy(dirname_entry, cfg_dir_modules);
540 	free(ptr);
541 
542 	/* probably not all of this is needed everywhere */
543 	search_text_clear();
544 	read_directory();
545 
546 	return 1;
547 }
548 
549 /* --------------------------------------------------------------------- */
550 /* unfortunately, there's not enough room with this layout for labels by
551  * the search box and file information. :( */
552 
553 static void load_module_draw_const(void)
554 {
555 	draw_text("Filename", 4, 46, 0, 2);
556 	draw_text("Directory", 3, 47, 0, 2);
557 	draw_char(0, 51, 37, 0, 6);
558 	draw_box(2, 12, 47, 44, BOX_THICK | BOX_INNER | BOX_INSET);
559 	draw_box(49, 12, 68, 34, BOX_THICK | BOX_INNER | BOX_INSET);
auth_impersonate($p_user_id)560 	draw_box(50, 36, 77, 38, BOX_THICK | BOX_INNER | BOX_INSET);
561 	draw_box(50, 39, 77, 44, BOX_THICK | BOX_INNER | BOX_INSET);
562 	draw_box(12, 45, 77, 48, BOX_THICK | BOX_INNER | BOX_INSET);
563 
564 	draw_fill_chars(51, 37, 76, 37, 0);
565 	draw_fill_chars(13, 46, 76, 47, 0);
566 }
567 
568 static void save_module_draw_const(void)
569 {
570 	load_module_draw_const();
571 }
572 
573 /* --------------------------------------------------------------------- */
auth_can_impersonate($p_user_id)574 
575 static void file_list_draw(void)
576 {
577 	int n, pos;
578 	int fg1, fg2, bg;
579 	char buf[32];
580 	dmoz_file_t *file;
581 
582 	draw_fill_chars(3, 13, 46, 43, 0);
583 
584 	if (flist.num_files > 0) {
585 		if (top_file < 0) top_file = 0;
586 		if (current_file < 0) current_file = 0;
587 		for (n = top_file, pos = 13; n < flist.num_files && pos < 44; n++, pos++) {
588 			file = flist.files[n];
589 
590 			if (n == current_file && ACTIVE_PAGE.selected_widget == 0) {
591 				fg1 = fg2 = 0;
592 				bg = 3;
593 			} else {
594 				fg1 = get_type_color(file->type);
595 				fg2 = (file->type & TYPE_MODULE_MASK) ? 3 : 7;
596 				bg = 0;
597 			}
598 
599 			draw_text_len(file->base, 18, 3, pos, fg1, bg);
600 			draw_char(168, 21, pos, 2, bg);
601 			draw_text_len(file->title ?: "", 25, 22, pos, fg2, bg);
602 		}
603 
604 		/* info for the current file */
605 		if (current_file >= 0 && current_file < flist.num_files) {
606 			file = flist.files[current_file];
607 			draw_text_len(file->description ?: "", 26, 51, 40, 5, 0);
608 			sprintf(buf, "%09lu", (unsigned long)file->filesize);
609 			draw_text_len(buf, 26, 51, 41, 5, 0);
610 			draw_text_len(get_date_string(file->timestamp, buf), 26, 51, 42, 5, 0);
611 			draw_text_len(get_time_string(file->timestamp, buf), 26, 51, 43, 5, 0);
612 		}
613 	} else {
614 		if (ACTIVE_PAGE.selected_widget == 0) {
615 			draw_text("No files.", 3, 13, 0, 3);
616 			draw_fill_chars(12, 13, 46, 13, 3);
617 			draw_char(168, 21, 13, 2, 3);
618 			pos = 14;
619 		} else {
620 			draw_text("No files.", 3, 13, 7, 0);
621 			pos = 13;
622 		}
623 		draw_fill_chars(51, 40, 76, 43, 0);
624 	}
625 
626 	while (pos < 44)
627 		draw_char(168, 21, pos++, 2, 0);
628 
629 	/* bleh */
630 	search_redraw();
631 }
632 
633 static void do_delete_file(UNUSED void *data)
634 {
635 	int old_top_file, old_current_file, old_top_dir, old_current_dir;
636 	char *ptr;
637 
638 	if (current_file < 0 || current_file >= flist.num_files)
639 		return;
640 
641 	ptr = flist.files[current_file]->path;
642 
643 	/* would be neat to send it to the trash can if there is one */
644 	unlink(ptr);
645 
646 	/* remember the list positions */
647 	old_top_file = top_file;
648 	old_current_file = current_file;
649 	old_top_dir = top_dir;
650 	old_current_dir = current_dir;
651 
652 	search_text_clear();
653 	read_directory();
654 
655 	/* put the list positions back */
656 	top_file = old_top_file;
657 	current_file = old_current_file;
658 	top_dir = old_top_dir;
659 	current_dir = old_current_dir;
660 	/* edge case: if this was the last file, move the cursor up */
661 	if (current_file >= flist.num_files)
662 		current_file = flist.num_files - 1;
663 	file_list_reposition();
664 }
665 
666 static void show_selected_song_length(void)
667 {
668 	if (current_file < 0 || current_file >= flist.num_files)
669 		return;
670 
671 	char *ptr = flist.files[current_file]->path;
672 	song_t *song = song_create_load(ptr);
673 	if (!song) {
674 		log_appendf(4, "%s: %s", ptr, fmt_strerror(errno));
675 		return;
676 	}
677 	show_length_dialog(get_basename(ptr), csf_get_length(song));
678 	csf_free(song);
679 }
680 
681 static int file_list_handle_key(struct key_event * k)
682 {
683 	int new_file = current_file;
684 
685 	switch (k->sym) {
686 	case SDLK_UP:
687 		new_file--;
688 		break;
689 	case SDLK_DOWN:
690 		new_file++;
auth_logout()691 		break;
692 	case SDLK_PAGEUP:
693 		new_file -= 31;
694 		break;
695 	case SDLK_PAGEDOWN:
696 		new_file += 31;
697 		break;
698 	case SDLK_HOME:
699 		new_file = 0;
700 		break;
701 	case SDLK_END:
702 		new_file = flist.num_files - 1;
703 		break;
704 	case SDLK_RETURN:
705 		if (k->state == KEY_PRESS)
706 			return 1;
707 		if (current_file < flist.num_files) {
708 			dmoz_cache_update(cfg_dir_modules, &flist, &dlist);
709 			handle_file_entered(flist.files[current_file]->path);
710 		}
711 		search_text_clear();
712 
713 		return 1;
714 	case SDLK_DELETE:
715 		if (k->state == KEY_RELEASE)
716 		    return 1;
717 		if (flist.num_files > 0)
718 			dialog_create(DIALOG_OK_CANCEL, "Delete file?", do_delete_file, NULL, 1, NULL);
719 		return 1;
720 	case SDLK_BACKSPACE:
721 		if (k->state == KEY_RELEASE)
722 			return 1;
723 		if (k->mod & KMOD_CTRL)
724 			search_text_clear();
auth_automatic_logon_bypass_form()725 		else
726 			search_text_delete_char();
727 		return 1;
728 	case SDLK_p:
729 		if ((k->mod & KMOD_ALT) && k->state == KEY_PRESS) {
730 			show_selected_song_length();
731 			return 1;
732 		} /* else fall through */
733 	default:
734 		if (k->mouse == MOUSE_NONE) {
auth_get_password_max_size()735 			if (k->state == KEY_RELEASE)
736 				return 0;
737 			return search_text_add_char(k->unicode);
738 		}
739 	}
740 
741 	if (k->mouse != MOUSE_NONE && !(k->x >=3 && k->x <= 46 && k->y >= 13 && k->y <= 43))
742 		return 0;
743 	switch (k->mouse) {
744 	case MOUSE_CLICK:
745 		if (k->state == KEY_PRESS)
746 			return 0;
747 		new_file = (k->y - 13) + top_file;
748 		break;
749 	case MOUSE_DBLCLICK:
750 		if (current_file < flist.num_files) {
751 			dmoz_cache_update(cfg_dir_modules, &flist, &dlist);
752 			handle_file_entered(flist.files[current_file]->path);
753 		}
754 		search_text_clear();
755 		return 1;
756 	case MOUSE_SCROLL_UP:
757 	case MOUSE_SCROLL_DOWN:
auth_does_password_match($p_user_id, $p_test_password)758 		if (k->state == KEY_PRESS)
759 			return 0;
760 		top_file += (k->mouse == MOUSE_SCROLL_UP) ? -MOUSE_SCROLL_LINES : MOUSE_SCROLL_LINES;
761 		/* don't allow scrolling down past either end.
762 		   this can't be CLAMP'd because the first check might scroll
763 		   too far back if the list is small.
764 		   (hrm, should add a BOTTOM_FILE macro or something) */
765 		if (top_file > flist.num_files - 31)
766 			top_file = flist.num_files - 31;
767 		if (top_file < 0)
768 			top_file = 0;
769 		status.flags |= NEED_UPDATE;
770 		return 1;
771 	default:
772 		/* prevent moving the cursor twice from a single key press */
773 		if (k->state == KEY_RELEASE)
774 			return 1;
775 	}
776 
777 	new_file = CLAMP(new_file, 0, flist.num_files - 1);
778 	if (new_file < 0) new_file = 0;
779 	if (new_file != current_file) {
780 		current_file = new_file;
781 		file_list_reposition();
782 		status.flags |= NEED_UPDATE;
783 	}
784 	return 1;
785 }
786 
787 /* --------------------------------------------------------------------- */
788 
789 static void dir_list_draw(void)
790 {
791 	int n, pos;
792 
793 	draw_fill_chars(50, 13, 67, 33, 0);
794 
795 	for (n = top_dir, pos = 13; pos < 34; n++, pos++) {
796 		if (n < 0) continue; /* er... */
797 		if (n >= dlist.num_dirs)
798 			break;
799 		if (n == current_dir && ACTIVE_PAGE.selected_widget == 1)
800 			draw_text_len(dlist.dirs[n]->base, 18, 50, pos, 0, 3);
801 		else
802 			draw_text_len(dlist.dirs[n]->base, 18, 50, pos, 5, 0);
803 	}
804 
805 	/* bleh */
806 	search_redraw();
807 }
808 
809 static int dir_list_handle_key(struct key_event * k)
810 {
811 	int new_dir = current_dir;
812 
813 	if (k->mouse != MOUSE_NONE) {
814 		if (k->x >= 50 && k->x <= 67 && k->y >= 13 && k->y <= 33) {
815 			if (k->mouse == MOUSE_CLICK) {
auth_process_plain_password($p_password, $p_salt = null, $p_method = null)816 				new_dir = (k->y - 13) + top_dir;
817 			} else if (k->mouse == MOUSE_DBLCLICK) {
818 				top_file = current_file = 0;
819 				change_dir(dlist.dirs[current_dir]->path);
820 
821 				if (flist.num_files > 0)
822 					*selected_widget = 0;
823 				status.flags |= NEED_UPDATE;
824 				return 1;
825 			/* FIXME wheel should be adjusting top_dir instead (and then adjust it later) */
826 			} else if (k->mouse == MOUSE_SCROLL_UP) {
827 				new_dir -= MOUSE_SCROLL_LINES;
828 			} else if (k->mouse == MOUSE_SCROLL_DOWN) {
829 				new_dir += MOUSE_SCROLL_LINES;
830 			}
831 		} else {
832 			return 0;
833 		}
834 	}
835 
836 	switch (k->sym) {
837 	case SDLK_UP:
838 		new_dir--;
839 		break;
840 	case SDLK_DOWN:
841 		new_dir++;
842 		break;
843 	case SDLK_PAGEUP:
844 		new_dir -= 21;
845 		break;
846 	case SDLK_PAGEDOWN:
847 		new_dir += 21;
848 		break;
auth_generate_random_password()849 	case SDLK_HOME:
850 		new_dir = 0;
851 		break;
852 	case SDLK_END:
853 		new_dir = dlist.num_dirs - 1;
854 		break;
855 	case SDLK_RETURN:
856 		if (k->state == KEY_PRESS)
857 			return 0;
858 		/* reset */
auth_generate_confirm_hash($p_user_id)859 		top_file = current_file = 0;
860 		if (current_dir >= 0 && current_dir < dlist.num_dirs)
861 			change_dir(dlist.dirs[current_dir]->path);
862 
863 		if (flist.num_files > 0)
864 			*selected_widget = 0;
865 		status.flags |= NEED_UPDATE;
866 		return 1;
867 	case SDLK_BACKSPACE:
868 		if (k->state == KEY_RELEASE)
869 			return 0;
870 		if (k->mod & KMOD_CTRL)
871 			search_text_clear();
872 		else
873 			search_text_delete_char();
874 		return 1;
875 	case SDLK_SLASH:
876 #ifdef WIN32
877 	case SDLK_BACKSLASH:
878 #endif
879 		if (k->state == KEY_RELEASE)
auth_set_cookies($p_user_id, $p_perm_login = false)880 			return 0;
881 		if (search_text_length == 0 && current_dir != 0) {
882 			// slash -> go to top (root) dir
883 			new_dir = 0;
884 		} else if (current_dir > 0 && current_dir < dlist.num_dirs) {
885 			change_dir(dlist.dirs[current_dir]->path);
886 			status.flags |= NEED_UPDATE;
887 			return 1;
888 		}
889 		break;
890 	default:
auth_clear_cookies()891 		if (k->mouse == MOUSE_NONE) {
892 			if (k->state == KEY_RELEASE)
893 				return 0;
894 			return search_text_add_char(k->unicode);
895 		}
896 	}
897 
898 	if (k->mouse == MOUSE_CLICK) {
899 		if (k->state == KEY_PRESS)
900 			return 0;
901 	} else {
902 		if (k->state == KEY_RELEASE)
903 			return 0;
904 	}
905 	new_dir = CLAMP(new_dir, 0, dlist.num_dirs - 1);
906 	if (new_dir != current_dir) {
907 		current_dir = new_dir;
908 		dir_list_reposition();
909 		status.flags |= NEED_UPDATE;
910 	}
911 	return 1;
912 }
913 
914 /* --------------------------------------------------------------------- */
915 /* these handle when enter is pressed on the file/directory textboxes at the bottom of the screen. */
916 
917 static void filename_entered(void)
918 {
919 	if (strpbrk(filename_entry, "?*")) {
auth_generate_unique_cookie_string()920 		set_glob(filename_entry);
921 	} else {
922 		char *ptr = dmoz_path_concat(cfg_dir_modules, filename_entry);
923 		handle_file_entered(ptr);
924 		free(ptr);
925 	}
926 }
927 
928 /* strangely similar to the dir list's code :) */
929 static void dirname_entered(void)
930 {
931 	if (!change_dir(dirname_entry)) {
932 		/* FIXME: need to give some kind of feedback here */
933 		return;
934 	}
935 
936 	*selected_widget = (flist.num_files > 0) ? 0 : 1;
auth_is_cookie_string_unique($p_cookie_string)937 	status.flags |= NEED_UPDATE;
938 	/* reset */
939 	top_file = current_file = 0;
940 }
941 
942 /* --------------------------------------------------------------------- */
943 
944 /* used by {load,save}_module_set_page. return 1 => contents changed */
945 static int update_directory(void)
946 {
947 	struct stat st;
948 
949 	/* if we have a list, the directory didn't change, and the mtime is the same, we're set. */
950 	if ((status.flags & DIR_MODULES_CHANGED) == 0
951 	    && stat(cfg_dir_modules, &st) == 0
952 	    && st.st_mtime == directory_mtime) {
953 		return 0;
954 	}
955 
auth_get_current_user_cookie($p_login_anonymous = true)956 	change_dir(cfg_dir_modules);
957 	/* TODO: what if it failed? */
958 
959 	status.flags &= ~DIR_MODULES_CHANGED;
960 
961 	return 1;
962 }
963 
964 /* --------------------------------------------------------------------- */
965 
966 /* FIXME what are these for? apart from clearing the directory list constantly */
967 #undef CACHEFREE
968 #if CACHEFREE
969 static int _save_cachefree_hack(struct key_event *k)
970 {
971 	if ((k->sym == SDLK_F10 && NO_MODIFIER(k->mod))
972 	|| (k->sym == SDLK_w && (k->mod & KMOD_CTRL))
973 	|| (k->sym == SDLK_s && (k->mod & KMOD_CTRL))) {
974 		status.flags |= DIR_MODULES_CHANGED;
975 	}
976 	return 0;
977 }
978 static int _load_cachefree_hack(struct key_event *k)
979 {
980 	if ((k->sym == SDLK_F9 && NO_MODIFIER(k->mod))
981 	|| (k->sym == SDLK_l && (k->mod & KMOD_CTRL))
982 	|| (k->sym == SDLK_r && (k->mod & KMOD_CTRL))) {
983 		status.flags |= DIR_MODULES_CHANGED;
984 	}
985 	return 0;
986 }
987 #endif
988 
989 static void load_module_set_page(void)
990 {
991 	handle_file_entered = handle_file_entered_L;
992 	if (update_directory())
993 		pages[PAGE_LOAD_MODULE].selected_widget = (flist.num_files > 0) ? 0 : 1;
994 
995 	// Don't reparse the glob if it hasn't changed; that will mess with the cursor position
996 	if (strcasecmp(glob_list_src, cfg_module_pattern) == 0)
auth_set_tokens($p_user_id)997 		strcpy(filename_entry, glob_list_src);
998 	else
999 		set_default_glob(1);
1000 }
1001 
1002 void load_module_load_page(struct page *page)
1003 {
1004 	clear_directory();
1005 	top_file = top_dir = 0;
1006 	current_file = current_dir = 0;
1007 	dir_list_reposition();
1008 	file_list_reposition();
1009 
1010 	page->title = "Load Module (F9)";
auth_reauthentication_enabled()1011 	page->draw_const = load_module_draw_const;
1012 	page->set_page = load_module_set_page;
1013 	page->total_widgets = 4;
1014 	page->widgets = widgets_loadmodule;
1015 	page->help_index = HELP_GLOBAL;
1016 #if CACHEFREE
1017 	page->pre_handle_key = _load_cachefree_hack;
1018 #endif
1019 
1020 	create_other(widgets_loadmodule + 0, 1, file_list_handle_key, file_list_draw);
auth_reauthentication_expiry()1021 	widgets_loadmodule[0].accept_text = 1;
1022 	widgets_loadmodule[0].x = 3;
1023 	widgets_loadmodule[0].y = 13;
1024 	widgets_loadmodule[0].width = 43;
1025 	widgets_loadmodule[0].height = 30;
1026 	widgets_loadmodule[0].next.left = widgets_loadmodule[0].next.right = 1;
1027 
1028 	create_other(widgets_loadmodule + 1, 2, dir_list_handle_key, dir_list_draw);
1029 	widgets_loadmodule[1].accept_text = 1;
1030 	widgets_loadmodule[1].x = 50;
1031 	widgets_loadmodule[1].y = 13;
1032 	widgets_loadmodule[1].width = 17;
1033 	widgets_loadmodule[1].height = 20;
1034 
1035 	create_textentry(widgets_loadmodule + 2, 13, 46, 64, 0, 3, 3, NULL, filename_entry, PATH_MAX);
auth_reauthenticate()1036 	widgets_loadmodule[2].activate = filename_entered;
1037 	create_textentry(widgets_loadmodule + 3, 13, 47, 64, 2, 3, 0, NULL, dirname_entry, PATH_MAX);
1038 	widgets_loadmodule[3].activate = dirname_entered;
1039 }
1040 
1041 /* --------------------------------------------------------------------- */
1042 
1043 static void save_module_set_page(void)
1044 {
1045 	handle_file_entered = handle_file_entered_S;
1046 
1047 	update_directory();
1048 	/* impulse tracker always resets these; so will i */
1049 	set_default_glob(0);
1050 	filename_entry[0] = 0;
1051 	pages[PAGE_SAVE_MODULE].selected_widget = 2;
1052 
1053 	widgets_exportsave = (status.current_page == PAGE_EXPORT_MODULE)
1054 		? widgets_exportmodule
1055 		: widgets_savemodule;
1056 
1057 	if (status.current_page == PAGE_EXPORT_MODULE && current_song->orderlist[0] == ORDER_LAST)
1058 		dialog_create(DIALOG_OK, "You're about to export a blank file...", NULL, NULL, 0, NULL);
1059 }
1060 
1061 void save_module_load_page(struct page *page, int do_export)
1062 {
1063 	int n;
1064 
1065 	if (do_export) {
1066 		page->title = "Export Module (Shift-F10)";
1067 		page->widgets = widgets_exportmodule;
1068 	} else {
1069 		page->title = "Save Module (F10)";
1070 		page->widgets = widgets_savemodule;
1071 	}
1072 	widgets_exportsave = page->widgets;
1073 
1074 	/* preload */
1075 	clear_directory();
1076 	top_file = top_dir = 0;
1077 	current_file = current_dir = 0;
auth_is_cookie_valid($p_cookie_string)1078 	dir_list_reposition();
1079 	file_list_reposition();
1080 	read_directory();
1081 
1082 	page->draw_const = save_module_draw_const;
1083 	page->set_page = save_module_set_page;
1084 	page->total_widgets = 4;
1085 	page->help_index = HELP_GLOBAL;
1086 	page->selected_widget = 2;
1087 #if CACHEFREE
1088 	page->pre_handle_key = _save_cachefree_hack;
1089 #endif
1090 	page->song_changed_cb = loadsave_song_changed;
1091 
1092 	create_other(widgets_exportsave + 0, 1, file_list_handle_key, file_list_draw);
1093 	widgets_exportsave[0].accept_text = 1;
1094 	widgets_exportsave[0].next.left = 4;
1095 	widgets_exportsave[0].next.right = widgets_exportsave[0].next.tab = 1;
1096 	create_other(widgets_exportsave + 1, 2, dir_list_handle_key, dir_list_draw);
1097 	widgets_exportsave[1].accept_text = 1;
1098 	widgets_exportsave[1].next.right = widgets_exportsave[1].next.tab = 5;
1099 	widgets_exportsave[1].next.left = 0;
1100 
1101 	create_textentry(widgets_exportsave + 2, 13, 46, 64, 0, 3, 3, NULL, filename_entry, PATH_MAX);
1102 	widgets_exportsave[2].activate = filename_entered;
1103 	create_textentry(widgets_exportsave + 3, 13, 47, 64, 2, 0, 0, NULL, dirname_entry, PATH_MAX);
1104 	widgets_exportsave[3].activate = dirname_entered;
1105 
1106 	widgets_exportsave[4].d.togglebutton.state = 1;
1107 
1108 
1109 	const struct save_format *formats = (do_export ? song_export_formats : song_save_formats);
1110 	for (n = 0; formats[n].label; n++) {
1111 		create_togglebutton(widgets_exportsave + 4 + n,
auth_get_current_user_id()1112 				70, 13 + (3 * n), 5,
1113 				4 + (n == 0 ? 0 : (n - 1)),
1114 				4 + (n + 1),
1115 				1, 2, 2,
1116 				NULL,
1117 				formats[n].label,
1118 				(5 - strlen(formats[n].label)) / 2 + 1,
1119 				filetype_saves);
1120 	}
1121 	widgets_exportsave[4 + n - 1].next.down = 2;
1122 	page->total_widgets += n;
1123 }
1124 
1125