1
0
mirror of https://github.com/rkd77/elinks.git synced 2024-06-21 00:25:37 +00:00
elinks/src/session/download.c
2023-12-28 13:03:45 +01:00

1975 lines
52 KiB
C

/** Downloads managment
* @file */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_SYS_CYGWIN_H
#include <sys/cygwin.h>
#endif
#include <sys/types.h>
#ifdef HAVE_FCNTL_H
#include <fcntl.h> /* OS/2 needs this after sys/types.h */
#endif
#include <sys/stat.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <utime.h>
#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/hash.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);
static struct hash *uri_tempfiles;
void
clear_uri_tempfiles(void)
{
struct hash_item *item;
int i;
if (!uri_tempfiles) {
return;
}
foreach_hash_item (item, *uri_tempfiles, i) {
mem_free_set(&item->value, NULL);
mem_free_set(&item->key, NULL);
}
free_hash(&uri_tempfiles);
}
static char *
check_url_tempfiles(const char *url)
{
struct hash_item *item;
if (!uri_tempfiles || !url) {
return NULL;
}
item = get_hash_item(uri_tempfiles, url, strlen(url));
if (!item || !item->value) {
return NULL;
}
return stracpy(item->value);
}
static void
set_uri_tempfile(const char *url, const char *value)
{
if (!uri_tempfiles) {
uri_tempfiles = init_hash8();
}
if (uri_tempfiles) {
size_t len = strlen(url);
char *copy = memacpy(url, len);
if (copy) {
add_hash_item(uri_tempfiles, copy, len, stracpy(value));
}
}
}
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) {
if (file_download->dlg_data->dlg && file_download->dlg_data->dlg->refresh) {
kill_timer(&file_download->dlg_data->dlg->refresh->timer);
}
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)) {
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 {
if (get_opt_bool("ui.sessions.postpone_unlink", NULL)) {
char *url = get_uri_string(file_download->uri, URI_PUBLIC);
if (url) {
set_uri_tempfile(url, file_download->file);
mem_free(url);
}
}
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);
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;
} else { // Check in cache
char *url = get_uri_string(type_query->uri, URI_PUBLIC);
if (url) {
char *filename = check_url_tempfiles(url);
if (filename) {
if (file_can_read(filename)) {
char *handler = subst_file(type_query->external_handler, filename, filename);
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);
}
mem_free(filename);
done_type_query(type_query);
mem_free(url);
return;
}
mem_free(filename);
}
mem_free(url);
}
}
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;
}