1
0
mirror of https://github.com/rkd77/elinks.git synced 2024-12-04 14:46:47 -05:00
elinks/src/bookmarks/bookmarks.c
2022-03-03 15:25:45 +01:00

773 lines
18 KiB
C

/* Internal bookmarks support */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "elinks.h"
#include "bfu/dialog.h"
#include "bfu/hierbox.h"
#include "bfu/listbox.h"
#include "bookmarks/backend/common.h"
#include "bookmarks/bookmarks.h"
#include "bookmarks/dialogs.h"
#include "config/home.h"
#include "config/options.h"
#include "intl/libintl.h"
#include "main/module.h"
#include "main/object.h"
#include "protocol/uri.h"
#include "session/task.h"
#include "terminal/tab.h"
#include "util/conv.h"
#include "util/hash.h"
#include "util/lists.h"
#include "util/memory.h"
#include "util/secsave.h"
#include "util/string.h"
/* The list of bookmarks */
INIT_LIST_OF(struct bookmark, bookmarks);
/* Set to 1, if bookmarks have changed. */
static int bookmarks_dirty = 0;
static struct hash *bookmark_cache = NULL;
static struct bookmark *bm_snapshot_last_folder;
/* Life functions */
static union option_info bookmark_options_info[] = {
INIT_OPT_TREE("", N_("Bookmarks"),
"bookmarks", OPT_ZERO,
N_("Bookmark options.")),
#ifdef CONFIG_XBEL_BOOKMARKS
INIT_OPT_INT("bookmarks", N_("File format"),
"file_format", OPT_ZERO, 0, 1, 0,
N_("File format for bookmarks (affects both reading and "
"saving):\n"
"0 is the default native ELinks format\n"
"1 is XBEL universal XML bookmarks format")),
#else
INIT_OPT_INT("bookmarks", N_("File format"),
"file_format", OPT_ZERO, 0, 1, 0,
N_("File format for bookmarks (affects both reading and "
"saving):\n"
"0 is the default native ELinks format\n"
"1 is XBEL universal XML bookmarks format (DISABLED)")),
#endif
INIT_OPT_BOOL("bookmarks", N_("Save folder state"),
"folder_state", OPT_ZERO, 1,
N_("When saving bookmarks also store whether folders are "
"expanded or not, so the look of the bookmark dialog is "
"kept across ELinks sessions. If disabled all folders will "
"appear unexpanded next time ELinks is run.")),
INIT_OPT_BOOL("ui.sessions", N_("Periodic snapshotting"),
"snapshot", OPT_ZERO, 0,
N_("Automatically save a snapshot of all tabs periodically. "
"This will periodically bookmark the tabs of each terminal "
"in a separate folder for recovery after a crash.\n"
"\n"
"This feature requires bookmark support.")),
NULL_OPTION_INFO
};
static enum evhook_status bookmark_change_hook(va_list ap, void *data);
static enum evhook_status bookmark_write_hook(va_list ap, void *data);
struct event_hook_info bookmark_hooks[] = {
{ "bookmark-delete", 0, bookmark_change_hook, {NULL} },
{ "bookmark-move", 0, bookmark_change_hook, {NULL} },
{ "bookmark-update", 0, bookmark_change_hook, {NULL} },
{ "periodic-saving", 0, bookmark_write_hook, {NULL} },
NULL_EVENT_HOOK_INFO,
};
static enum evhook_status
bookmark_change_hook(va_list ap, void *data)
{
struct bookmark *bookmark = va_arg(ap, struct bookmark *);
if (bookmark == bm_snapshot_last_folder)
bm_snapshot_last_folder = NULL;
return EVENT_HOOK_STATUS_NEXT;
}
static void bookmark_snapshot();
static enum evhook_status
bookmark_write_hook(va_list ap, void *data)
{
if (get_opt_bool("ui.sessions.snapshot", NULL)
&& !get_cmd_opt_bool("anonymous"))
bookmark_snapshot();
write_bookmarks();
return EVENT_HOOK_STATUS_NEXT;
}
static int
change_hook_folder_state(struct session *ses, struct option *current,
struct option *changed)
{
if (!changed->value.number) {
/* We are to collapse all folders on exit; mark bookmarks dirty
* to ensure that this will happen. */
bookmarks_set_dirty();
}
return 0;
}
static void
init_bookmarks(struct module *module)
{
static const struct change_hook_info bookmarks_change_hooks[] = {
{ "bookmarks.folder_state", change_hook_folder_state },
{ NULL, NULL },
};
register_change_hooks(bookmarks_change_hooks);
read_bookmarks();
}
/* Clears the bookmark list */
static void
free_bookmarks(LIST_OF(struct bookmark) *bookmarks_list,
LIST_OF(struct listbox_item) *box_items)
{
struct bookmark *bm;
foreach (bm, *bookmarks_list) {
if (!list_empty(bm->child))
free_bookmarks(&bm->child, &bm->box_item->child);
mem_free(bm->title);
mem_free(bm->url);
}
free_list(*box_items);
free_list(*bookmarks_list);
if (bookmark_cache) free_hash(&bookmark_cache);
}
/* Does final cleanup and saving of bookmarks */
static void
done_bookmarks(struct module *module)
{
/* This is a clean shutdown, so delete the last snapshot. */
if (bm_snapshot_last_folder) delete_bookmark(bm_snapshot_last_folder);
bm_snapshot_last_folder = NULL;
write_bookmarks();
free_bookmarks(&bookmarks, &bookmark_browser.root.child);
free_last_searched_bookmark();
}
struct module bookmarks_module = struct_module(
/* name: */ N_("Bookmarks"),
/* options: */ bookmark_options_info,
/* hooks: */ bookmark_hooks,
/* submodules: */ NULL,
/* data: */ NULL,
/* init: */ init_bookmarks,
/* done: */ done_bookmarks
);
/* Read/write wrappers */
/* Loads the bookmarks from file */
void
read_bookmarks(void)
{
bookmarks_read();
}
void
write_bookmarks(void)
{
if (get_cmd_opt_bool("anonymous")) {
bookmarks_unset_dirty();
return;
}
bookmarks_write(&bookmarks);
}
/* Bookmarks manipulation */
void
bookmarks_set_dirty(void)
{
bookmarks_dirty = 1;
}
void
bookmarks_unset_dirty(void)
{
bookmarks_dirty = 0;
}
int
bookmarks_are_dirty(void)
{
return (bookmarks_dirty == 1);
}
#define check_bookmark_cache(url) (bookmark_cache && (url) && *(url))
static void
done_bookmark(struct bookmark *bm)
{
done_listbox_item(&bookmark_browser, bm->box_item);
mem_free(bm->title);
mem_free(bm->url);
mem_free(bm);
}
void
delete_bookmark(struct bookmark *bm)
{
static int delete_bookmark_event_id = EVENT_NONE;
while (!list_empty(bm->child)) {
delete_bookmark((struct bookmark *)bm->child.next);
}
if (check_bookmark_cache(bm->url)) {
struct hash_item *item;
item = get_hash_item(bookmark_cache, bm->url, strlen(bm->url));
if (item) del_hash_item(bookmark_cache, item);
}
set_event_id(delete_bookmark_event_id, "bookmark-delete");
trigger_event(delete_bookmark_event_id, bm);
del_from_list(bm);
bookmarks_set_dirty();
done_bookmark(bm);
}
/** Deletes any bookmarks with no URLs (i.e., folders) and of which
* the title matches the given argument.
*
* @param foldername
* The title of the folder, in UTF-8. */
static void
delete_folder_by_name(const char *foldername)
{
struct bookmark *bookmark, *next;
foreachsafe (bookmark, next, bookmarks) {
if ((bookmark->url && *bookmark->url)
|| strcmp(bookmark->title, foldername))
continue;
delete_bookmark(bookmark);
}
}
/** Allocate and initialize a bookmark in the given folder. This
* however does not set bookmark.box_item; use add_bookmark() for
* that.
*
* @param root
* The folder in which to add the bookmark, or NULL to add it at
* top level.
* @param title
* Title of the bookmark. Must be in UTF-8 and not NULL.
* "-" means add a separator.
* @param url
* URL to which the bookmark will point. Must be in UTF-8.
* NULL or "" means add a bookmark folder.
*
* @return the new bookmark, or NULL on error. */
static struct bookmark *
init_bookmark(struct bookmark *root, const char *title, const char *url)
{
struct bookmark *bm;
bm = (struct bookmark *)mem_calloc(1, sizeof(*bm));
if (!bm) return NULL;
bm->title = stracpy(title);
if (!bm->title) {
mem_free(bm);
return NULL;
}
sanitize_title(bm->title);
bm->url = stracpy(empty_string_or_(url));
if (!bm->url) {
mem_free(bm->title);
mem_free(bm);
return NULL;
}
sanitize_url(bm->url);
bm->root = root;
init_list(bm->child);
object_nolock(bm, "bookmark");
return bm;
}
static void
add_bookmark_item_to_bookmarks(struct bookmark *bm, struct bookmark *root, int place)
{
/* Actually add it */
if (place) {
if (root)
add_to_list_end(root->child, bm);
else
add_to_list_end(bookmarks, bm);
} else {
if (root)
add_to_list(root->child, bm);
else
add_to_list(bookmarks, bm);
}
bookmarks_set_dirty();
/* Hash creation if needed. */
if (!bookmark_cache)
bookmark_cache = init_hash8();
/* Create a new entry. */
if (check_bookmark_cache(bm->url))
add_hash_item(bookmark_cache, bm->url, strlen(bm->url), bm);
}
/** Add a bookmark to the bookmark list.
*
* @param root
* The folder in which to add the bookmark, or NULL to add it at
* top level.
* @param place
* 0 means add to the top. 1 means add to the bottom.
* @param title
* Title of the bookmark. Must be in UTF-8 and not NULL.
* "-" means add a separator.
* @param url
* URL to which the bookmark will point. Must be in UTF-8.
* NULL or "" means add a bookmark folder.
*
* @return the new bookmark, or NULL on error.
*
* @see add_bookmark_cp() */
struct bookmark *
add_bookmark(struct bookmark *root, int place, const char *title,
const char *url)
{
enum listbox_item_type type;
struct bookmark *bm;
bm = init_bookmark(root, title, url);
if (!bm) return NULL;
if (url && *url) {
type = BI_LEAF;
} else if (title && title[0] == '-' && title[1] == '\0') {
type = BI_SEPARATOR;
} else {
type = BI_FOLDER;
}
bm->box_item = add_listbox_item(&bookmark_browser,
root ? root->box_item : NULL,
type,
(void *) bm,
place ? -1 : 1);
if (!bm->box_item) {
mem_free(bm->url);
mem_free(bm->title);
mem_free(bm);
return NULL;
}
add_bookmark_item_to_bookmarks(bm, root, place);
return bm;
}
/** Add a bookmark to the bookmark list.
*
* @param root
* The folder in which to add the bookmark, or NULL to add it at
* top level.
* @param place
* 0 means add to the top. 1 means add to the bottom.
* @param codepage
* Codepage of @a title and @a url.
* @param title
* Title of the bookmark. Must not be NULL.
* "-" means add a separator.
* @param url
* URL to which the bookmark will point.
* NULL or "" means add a bookmark folder.
*
* @return the new bookmark.
*
* @see add_bookmark() */
struct bookmark *
add_bookmark_cp(struct bookmark *root, int place, int codepage,
const char *title, const char *url)
{
const int utf8_cp = get_cp_index("UTF-8");
struct conv_table *table;
char *utf8_title = NULL;
char *utf8_url = NULL;
struct bookmark *bookmark = NULL;
if (!url)
url = "";
table = get_translation_table(codepage, utf8_cp);
if (!table)
return NULL;
utf8_title = convert_string(table, title, strlen(title),
utf8_cp, CSM_NONE,
NULL, NULL, NULL);
utf8_url = convert_string(table, url, strlen(url),
utf8_cp, CSM_NONE,
NULL, NULL, NULL);
if (utf8_title && utf8_url)
bookmark = add_bookmark(root, place,
utf8_title, utf8_url);
mem_free_if(utf8_title);
mem_free_if(utf8_url);
return bookmark;
}
/* Updates an existing bookmark.
*
* If there's any problem, return 0. Otherwise, return 1.
*
* If any of the fields are NULL, the value is left unchanged. */
int
update_bookmark(struct bookmark *bm, int codepage,
char *title, char *url)
{
static int update_bookmark_event_id = EVENT_NONE;
const int utf8_cp = get_cp_index("UTF-8");
struct conv_table *table;
char *title2 = NULL;
char *url2 = NULL;
table = get_translation_table(codepage, utf8_cp);
if (!table)
return 0;
if (url) {
url2 = convert_string(table, url, strlen(url),
utf8_cp, CSM_NONE,
NULL, NULL, NULL);
if (!url2) return 0;
sanitize_url(url2);
}
if (title) {
title2 = convert_string(table, title, strlen(title),
utf8_cp, CSM_NONE,
NULL, NULL, NULL);
if (!title2) {
mem_free_if(url2);
return 0;
}
sanitize_title(title2);
}
set_event_id(update_bookmark_event_id, "bookmark-update");
trigger_event(update_bookmark_event_id, bm, title2, url2);
if (title2) {
mem_free_set(&bm->title, title2);
}
if (url2) {
if (check_bookmark_cache(bm->url)) {
struct hash_item *item;
int len = strlen(bm->url);
item = get_hash_item(bookmark_cache, bm->url, len);
if (item) del_hash_item(bookmark_cache, item);
}
if (check_bookmark_cache(url2)) {
add_hash_item(bookmark_cache, url2, strlen(url2), bm);
}
mem_free_set(&bm->url, url2);
}
bookmarks_set_dirty();
return 1;
}
/** Search for a bookmark with the given title. The search does not
* recurse into subfolders.
*
* @param folder
* Search in this folder. NULL means search in the root.
*
* @param title
* Search for this title. Must be in UTF-8 and not NULL.
*
* @return The bookmark, or NULL if not found. */
struct bookmark *
get_bookmark_by_name(struct bookmark *folder, char *title)
{
struct bookmark *bookmark;
LIST_OF(struct bookmark) *lh;
lh = folder ? &folder->child : &bookmarks;
foreach (bookmark, *lh)
if (!strcmp(bookmark->title, title)) return bookmark;
return NULL;
}
/* Search bookmark cache for item matching url. */
struct bookmark *
get_bookmark(char *url)
{
struct hash_item *item;
/** @todo Bug 1066: URLs in bookmark_cache should be UTF-8 */
if (!check_bookmark_cache(url))
return NULL;
/* Search for cached entry. */
item = get_hash_item(bookmark_cache, url, strlen(url));
return (struct bookmark *)(item ? item->value : NULL);
}
static void
bookmark_terminal(struct terminal *term, struct bookmark *folder)
{
char title[MAX_STR_LEN], url[MAX_STR_LEN];
struct window *tab;
int term_cp = get_terminal_codepage(term);
foreachback_tab (tab, term->windows) {
struct session *ses = (struct session *)tab->data;
if (!get_current_url(ses, url, MAX_STR_LEN))
continue;
if (!get_current_title(ses, title, MAX_STR_LEN))
continue;
add_bookmark_cp(folder, 1, term_cp, title, url);
}
}
/** Create a bookmark for each document on the specified terminal,
* and a folder to contain those bookmarks.
*
* @param term
* The terminal whose open documents should be bookmarked.
*
* @param foldername
* The name of the new bookmark folder, in UTF-8. */
void
bookmark_terminal_tabs(struct terminal *term, char *foldername)
{
struct bookmark *folder = add_bookmark(NULL, 1, foldername, NULL);
if (!folder) return;
bookmark_terminal(term, folder);
}
static void
bookmark_all_terminals(struct bookmark *folder)
{
unsigned int n = 0;
struct terminal *term;
if (get_cmd_opt_bool("anonymous"))
return;
if (list_is_singleton(terminals)) {
bookmark_terminal((struct terminal *)terminals.next, folder);
return;
}
foreach (term, terminals) {
char subfoldername[5];
struct bookmark *subfolder;
if (ulongcat(subfoldername, NULL, n, sizeof(subfoldername)-1, 0)
>= sizeof(subfoldername)-1)
return;
++n;
/* Because subfoldername[] contains only digits,
* it is OK as UTF-8. */
subfolder = add_bookmark(folder, 1, subfoldername, NULL);
if (!subfolder) return;
bookmark_terminal(term, subfolder);
}
}
char *
get_auto_save_bookmark_foldername_utf8(void)
{
char *foldername;
int from_cp, to_cp;
struct conv_table *convert_table;
foldername = get_opt_str("ui.sessions.auto_save_foldername", NULL);
if (!*foldername) return NULL;
/* The charset of the string returned by get_opt_str()
* seems to be documented nowhere. Let's assume it is
* the system charset. */
from_cp = get_cp_index("System");
to_cp = get_cp_index("UTF-8");
convert_table = get_translation_table(from_cp, to_cp);
if (!convert_table) return NULL;
return convert_string(convert_table,
foldername, strlen(foldername),
to_cp, CSM_NONE,
NULL, NULL, NULL);
}
void
bookmark_auto_save_tabs(struct terminal *term)
{
char *foldername; /* UTF-8 */
if (get_cmd_opt_bool("anonymous")
|| !get_opt_bool("ui.sessions.auto_save", NULL))
return;
foldername = get_auto_save_bookmark_foldername_utf8();
if (!foldername) return;
/* Ensure uniqueness of the auto save folder, so it is possible to
* restore the (correct) session when starting up. */
delete_folder_by_name(foldername);
bookmark_terminal_tabs(term, foldername);
mem_free(foldername);
}
static void
bookmark_snapshot(void)
{
struct string folderstring;
struct bookmark *folder;
if (!init_string(&folderstring)) return;
add_to_string(&folderstring, "Session snapshot");
#ifdef HAVE_STRFTIME
add_to_string(&folderstring, " - ");
add_date_to_string(&folderstring, get_opt_str("ui.date_format", NULL),
NULL);
#endif
/* folderstring must be in the system codepage because
* add_date_to_string() uses strftime(). */
folder = add_bookmark_cp(NULL, 1, get_cp_index("System"),
folderstring.source, NULL);
done_string(&folderstring);
if (!folder) return;
bookmark_all_terminals(folder);
if (bm_snapshot_last_folder) delete_bookmark(bm_snapshot_last_folder);
bm_snapshot_last_folder = folder;
}
/** Open all bookmarks from the named folder.
*
* @param ses
* The session in which to open the first bookmark. The other
* bookmarks of the folder open in new tabs on the same terminal.
*
* @param foldername
* The name of the bookmark folder, in UTF-8. */
void
open_bookmark_folder(struct session *ses, char *foldername)
{
struct bookmark *bookmark;
struct bookmark *folder = NULL;
struct bookmark *current = NULL;
assert(foldername && ses);
if_assert_failed return;
foreach (bookmark, bookmarks) {
if (bookmark->box_item->type != BI_FOLDER)
continue;
if (strcmp(bookmark->title, foldername))
continue;
folder = bookmark;
break;
}
if (!folder) return;
foreach (bookmark, folder->child) {
struct uri *uri;
if (bookmark->box_item->type == BI_FOLDER
|| bookmark->box_item->type == BI_SEPARATOR
|| !*bookmark->url)
continue;
/** @todo Bug 1066: Tell the URI layer that
* bookmark->url is UTF-8. */
uri = get_translated_uri(bookmark->url, NULL);
if (!uri) continue;
/* Save the first bookmark for the current tab */
if (!current) {
current = bookmark;
goto_uri(ses, uri);
} else {
open_uri_in_new_tab(ses, uri, 1, 0);
}
done_uri(uri);
}
}