/** Downloads managment * @file */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #ifdef HAVE_SYS_CYGWIN_H #include #endif #include #ifdef HAVE_FCNTL_H #include /* OS/2 needs this after sys/types.h */ #endif #include #ifdef HAVE_UNISTD_H #include #endif #include #include "elinks.h" #include "bfu/dialog.h" #include "cache/cache.h" #include "config/options.h" #include "dialogs/document.h" #include "dialogs/download.h" #include "dialogs/menu.h" #include "intl/libintl.h" #include "main/object.h" #include "main/select.h" #include "mime/mime.h" #include "network/connection.h" #include "network/progress.h" #include "network/state.h" #include "osdep/osdep.h" #include "protocol/bittorrent/dialogs.h" #include "protocol/date.h" #include "protocol/protocol.h" #include "protocol/uri.h" #include "session/download.h" #include "session/history.h" #include "session/location.h" #include "session/session.h" #include "session/task.h" #include "terminal/draw.h" #include "terminal/screen.h" #include "terminal/terminal.h" #include "util/conv.h" #include "util/error.h" #include "util/file.h" #include "util/lists.h" #include "util/memlist.h" #include "util/memory.h" #include "util/string.h" #include "util/time.h" /* TODO: tp_*() should be in separate file, I guess? --pasky */ INIT_LIST_OF(struct file_download, downloads); int download_is_progressing(struct download *download) { return download && is_in_state(download->state, S_TRANS) && has_progress(download->progress); } int are_there_downloads(void) { struct file_download *file_download; foreach (file_download, downloads) if (!file_download->external_handler) return 1; return 0; } static void download_data(struct download *download, struct file_download *file_download); /*! @note If this fails, the caller is responsible of freeing @a file * and closing @a fd. */ struct file_download * init_file_download(struct uri *uri, struct session *ses, char *file, int fd) { struct file_download *file_download; file_download = (struct file_download *)mem_calloc(1, sizeof(*file_download)); if (!file_download) return NULL; /* Actually we could allow fragments in the URI and just change all the * places that compares and shows the URI, but for now it is much * easier this way. */ file_download->uri = get_composed_uri(uri, URI_BASE); if (!file_download->uri) { mem_free(file_download); return NULL; } init_download_display(file_download); file_download->file = file; file_download->handle = fd; file_download->download.callback = (download_callback_T *) download_data; file_download->download.data = file_download; file_download->ses = ses; /* The tab may be closed, but we will still want to ie. open the * handler on that terminal. */ file_download->term = ses->tab->term; object_nolock(file_download, "file_download"); /* Debugging purpose. */ add_to_list(downloads, file_download); return file_download; } void abort_download(struct file_download *file_download) { #if 0 /* When hacking to cleanup the download code, remove lots of duplicated * code and implement stuff from bug 435 we should reintroduce this * assertion. Currently it will trigger often and shows that the * download dialog code potentially could access free()d memory. */ assert(!is_object_used(file_download)); #endif done_download_display(file_download); if (file_download->ses) check_questions_queue(file_download->ses); if (file_download->dlg_data) cancel_dialog(file_download->dlg_data, NULL); cancel_download(&file_download->download, file_download->stop); if (file_download->uri) done_uri(file_download->uri); if (file_download->handle != -1) { prealloc_truncate(file_download->handle, file_download->seek); close(file_download->handle); } mem_free_if(file_download->external_handler); if (file_download->file) { if (file_download->delete_) unlink(file_download->file); mem_free(file_download->file); } mem_free_if(file_download->inpext); mem_free_if(file_download->outext); del_from_list(file_download); mem_free(file_download); } static void kill_downloads_to_file(char *file) { struct file_download *file_download; foreach (file_download, downloads) { if (strcmp(file_download->file, file)) continue; file_download = file_download->prev; abort_download(file_download->next); } } void abort_all_downloads(void) { while (!list_empty(downloads)) abort_download((struct file_download *)downloads.next); } void destroy_downloads(struct session *ses) { struct file_download *file_download, *next; struct session *s; /* We are supposed to blat all downloads to external handlers belonging * to @ses, but we will refuse to do so if there is another session * bound to this terminal. That looks like the reasonable thing to do, * fulfilling the principle of least astonishment. */ foreach (s, sessions) { if (s == ses || s->tab->term != ses->tab->term) continue; foreach (file_download, downloads) { if (file_download->ses != ses) continue; file_download->ses = s; } return; } foreachsafe (file_download, next, downloads) { if (file_download->ses != ses) continue; if (!file_download->external_handler) { file_download->ses = NULL; continue; } abort_download(file_download); } } void detach_downloads_from_terminal(struct terminal *term) { struct file_download *file_download, *next; assert(term != NULL); if_assert_failed return; foreachsafe (file_download, next, downloads) { if (file_download->term != term) continue; if (!file_download->external_handler) { file_download->term = NULL; if (file_download->ses && file_download->ses->tab->term == term) file_download->ses = NULL; continue; } abort_download(file_download); } } static void download_error_dialog(struct file_download *file_download, int saved_errno) { char *emsg = (char *) strerror(saved_errno); struct session *ses = file_download->ses; struct terminal *term = file_download->term; if (!ses) return; info_box(term, MSGBOX_FREE_TEXT, N_("Download error"), ALIGN_CENTER, msg_text(term, N_("Could not create file '%s':\n%s"), file_download->file, emsg)); } static int write_cache_entry_to_file(struct cache_entry *cached, struct file_download *file_download) { struct fragment *frag; if (file_download->download.progress && file_download->download.progress->seek) { file_download->seek = file_download->download.progress->seek; file_download->download.progress->seek = 0; /* This is exclusive with the prealloc, thus we can perform * this in front of that thing safely. */ if (lseek(file_download->handle, file_download->seek, SEEK_SET) < 0) { download_error_dialog(file_download, errno); return 0; } } foreach (frag, cached->frag) { off_t remain = file_download->seek - frag->offset; int *h = &file_download->handle; ssize_t w; if (remain < 0 || frag->length <= remain) continue; #ifdef USE_OPEN_PREALLOC if (!file_download->seek && (!file_download->download.progress || file_download->download.progress->size > 0)) { close(*h); *h = open_prealloc(file_download->file, O_CREAT|O_WRONLY|O_TRUNC, 0666, file_download->download.progress ? file_download->download.progress->size : cached->length); if (*h == -1) { download_error_dialog(file_download, errno); return 0; } set_bin(*h); } #endif w = safe_write(*h, frag->data + remain, frag->length - remain); if (w == -1) { download_error_dialog(file_download, errno); return 0; } file_download->seek += w; } return 1; } static void abort_download_and_beep(struct file_download *file_download, struct terminal *term) { if (term && get_opt_int("document.download.notify_bell", file_download->ses) + file_download->notify >= 2) { beep_terminal(term); } abort_download(file_download); } struct exec_mailcap { struct session *ses; char *command; char *file; }; static void do_follow_url_mailcap(struct session *ses, struct uri *uri) { if (!uri) { print_error_dialog(ses, connection_state(S_BAD_URL), uri, PRI_CANCEL); return; } ses->reloadlevel = CACHE_MODE_NORMAL; if (ses->task.type == TASK_FORWARD) { if (compare_uri(ses->loading_uri, uri, 0)) { /* We're already loading the URL. */ return; } } abort_loading(ses, 0); ses_goto(ses, uri, NULL, NULL, CACHE_MODE_NORMAL, TASK_FORWARD, 0); } static void exec_mailcap_command(void *data) { struct exec_mailcap *exec_mailcap = (struct exec_mailcap *)data; if (exec_mailcap) { if (exec_mailcap->command) { struct string string; if (init_string(&string)) { static char mailcap_elmailcap[] = "mailcap:elmailcap"; struct uri *ref = get_uri(mailcap_elmailcap, URI_NONE); struct uri *uri; struct session *ses = exec_mailcap->ses; add_to_string(&string, "mailcap:"); add_to_string(&string, exec_mailcap->command); if (exec_mailcap->file) { add_to_string(&string, " && /bin/rm -f "); add_to_string(&string, exec_mailcap->file); } uri = get_uri(string.source, URI_NONE); done_string(&string); set_session_referrer(ses, ref); if (ref) done_uri(ref); do_follow_url_mailcap(ses, uri); if (uri) done_uri(uri); } mem_free(exec_mailcap->command); } mem_free_if(exec_mailcap->file); mem_free(exec_mailcap); } } static void exec_later(struct session *ses, char *handler, char *file) { struct exec_mailcap *exec_mailcap = (struct exec_mailcap *)mem_calloc(1, sizeof(*exec_mailcap)); if (exec_mailcap) { exec_mailcap->ses = ses; exec_mailcap->command = null_or_stracpy(handler); exec_mailcap->file = null_or_stracpy(file); register_bottom_half(exec_mailcap_command, exec_mailcap); } } static void exec_dgi_command(void *data) { struct exec_dgi *exec_dgi = (struct exec_dgi *)data; if (exec_dgi) { if (exec_dgi->command) { struct string string; if (init_string(&string)) { static char dgi_dgi[] = "dgi://"; struct uri *ref = get_uri(dgi_dgi, URI_NONE); struct uri *uri; struct session *ses = exec_dgi->ses; add_to_string(&string, "dgi:///dgi?command="); add_to_string(&string, exec_dgi->command); add_to_string(&string, "&filename="); if (exec_dgi->file) { add_to_string(&string, exec_dgi->file); } add_to_string(&string, "&inpext="); if (exec_dgi->inpext) { add_to_string(&string, exec_dgi->inpext); } add_to_string(&string, "&outext="); if (exec_dgi->outext) { add_to_string(&string, exec_dgi->outext); } if (exec_dgi->del) { add_to_string(&string, "&delete=1"); } uri = get_uri(string.source, URI_BASE_FRAGMENT); done_string(&string); set_session_referrer(ses, ref); if (ref) done_uri(ref); do_follow_url_mailcap(ses, uri); if (uri) done_uri(uri); } mem_free(exec_dgi->command); } mem_free_if(exec_dgi->file); mem_free_if(exec_dgi->inpext); mem_free_if(exec_dgi->outext); mem_free(exec_dgi); } } static void exec_later_dgi(struct session *ses, char *handler, char *file, char *inpext, char *outext, int del) { struct exec_dgi *exec_dgi = (struct exec_dgi *)mem_calloc(1, sizeof(*exec_dgi)); if (exec_dgi) { exec_dgi->ses = ses; exec_dgi->command = null_or_stracpy(handler); exec_dgi->file = null_or_stracpy(file); exec_dgi->inpext = null_or_stracpy(inpext); exec_dgi->outext = null_or_stracpy(outext); exec_dgi->del = del; register_bottom_half(exec_dgi_command, exec_dgi); } } static void download_data_store(struct download *download, struct file_download *file_download) { struct terminal *term = file_download->term; assert_terminal_ptr_not_dangling(term); if_assert_failed term = file_download->term = NULL; if (is_in_progress_state(download->state)) { if (file_download->dlg_data) redraw_dialog(file_download->dlg_data, 1); return; } /* If the original terminal of the download has been closed, * display any messages in the default terminal instead. */ if (term == NULL) term = get_default_terminal(); /* may be NULL too */ if (!is_in_state(download->state, S_OK)) { char *url = get_uri_string(file_download->uri, URI_PUBLIC); struct connection_state state = download->state; /* abort_download_and_beep allows term==NULL. */ abort_download_and_beep(file_download, term); if (!url) return; if (term) { info_box(term, MSGBOX_FREE_TEXT, N_("Download error"), ALIGN_CENTER, msg_text(term, N_("Error downloading %s:\n\n%s"), url, get_state_message(state, term))); } mem_free(url); return; } if (file_download->external_handler) { if (term == NULL) { /* There is no terminal in which to run the handler. * Abort the download. file_download->delete_ should * be 1 here so that the following call also deletes * the temporary file. */ abort_download(file_download); return; } prealloc_truncate(file_download->handle, file_download->seek); close(file_download->handle); file_download->handle = -1; if (file_download->copiousoutput) { exec_later(file_download->ses, file_download->external_handler, file_download->file); /* Temporary file is deleted by the mailcap_protocol_handler */ file_download->delete_ = 0; } else if (file_download->dgi) { exec_later_dgi(file_download->ses, file_download->external_handler, file_download->file, file_download->inpext, file_download->outext, 1); /* Temporary file is deleted by the dgi_protocol_handler */ file_download->delete_ = 0; } else { exec_on_terminal(term, file_download->external_handler, file_download->file, file_download->block ? TERM_EXEC_FG : TERM_EXEC_BG); } file_download->delete_ = 0; abort_download_and_beep(file_download, term); return; } if (file_download->notify && term) { char *url = get_uri_string(file_download->uri, URI_PUBLIC); /* This is apparently a little racy. Deleting the box item will * update the download browser _after_ the notification dialog * has been drawn whereby it will be hidden. This should make * the download browser update before launcing any * notification. */ done_download_display(file_download); if (url) { info_box(term, MSGBOX_FREE_TEXT, N_("Download"), ALIGN_CENTER, msg_text(term, N_("Download complete:\n%s"), url)); mem_free(url); } } if (file_download->remotetime && get_opt_bool("document.download.set_original_time", file_download->ses)) { struct utimbuf foo; foo.actime = foo.modtime = file_download->remotetime; utime(file_download->file, &foo); } /* abort_download_and_beep allows term==NULL. */ abort_download_and_beep(file_download, term); } static void download_data(struct download *download, struct file_download *file_download) { struct cache_entry *cached = download->cached; if (!cached || is_in_queued_state(download->state)) { download_data_store(download, file_download); return; } if (cached->last_modified) file_download->remotetime = parse_date(&cached->last_modified, NULL, 0, 1); if (cached->redirect && file_download->redirect_cnt++ < MAX_REDIRECTS) { cancel_download(&file_download->download, 0); assertm(compare_uri(cached->uri, file_download->uri, 0), "Redirecting using bad base URI"); done_uri(file_download->uri); file_download->uri = get_uri_reference(cached->redirect); file_download->download.state = connection_state(S_WAIT_REDIR); if (file_download->dlg_data) redraw_dialog(file_download->dlg_data, 1); load_uri(file_download->uri, cached->uri, &file_download->download, PRI_DOWNLOAD, CACHE_MODE_NORMAL, download->progress ? download->progress->start : 0); return; } if (!write_cache_entry_to_file(cached, file_download)) { detach_connection(download, file_download->seek); abort_download(file_download); return; } detach_connection(download, file_download->seek); download_data_store(download, file_download); } /** Type of the callback function that will be called when the user * answers the question posed by lookup_unique_name(). * * @param term * The terminal on which the callback should display any windows. * Comes directly from the @a term argument of lookup_unique_name(). * * @param file * The name of the local file to which the data should be downloaded, * or NULL if the download should not begin. The callback is * responsible of doing mem_free(@a file). * * @param data * A pointer to any data that the callback cares about. * Comes directly from the @a data argument of lookup_unique_name(). * * @param flags * The same as the @a flags argument of create_download_file(), * except the ::DOWNLOAD_RESUME_SELECTED bit will be changed to match * what the user chose. * * @relates lun_hop */ typedef void lun_callback_T(struct terminal *term, char *file, void *data, download_flags_T flags); /** The user is being asked what to do when the local file for * the download already exists. This structure is allocated by * lookup_unique_name() and freed by each lun_* function: * lun_alternate(), lun_cancel(), lun_overwrite(), and lun_resume(). */ struct lun_hop { /** The terminal in which ELinks is asking the question. * This gets passed to #callback. */ struct terminal *term; /** The name of the local file into which the data was * originally going to be downloaded, but which already * exists. In this string, "~" has already been expanded * to the home directory. The string must be freed with * mem_free(). */ char *ofile; /** An alternative file name that the user may choose instead * of #ofile. The string must be freed with mem_free(). */ char *file; /** This function will be called when the user answers. */ lun_callback_T *callback; /** A pointer to be passed to #callback. */ void *data; /** Saved flags to be passed to #callback. * If the user chooses to resume, then lun_resume() sets * ::DOWNLOAD_RESUME_SELECTED when it calls #callback. * * @invariant The ::DOWNLOAD_RESUME_SELECTED bit should be * clear here because otherwise there would have been no * reason to ask the user and initialize this structure. */ download_flags_T flags; }; /** Data saved by common_download() for the common_download_do() * callback. */ struct cmdw_hop { struct session *ses; /** The URI from which the data will be downloaded. */ struct uri *download_uri; /** The name of the local file to which the data will be * downloaded. This is initially NULL, but its address is * given to create_download_file(), which arranges for the * pointer to be set before common_download_do() is called. * The string must be freed with mem_free(). */ char *real_file; }; /** Data saved by continue_download() for the continue_download_do() * callback. */ struct codw_hop { struct type_query *type_query; /** The name of the local file to which the data will be * downloaded. This is initially NULL, but its address is * given to create_download_file(), which arranges for the * pointer to be set before continue_download_do() is called. * The string must be freed with mem_free(). */ char *real_file; char *file; }; /** Data saved by create_download_file() for the create_download_file_do() * callback. */ struct cdf_hop { /** Where to save the name of the file that was actually * opened. One of the arguments of #callback is a file * descriptor for this file. @c real_file can be NULL if * #callback does not care about the name. */ char **real_file; /** This function will be called when the file has been opened, * or when it is known that the file will not be opened. */ cdf_callback_T *callback; /** A pointer to be passed to #callback. */ void *data; }; /** The use chose "Save under the alternative name" when asked where * to download a file. * * lookup_unique_name() passes this function as a ::done_handler_T to * msg_box(). * * @relates lun_hop */ static void lun_alternate(void *lun_hop_) { struct lun_hop *lun_hop = (struct lun_hop *)lun_hop_; lun_hop->callback(lun_hop->term, lun_hop->file, lun_hop->data, lun_hop->flags); mem_free_if(lun_hop->ofile); mem_free(lun_hop); } /** The use chose "Cancel" when asked where to download a file. * * lookup_unique_name() passes this function as a ::done_handler_T to * msg_box(). * * @relates lun_hop */ static void lun_cancel(void *lun_hop_) { struct lun_hop *lun_hop = (struct lun_hop *)lun_hop_; lun_hop->callback(lun_hop->term, NULL, lun_hop->data, lun_hop->flags); mem_free_if(lun_hop->ofile); mem_free_if(lun_hop->file); mem_free(lun_hop); } /** The use chose "Overwrite the original file" when asked where to * download a file. * * lookup_unique_name() passes this function as a ::done_handler_T to * msg_box(). * * @relates lun_hop */ static void lun_overwrite(void *lun_hop_) { struct lun_hop *lun_hop = (struct lun_hop *)lun_hop_; lun_hop->callback(lun_hop->term, lun_hop->ofile, lun_hop->data, lun_hop->flags); mem_free_if(lun_hop->file); mem_free(lun_hop); } /** The user chose "Resume download of the original file" when asked * where to download a file. * * lookup_unique_name() passes this function as a ::done_handler_T to * msg_box(). * * @relates lun_hop */ static void lun_resume(void *lun_hop_) { struct lun_hop *lun_hop = (struct lun_hop *)lun_hop_; lun_hop->callback(lun_hop->term, lun_hop->ofile, lun_hop->data, lun_hop->flags | DOWNLOAD_RESUME_SELECTED); mem_free_if(lun_hop->file); mem_free(lun_hop); } /** If attempting to download to an existing file, perhaps ask * the user whether to resume, overwrite, or save elsewhere. * This function constructs a struct lun_hop, which will be freed * when the user answers the question. * * @param term * The terminal in which this function should show its UI. * * @param[in] ofile * A proposed name for the local file to which the data would be * downloaded. "~" here refers to the home directory. * lookup_unique_name() treats this original string as read-only. * * @param[in] flags * Flags controlling how to download the file. * ::DOWNLOAD_RESUME_ALLOWED adds a "Resume" button to the dialog. * ::DOWNLOAD_RESUME_SELECTED means the user already chose to resume * downloading (with ::ACT_MAIN_LINK_DOWNLOAD_RESUME), before ELinks * even asked for the file name; thus don't ask whether to overwrite. * Other flags, such as ::DOWNLOAD_EXTERNAL, have no effect at this * level but they get passed to @a callback. * * @param callback * Will be called when the user answers, or right away if the question * need not or cannot be asked. * * @param data * A pointer to be passed to @a callback. * * @relates lun_hop */ static void lookup_unique_name(struct terminal *term, char *ofile, download_flags_T flags, lun_callback_T *callback, void *data) { /* [gettext_accelerator_context(.lookup_unique_name)] */ struct lun_hop *lun_hop = NULL; char *file = NULL; struct dialog_data *dialog_data; int overwrite; ofile = expand_tilde(ofile); if (!ofile) goto error; /* Minor code duplication to prevent useless call to get_opt_int() * if possible. --Zas */ if (flags & DOWNLOAD_RESUME_SELECTED) { callback(term, ofile, data, flags); return; } /* !overwrite means always silently overwrite, which may be admitelly * indeed a little confusing ;-) */ overwrite = get_opt_int("document.download.overwrite", NULL); if (!overwrite) { /* Nothing special to do... */ callback(term, ofile, data, flags); return; } /* Check if file is a directory, and use a default name if it's the * case. */ if (file_is_dir(ofile)) { info_box(term, MSGBOX_FREE_TEXT, N_("Download error"), ALIGN_CENTER, msg_text(term, N_("'%s' is a directory."), ofile)); goto error; } /* Check if the file already exists (file != ofile). */ file = (flags & DOWNLOAD_OVERWRITE) ? ofile : get_unique_name(ofile); if (!file || overwrite == 1 || file == ofile) { /* Still nothing special to do... */ if (file != ofile) mem_free(ofile); callback(term, file, data, flags & ~DOWNLOAD_RESUME_SELECTED); return; } /* overwrite == 2 (ask) and file != ofile (=> original file already * exists) */ lun_hop = (struct lun_hop *)mem_calloc(1, sizeof(*lun_hop)); if (!lun_hop) goto error; lun_hop->term = term; lun_hop->ofile = ofile; lun_hop->file = file; /* file != ofile verified above */ lun_hop->callback = callback; lun_hop->data = data; lun_hop->flags = flags; dialog_data = msg_box( term, NULL, MSGBOX_FREE_TEXT, N_("File exists"), ALIGN_CENTER, msg_text(term, N_("This file already exists:\n" "%s\n\n" "The alternative filename is:\n" "%s"), empty_string_or_(lun_hop->ofile), empty_string_or_(file)), lun_hop, 4, MSG_BOX_BUTTON(N_("Sa~ve under the alternative name"), lun_alternate, B_ENTER), MSG_BOX_BUTTON(N_("~Overwrite the original file"), lun_overwrite, 0), MSG_BOX_BUTTON((flags & DOWNLOAD_RESUME_ALLOWED ? N_("~Resume download of the original file") : NULL), lun_resume, 0), MSG_BOX_BUTTON(N_("~Cancel"), lun_cancel, B_ESC)); if (!dialog_data) goto error; return; error: mem_free_if(lun_hop); if (file != ofile) mem_free_if(file); mem_free_if(ofile); callback(term, NULL, data, flags & ~DOWNLOAD_RESUME_SELECTED); } /** Now that the final name of the download file has been chosen, * open the file and call the ::cdf_callback_T that was originally * given to create_download_file(). * * create_download_file() passes this function as a ::lun_callback_T * to lookup_unique_name(). * * @relates cdf_hop */ static void create_download_file_do(struct terminal *term, char *file, void *data, download_flags_T flags) { struct cdf_hop *cdf_hop = (struct cdf_hop *)data; char *wd; int h = -1; int saved_errno; #ifdef NO_FILE_SECURITY int sf = 0; #else int sf = !!(flags & DOWNLOAD_EXTERNAL); #endif if (!file) goto finish; wd = get_cwd(); set_cwd(term->cwd); /* Create parent directories if needed. */ mkalldirs(file); /* O_APPEND means repositioning at the end of file before each write(), * thus ignoring seek()s and that can hide mysterious bugs. IMHO. * --pasky */ h = open(file, O_CREAT | O_WRONLY | (flags & DOWNLOAD_RESUME_SELECTED ? 0 : O_TRUNC) | (sf && !(flags & DOWNLOAD_RESUME_SELECTED) && !(flags & DOWNLOAD_OVERWRITE) ? O_EXCL : 0), sf ? 0600 : 0666); saved_errno = errno; /* Saved in case of ... --Zas */ if (wd) { set_cwd(wd); mem_free(wd); } if (h == -1) { info_box(term, MSGBOX_FREE_TEXT, N_("Download error"), ALIGN_CENTER, msg_text(term, N_("Could not create file '%s':\n%s"), file, strerror(saved_errno))); mem_free(file); goto finish; } else { set_bin(h); if (!(flags & DOWNLOAD_EXTERNAL)) { char *download_dir = get_opt_str("document.download.directory", NULL); int i; safe_strncpy(download_dir, file, MAX_STR_LEN); /* Find the used directory so it's available in history */ for (i = strlen(download_dir); i >= 0; i--) if (dir_sep(download_dir[i])) break; download_dir[i + 1] = 0; } } if (cdf_hop->real_file) *cdf_hop->real_file = file; else mem_free(file); finish: cdf_hop->callback(term, h, cdf_hop->data, flags); mem_free(cdf_hop); } /** Create a file to which data can be downloaded. * This function constructs a struct cdf_hop that will be freed * when @a callback returns. * * @param term * If any dialog boxes are needed, show them in this terminal. * * @param fi * A proposed name for the local file to which the data would be * downloaded. "~" here refers to the home directory. * create_download_file() treats this original string as read-only. * * @param real_file * If non-NULL, prepare to save in *@a real_file the name of the local * file that was eventually opened. @a callback must then arrange for * this string to be freed with mem_free(). * * @param flags * Flags controlling how to download the file. * ::DOWNLOAD_RESUME_ALLOWED adds a "Resume" button to the dialog. * ::DOWNLOAD_RESUME_SELECTED skips the dialog entirely. * ::DOWNLOAD_EXTERNAL causes the file to be created with settings * suitable for a temporary file: give only the user herself access to * the file (even if the umask is looser), and create the file with * @c O_EXCL unless resuming. * * @param callback * This function will be called when the file has been opened, * or when it is known that the file will not be opened. * * @param data * A pointer to be passed to @a callback. * * @relates cdf_hop */ void create_download_file(struct terminal *term, char *fi, char **real_file, download_flags_T flags, cdf_callback_T *callback, void *data) { struct cdf_hop *cdf_hop = (struct cdf_hop *)mem_calloc(1, sizeof(*cdf_hop)); char *wd; if (!cdf_hop) { callback(term, -1, data, flags & ~DOWNLOAD_RESUME_SELECTED); return; } cdf_hop->real_file = real_file; cdf_hop->callback = callback; cdf_hop->data = data; /* FIXME: The wd bussiness is probably useless here? --pasky */ wd = get_cwd(); set_cwd(term->cwd); /* Also the tilde will be expanded here. */ lookup_unique_name(term, fi, flags, create_download_file_do, cdf_hop); if (wd) { set_cwd(wd); mem_free(wd); } } static char * get_temp_name(struct uri *uri) { char *extension; char *nm; extension = get_extension_from_uri(uri); if (!extension) extension = stracpy(""); nm = tempname(NULL, ELINKS_TEMPNAME_PREFIX, extension); mem_free(extension); return nm; } static char * subst_file(char *prog, char *file, char *uri) { struct string name; /* When there is no %s in the mailcap entry, the handler program reads * data from stdin instead of a file. */ int input = 1; char *replace; char *original = (char *)("% "); int truncate; int tlen = 40; if (!init_string(&name)) return NULL; while (*prog) { int p; for (p = 0; prog[p] && prog[p] != '%'; p++); add_bytes_to_string(&name, prog, p); prog += p; if (*prog == '%') { prog++; truncate = 0; if (*prog == 'f' || *prog == ' ' || *prog == '\0') replace = file; else if (*prog == 'u') { replace = uri; if (!memcmp(uri, "data:", sizeof("data:") - 1)) truncate = 1; } else if (*prog == '%') replace = (char *)("%"); else { original[1] = *prog; replace = original; } if (*prog == ' ' || *prog == '\0') prog--; input = 0; #if defined(HAVE_CYGWIN_CONV_TO_FULL_WIN32_PATH) #ifdef MAX_PATH char new_path[MAX_PATH]; #else char new_path[1024]; #endif cygwin_conv_to_full_win32_path(replace, new_path); add_to_string(&name, new_path); #else if (! truncate || strlen(replace) <= tlen) add_shell_quoted_to_string(&name, replace, strlen(replace)); else { add_shell_quoted_to_string(&name, replace, tlen); add_shell_quoted_to_string(&name, "...", sizeof("...") - 1); } #endif prog++; } } if (input) { struct string s; if (init_string(&s)) { add_to_string(&s, "/bin/cat "); add_shell_quoted_to_string(&s, file, strlen(file)); add_to_string(&s, " | "); add_string_to_string(&s, &name); done_string(&name); return s.source; } } return name.source; } /*! common_download() passes this function as a ::cdf_callback_T to * create_download_file(). * * @relates cmdw_hop */ static void common_download_do(struct terminal *term, int fd, void *data, download_flags_T flags) { struct file_download *file_download; struct cmdw_hop *cmdw_hop = (struct cmdw_hop *)data; struct uri *download_uri = cmdw_hop->download_uri; char *file = cmdw_hop->real_file; struct session *ses = cmdw_hop->ses; struct stat buf; mem_free(cmdw_hop); if (!file || fstat(fd, &buf)) goto finish; file_download = init_file_download(download_uri, ses, file, fd); if (!file_download) goto finish; /* If init_file_download succeeds, it takes ownership of file * and fd. */ file = NULL; fd = -1; if (flags & DOWNLOAD_RESUME_SELECTED) file_download->seek = buf.st_size; display_download(ses->tab->term, file_download, ses); load_uri(file_download->uri, ses->referrer, &file_download->download, PRI_DOWNLOAD, CACHE_MODE_NORMAL, file_download->seek); finish: mem_free_if(file); if (fd != -1) close(fd); done_uri(download_uri); } /** Begin or resume downloading from session.download_uri to the * @a file specified by the user. * * This function contains the code shared between start_download() and * resume_download(). * * @relates cmdw_hop */ static void common_download(struct session *ses, char *file, download_flags_T flags) { struct cmdw_hop *cmdw_hop; if (!ses->download_uri) return; cmdw_hop = (struct cmdw_hop *)mem_calloc(1, sizeof(*cmdw_hop)); if (!cmdw_hop) return; cmdw_hop->ses = ses; cmdw_hop->download_uri = ses->download_uri; ses->download_uri = NULL; kill_downloads_to_file(file); create_download_file(ses->tab->term, file, &cmdw_hop->real_file, flags, common_download_do, cmdw_hop); } /** Begin downloading from session.download_uri to the @a file * specified by the user. * * The ::ACT_MAIN_SAVE_AS, ::ACT_MAIN_SAVE_URL_AS, * ::ACT_MAIN_LINK_DOWNLOAD, and ::ACT_MAIN_LINK_DOWNLOAD_IMAGE * actions pass this function as the @c std callback to query_file(). * * @relates cmdw_hop */ void start_download(void *ses, char *file) { common_download((struct session *)ses, file, DOWNLOAD_RESUME_ALLOWED); } /** Resume downloading from session.download_uri to the @a file * specified by the user. * * The ::ACT_MAIN_LINK_DOWNLOAD_RESUME action passes this function as * the @c std callback to query_file(). * * @relates cmdw_hop */ void resume_download(void *ses, char *file) { common_download((struct session *)ses, file, DOWNLOAD_RESUME_ALLOWED | DOWNLOAD_RESUME_SELECTED); } /** Resume downloading a file, based on information in struct * codw_hop. This function actually starts a new download from the * current end of the file, even though a download from the beginning * is already in progress at codw_hop->type_query->download. The * caller will cancel the preexisting download after this function * returns. * * @relates codw_hop */ static void transform_codw_to_cmdw(struct terminal *term, int fd, struct codw_hop *codw_hop, download_flags_T flags) { struct type_query *type_query = codw_hop->type_query; struct cmdw_hop *cmdw_hop = (struct cmdw_hop *)mem_calloc(1, sizeof(*cmdw_hop)); if (!cmdw_hop) { close(fd); return; } cmdw_hop->ses = type_query->ses; cmdw_hop->download_uri = get_uri_reference(type_query->uri); cmdw_hop->real_file = codw_hop->real_file; codw_hop->real_file = NULL; common_download_do(term, fd, cmdw_hop, flags); } /*! continue_download() passes this function as a ::cdf_callback_T to * create_download_file(). * * @relates codw_hop */ static void continue_download_do(struct terminal *term, int fd, void *data, download_flags_T flags) { struct codw_hop *codw_hop = (struct codw_hop *)data; struct file_download *file_download = NULL; struct type_query *type_query; assert(codw_hop); assert(codw_hop->type_query); assert(codw_hop->type_query->uri); assert(codw_hop->type_query->ses); type_query = codw_hop->type_query; if (!codw_hop->real_file) goto cancel; if (flags & DOWNLOAD_RESUME_SELECTED) { transform_codw_to_cmdw(term, fd, codw_hop, flags); fd = -1; /* ownership transfer */ goto cancel; } file_download = init_file_download(type_query->uri, type_query->ses, codw_hop->real_file, fd); if (!file_download) goto cancel; /* If init_file_download succeeds, it takes ownership of * codw_hop->real_file and fd. */ codw_hop->real_file = NULL; fd = -1; if (type_query->dgi && type_query->external_handler) { file_download->external_handler = type_query->external_handler; file_download->file = codw_hop->file; file_download->inpext = null_or_stracpy(type_query->inpext); file_download->outext = null_or_stracpy(type_query->outext); file_download->dgi = type_query->dgi; file_download->delete_ = 1; /* change owners a few lines above */ codw_hop->file = NULL; type_query->external_handler = NULL; } else if (type_query->external_handler) { file_download->external_handler = subst_file(type_query->external_handler, codw_hop->file, type_query->uri->string); file_download->delete_ = 1; file_download->copiousoutput = type_query->copiousoutput; mem_free(codw_hop->file); mem_free_set(&type_query->external_handler, NULL); } file_download->block = !!type_query->block; /* Done here and not in init_file_download() so that the external * handler can become initialized. */ display_download(term, file_download, type_query->ses); move_download(&type_query->download, &file_download->download, PRI_DOWNLOAD); done_type_query(type_query); mem_free(codw_hop); return; cancel: mem_free_if(codw_hop->real_file); if (fd != -1) close(fd); if (type_query->external_handler) mem_free_if(codw_hop->file); tp_cancel(type_query); mem_free(codw_hop); } /** When asked what to do with a file, the user chose to download it * to a local file named @a file. * Or an external handler was selected, in which case * type_query.external_handler is non-NULL and @a file does not * matter because this function will generate a name. * * tp_save() passes this function as the @c std callback to query_file(). * * @relates codw_hop */ static void continue_download(void *data, char *file) { struct type_query *type_query = (struct type_query *)data; struct codw_hop *codw_hop = (struct codw_hop *)mem_calloc(1, sizeof(*codw_hop)); if (!codw_hop) { tp_cancel(type_query); return; } if (type_query->external_handler) { file = get_temp_name(type_query->uri); if (!file) { mem_free(codw_hop); tp_cancel(type_query); return; } } codw_hop->type_query = type_query; codw_hop->file = file; kill_downloads_to_file(file); create_download_file(type_query->ses->tab->term, file, &codw_hop->real_file, type_query->external_handler ? DOWNLOAD_RESUME_ALLOWED | DOWNLOAD_EXTERNAL | DOWNLOAD_OVERWRITE : DOWNLOAD_RESUME_ALLOWED, continue_download_do, codw_hop); } /*! @relates type_query */ static struct type_query * find_type_query(struct session *ses) { struct type_query *type_query; foreach (type_query, ses->type_queries) if (compare_uri(type_query->uri, ses->loading_uri, 0)) return type_query; return NULL; } /** Prepare to ask the user what to do with a file, but don't display * the window yet. To display it, do_type_query() must be called * separately. setup_download_handler() takes care of that. * * @relates type_query */ static struct type_query * init_type_query(struct session *ses, struct download *download, struct cache_entry *cached) { struct type_query *type_query; type_query = (struct type_query *)mem_calloc(1, sizeof(*type_query)); if (!type_query) return NULL; type_query->uri = get_uri_reference(ses->loading_uri); type_query->ses = ses; type_query->target_frame = null_or_stracpy(ses->task.target.frame); type_query->cached = cached; type_query->cgi = cached->cgi; object_lock(type_query->cached); move_download(download, &type_query->download, PRI_MAIN); download->state = connection_state(S_OK); add_to_list(ses->type_queries, type_query); return type_query; } /** Cancel any download started for @a type_query, remove the structure * from the session.type_queries list, and free it. * * @relates type_query */ void done_type_query(struct type_query *type_query) { /* Unregister any active download */ cancel_download(&type_query->download, 0); object_unlock(type_query->cached); done_uri(type_query->uri); mem_free_if(type_query->inpext); mem_free_if(type_query->outext); mem_free_if(type_query->external_handler); mem_free_if(type_query->target_frame); del_from_list(type_query); mem_free(type_query); } /** The user chose "Cancel" when asked what to do with a file, * or the type query was cancelled for some other reason. * * do_type_query() and bittorrent_query_callback() pass this function * as a ::done_handler_T to add_dlg_ok_button(), and tp_save() passes * this function as a @c cancel callback to query_file(). * * @relates type_query */ void tp_cancel(void *data) { struct type_query *type_query = (struct type_query *)data; /* XXX: Should we really abort? (1 vs 0 as the last param) --pasky */ cancel_download(&type_query->download, 1); done_type_query(type_query); } /** The user chose "Save" when asked what to do with a file. * Now ask her where to save the file. * * do_type_query() and bittorrent_query_callback() pass this function * as a ::done_handler_T to add_dlg_ok_button(). * * @relates type_query */ void tp_save(struct type_query *type_query) { mem_free_set(&type_query->external_handler, NULL); query_file(type_query->ses, type_query->uri, type_query, continue_download, tp_cancel, 1); } /** The user chose "Show header" when asked what to do with a file. * * do_type_query() passes this function as a ::widget_handler_T to * add_dlg_button(). Unlike with add_dlg_ok_button(), pressing this * button does not close the dialog box. This way, the user can * first examine the header and then choose what to do. * * @relates type_query */ static widget_handler_status_T tp_show_header(struct dialog_data *dlg_data, struct widget_data *widget_data) { struct type_query *type_query = (struct type_query *)widget_data->widget->data; cached_header_dialog(type_query->ses, type_query->cached); return EVENT_PROCESSED; } /** The user chose "Display" when asked what to do with a file, * or she chose "Open" and there is no external handler. * * do_type_query() and bittorrent_query_callback() pass this function * as a ::done_handler_T to add_dlg_ok_button(). * * @bug FIXME: We need to modify this function to take frame data * instead, as we want to use this function for frames as well (now, * when frame has content type text/plain, it is ignored and displayed * as HTML). * * @relates type_query */ void tp_display(struct type_query *type_query) { struct view_state *vs; struct session *ses = type_query->ses; struct uri *loading_uri = ses->loading_uri; char *target_frame = null_or_stracpy(ses->task.target.frame); ses->loading_uri = type_query->uri; mem_free_set(&ses->task.target.frame, null_or_stracpy(type_query->target_frame)); vs = ses_forward(ses, /* type_query->frame */ 0); if (vs) vs->plain = 1; ses->loading_uri = loading_uri; mem_free_set(&ses->task.target.frame, target_frame); if (/* !type_query->frame */ 1) { struct download *old = &type_query->download; struct download *new_ = &cur_loc(ses)->download; new_->callback = (download_callback_T *) doc_loading_callback; new_->data = ses; move_download(old, new_, PRI_MAIN); } display_timer(ses); done_type_query(type_query); } /** The user chose "Open" when asked what to do with a file. * Or an external handler was found and it has been configured * to run without asking. * * do_type_query() passes this function as a ::done_handler_T to * add_dlg_ok_button(). * * @relates type_query */ static void tp_open(struct type_query *type_query) { if (!type_query->external_handler || !*type_query->external_handler) { tp_display(type_query); return; } if (type_query->uri->protocol == PROTOCOL_FILE && !type_query->cgi) { char *file = get_uri_string(type_query->uri, URI_PATH); char *handler = NULL; if (type_query->dgi) { if (file) { decode_uri(file); } exec_later_dgi(type_query->ses, type_query->external_handler, file, type_query->inpext, type_query->outext, 0); mem_free_if(file); done_type_query(type_query); return; } if (file) { decode_uri(file); handler = subst_file(type_query->external_handler, file, file); mem_free(file); } if (handler) { if (type_query->copiousoutput) { exec_later(type_query->ses, handler, NULL); } else { exec_on_terminal(type_query->ses->tab->term, handler, "", type_query->block ? TERM_EXEC_FG : TERM_EXEC_BG); } mem_free(handler); } done_type_query(type_query); return; } continue_download(type_query, (char *)("")); } /*! Ask the user what to do with a file. * * This function does not support BitTorrent downloads. * For those, query_bittorrent_dialog() must be called instead. * setup_download_handler() takes care of this. * * @relates type_query */ static void do_type_query(struct type_query *type_query, char *ct, struct mime_handler *handler) { /* [gettext_accelerator_context(.do_type_query)] */ struct string filename; const char *description; const char *desc_sep; char *format, *text, *title; struct dialog *dlg; #define TYPE_QUERY_WIDGETS_COUNT 8 int widgets = TYPE_QUERY_WIDGETS_COUNT; struct terminal *term = type_query->ses->tab->term; struct memory_list *ml; struct dialog_data *dlg_data; int selected_widget; mem_free_set(&type_query->external_handler, NULL); if (handler) { type_query->block = handler->block; type_query->copiousoutput = handler->copiousoutput; type_query->dgi = handler->dgi; type_query->inpext = null_or_stracpy(handler->inpext); type_query->outext = null_or_stracpy(handler->outext); if (!handler->ask) { type_query->external_handler = stracpy(handler->program); tp_open(type_query); return; } /* Start preparing for the type query dialog. */ description = handler->description; desc_sep = *description ? "; " : ""; title = N_("What to do?"); } else { title = N_("Unknown type"); description = ""; desc_sep = ""; } dlg = calloc_dialog(TYPE_QUERY_WIDGETS_COUNT, MAX_STR_LEN * 2); if (!dlg) return; if (init_string(&filename)) { add_mime_filename_to_string(&filename, type_query->uri); /* Let's make the filename pretty for display & save */ /* TODO: The filename can be the empty string here. See bug 396. */ #ifdef CONFIG_UTF8 if (term->utf8_cp) decode_uri_string(&filename); else #endif /* CONFIG_UTF8 */ decode_uri_string_for_display(&filename); } text = get_dialog_offset(dlg, TYPE_QUERY_WIDGETS_COUNT); /* For "default directory index pages" with wrong content-type * the filename can be NULL, e.g. http://www.spamhaus.org in bug 396. */ if (filename.length) { format = _("What would you like to do with the file '%s' (type: %s%s%s)?", term); snprintf(text, MAX_STR_LEN, format, filename.source, ct, desc_sep, description); } else { format = _("What would you like to do with the file (type: %s%s%s)?", term); snprintf(text, MAX_STR_LEN, format, ct, desc_sep, description); } done_string(&filename); dlg->title = _(title, term); dlg->layouter = generic_dialog_layouter; dlg->layout.padding_top = 1; dlg->layout.fit_datalen = 1; dlg->udata2 = type_query; add_dlg_text(dlg, text, ALIGN_LEFT, 0); /* Add input field or text widget with info about the program handler. */ if (!get_cmd_opt_bool("anonymous")) { char *field = (char *)mem_calloc(1, MAX_STR_LEN); if (!field) { mem_free(dlg); return; } if (handler && handler->program) { safe_strncpy(field, handler->program, MAX_STR_LEN); } /* xgettext:no-c-format */ add_dlg_field(dlg, _("Program ('%f' will be replaced by the filename, " "'%u' by the uri)", term), 0, 0, NULL, MAX_STR_LEN, field, NULL); type_query->external_handler = field; add_dlg_checkbox(dlg, _("Block the terminal", term), &type_query->block); selected_widget = 3; } else if (handler) { char *field = text + MAX_STR_LEN; format = _("The file will be opened with the program '%s'.", term); snprintf(field, MAX_STR_LEN, format, handler->program); add_dlg_text(dlg, field, ALIGN_LEFT, 0); type_query->external_handler = stracpy(handler->program); if (!type_query->external_handler) { mem_free(dlg); return; } widgets--; selected_widget = 2; } else { widgets -= 2; selected_widget = 1; } /* Add buttons if they are both usable and allowed. */ if (!get_cmd_opt_bool("anonymous") || handler) { add_dlg_ok_button(dlg, _("~Open", term), B_ENTER, (done_handler_T *) tp_open, type_query); } else { widgets--; } if (!get_cmd_opt_bool("anonymous")) { add_dlg_ok_button(dlg, _("Sa~ve", term), B_ENTER, (done_handler_T *) tp_save, type_query); } else { widgets--; } add_dlg_ok_button(dlg, _("~Display", term), B_ENTER, (done_handler_T *) tp_display, type_query); if (type_query->cached && type_query->cached->head) { add_dlg_button(dlg, _("Show ~header", term), B_ENTER, tp_show_header, type_query); } else { widgets--; } add_dlg_ok_button(dlg, _("~Cancel", term), B_ESC, (done_handler_T *) tp_cancel, type_query); add_dlg_end(dlg, widgets); ml = getml(dlg, (void *) NULL); if (!ml) { /* XXX: Assume that the allocated @external_handler will be * freed when releasing the @type_query. */ mem_free(dlg); return; } dlg_data = do_dialog(term, dlg, ml); /* Don't focus the text field; we want the user to be able * to select a button by typing the first letter of its label * without having to first leave the text field. */ if (dlg_data) { select_widget_by_id(dlg_data, selected_widget); } } struct { const char *type; unsigned int plain:1; } static const known_types[] = { { "text/html", 0 }, { "text/plain", 1 }, { "text/gemini", 0 }, { "application/xhtml+xml", 0 }, /* RFC 3236 */ #ifdef CONFIG_DOM { "application/docbook+xml", 1 }, { "application/rss+xml", 0 }, { "application/xbel+xml", 1 }, { "application/xbel", 1 }, { "application/x-xbel", 1 }, #endif { NULL, 1 }, }; /*! @relates type_query */ int setup_download_handler(struct session *ses, struct download *loading, struct cache_entry *cached, int frame) { struct mime_handler *handler; struct view_state *vs; struct type_query *type_query; char *ctype = get_content_type(cached); int plaintext = 1; int ret = 0; int xwin, i; if (!ctype || !*ctype) goto plaintext_follow; for (i = 0; known_types[i].type; i++) { if (c_strcasecmp(ctype, known_types[i].type)) continue; plaintext = known_types[i].plain; goto plaintext_follow; } xwin = ses->tab->term->environment & ENV_XWIN; handler = get_mime_type_handler(ctype, xwin); if (!handler && strlen(ctype) >= 4 && !c_strncasecmp(ctype, "text", 4)) goto plaintext_follow; type_query = find_type_query(ses); if (type_query) { ret = 1; } else { type_query = init_type_query(ses, loading, cached); if (type_query) { ret = 1; #ifdef CONFIG_BITTORRENT /* A terrible waste of a good MIME handler here, but we want * to use the type_query this is easier. */ if ((!c_strcasecmp(ctype, "application/x-bittorrent") || !c_strcasecmp(ctype, "application/x-torrent")) && !get_cmd_opt_bool("anonymous")) query_bittorrent_dialog(type_query); else #endif do_type_query(type_query, ctype, handler); } } mem_free_if(handler); return ret; plaintext_follow: vs = ses_forward(ses, frame); if (vs) vs->plain = plaintext; return 0; }