/* * Copyright 2012 - 2013 Michael Drake * * This file is part of NetSurf, http://www.netsurf-browser.org/ * * NetSurf is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * NetSurf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "content/urldb.h" #include "desktop/global_history.h" #include "desktop/treeview.h" #include "utils/messages.h" #include "utils/utils.h" #include "utils/utf8.h" #include "utils/libdom.h" #include "utils/log.h" #define N_DAYS 28 #define N_SEC_PER_DAY (60 * 60 * 24) enum global_history_folders { GH_TODAY = 0, GH_YESTERDAY, GH_2_DAYS_AGO, GH_3_DAYS_AGO, GH_4_DAYS_AGO, GH_5_DAYS_AGO, GH_6_DAYS_AGO, GH_LAST_WEEK, GH_2_WEEKS_AGO, GH_3_WEEKS_AGO, GH_N_FOLDERS }; enum global_history_fields { GH_TITLE, GH_URL, GH_LAST_VISIT, GH_VISITS, GH_PERIOD, N_FIELDS }; struct global_history_folder { treeview_node *folder; struct treeview_field_data data; }; struct global_history_ctx { treeview *tree; struct treeview_field_desc fields[N_FIELDS]; struct global_history_folder folders[GH_N_FOLDERS]; time_t today; int weekday; bool built; }; struct global_history_ctx gh_ctx; struct global_history_entry { bool user_delete; int slot; nsurl *url; time_t t; treeview_node *entry; struct global_history_entry *next; struct global_history_entry *prev; struct treeview_field_data data[N_FIELDS - 1]; }; struct global_history_entry *gh_list[N_DAYS]; /** * Find an entry in the global history * * \param url The URL to find * \return Pointer to history entry, or NULL if not found */ static struct global_history_entry *global_history_find(nsurl *url) { int i; struct global_history_entry *e; for (i = 0; i < N_DAYS; i++) { e = gh_list[i]; while (e != NULL) { if (nsurl_compare(e->url, url, NSURL_COMPLETE) == true) { /* Got a match */ return e; } e = e->next; } } /* No match found */ return NULL; } /** * Initialise the treeview directories * * \param f Ident for folder to create * \return NSERROR_OK on success, appropriate error otherwise */ static nserror global_history_create_dir(enum global_history_folders f) { nserror err; time_t t = gh_ctx.today; treeview_node *relation = NULL; enum treeview_relationship rel = TREE_REL_FIRST_CHILD; const char *label; int age; int i; switch (f) { case GH_TODAY: label = "DateToday"; age = 0; break; case GH_YESTERDAY: label = "DateYesterday"; age = 1; break; case GH_2_DAYS_AGO: label = "Date2Days"; age = 2; break; case GH_3_DAYS_AGO: label = "Date3Days"; age = 3; break; case GH_4_DAYS_AGO: label = "Date4Days"; age = 4; break; case GH_5_DAYS_AGO: label = "Date5Days"; age = 5; break; case GH_6_DAYS_AGO: label = "Date6Days"; age = 6; break; case GH_LAST_WEEK: label = "Date1Week"; age = 7; break; case GH_2_WEEKS_AGO: label = "Date2Week"; age = 14; break; case GH_3_WEEKS_AGO: label = "Date3Week"; age = 21; break; default: assert(0); break; } t -= age * N_SEC_PER_DAY; label = messages_get(label); for (i = f - 1; i >= 0; i--) { if (gh_ctx.folders[i].folder != NULL) { relation = gh_ctx.folders[i].folder; rel = TREE_REL_NEXT_SIBLING; break; } } gh_ctx.folders[f].data.field = gh_ctx.fields[N_FIELDS - 1].field; gh_ctx.folders[f].data.value = label; gh_ctx.folders[f].data.value_len = strlen(label); err = treeview_create_node_folder(gh_ctx.tree, &gh_ctx.folders[f].folder, relation, rel, &gh_ctx.folders[f].data, &gh_ctx.folders[f], gh_ctx.built ? TREE_OPTION_NONE : TREE_OPTION_SUPPRESS_RESIZE | TREE_OPTION_SUPPRESS_REDRAW); return err; } /** * Get the treeview folder for history entires in a particular slot * * \param parent Updated to parent folder. * \param slot Global history slot of entry we want folder node for * \return NSERROR_OK on success, appropriate error otherwise */ static inline nserror global_history_get_parent_treeview_node( treeview_node **parent, int slot) { int folder_index; struct global_history_folder *f; nserror err; if (slot < 7) { folder_index = slot; } else if (slot < 14) { folder_index = GH_LAST_WEEK; } else if (slot < 21) { folder_index = GH_2_WEEKS_AGO; } else if (slot < N_DAYS) { folder_index = GH_3_WEEKS_AGO; } else { /* Slot value is invalid */ return NSERROR_BAD_PARAMETER; } /* Get the folder */ f = &(gh_ctx.folders[folder_index]); if (f->folder == NULL) { err = global_history_create_dir(folder_index); if (err != NSERROR_OK) { *parent = NULL; return err; } } /* Return the parent treeview folder */ *parent = f->folder; return NSERROR_OK; } /** * Set a global history entry's data from the url_data. * * \param e Global history entry to set up * \param url_data Data associated with entry's URL * \return NSERROR_OK on success, appropriate error otherwise */ static nserror global_history_create_treeview_field_data( struct global_history_entry *e, const struct url_data *data) { const char *title = (data->title != NULL) ? data->title : ""; char buffer[16]; const char *last_visited; char *last_visited2; int len; e->data[GH_TITLE].field = gh_ctx.fields[GH_TITLE].field; e->data[GH_TITLE].value = strdup(title); e->data[GH_TITLE].value_len = (e->data[GH_TITLE].value != NULL) ? strlen(title) : 0; e->data[GH_URL].field = gh_ctx.fields[GH_URL].field; e->data[GH_URL].value = nsurl_access(e->url); e->data[GH_URL].value_len = nsurl_length(e->url); last_visited = ctime(&data->last_visit); last_visited2 = strdup(last_visited); if (last_visited2 != NULL) { assert(last_visited2[24] == '\n'); last_visited2[24] = '\0'; } e->data[GH_LAST_VISIT].field = gh_ctx.fields[GH_LAST_VISIT].field; e->data[GH_LAST_VISIT].value = last_visited2; e->data[GH_LAST_VISIT].value_len = (last_visited2 != NULL) ? 24 : 0; len = snprintf(buffer, 16, "%u", data->visits); if (len == 16) { len--; buffer[len] = '\0'; } e->data[GH_VISITS].field = gh_ctx.fields[GH_VISITS].field; e->data[GH_VISITS].value = strdup(buffer); e->data[GH_VISITS].value_len = len; return NSERROR_OK; } /** * Add a global history entry to the treeview * * \param e entry to add to treeview * \param slot global history slot containing entry * \return NSERROR_OK on success, or appropriate error otherwise * * It is assumed that the entry is unique (for its URL) in the global * history table */ static nserror global_history_entry_insert(struct global_history_entry *e, int slot) { nserror err; treeview_node *parent; err = global_history_get_parent_treeview_node(&parent, slot); if (err != NSERROR_OK) { return err; } err = treeview_create_node_entry(gh_ctx.tree, &(e->entry), parent, TREE_REL_FIRST_CHILD, e->data, e, gh_ctx.built ? TREE_OPTION_NONE : TREE_OPTION_SUPPRESS_RESIZE | TREE_OPTION_SUPPRESS_REDRAW); if (err != NSERROR_OK) { return err; } return NSERROR_OK; } /** * Add an entry to the global history (creates the entry). * * If the treeview has already been created, the entry will be added to the * treeview. Otherwise, the entry will have to be added to the treeview later. * * When we first create the global history we create it without the treeview, to * simplfy sorting the entries. * * \param url URL for entry to add to history * \param slot Global history slot to contain history entry * \param data URL data for the entry * \param got_treeview Whether the treeview has been created already * \return NSERROR_OK on success, or appropriate error otherwise */ static nserror global_history_add_entry_internal(nsurl *url, int slot, const struct url_data *data, bool got_treeview) { nserror err; struct global_history_entry *e; /* Create new local history entry */ e = malloc(sizeof(struct global_history_entry)); if (e == NULL) { return NSERROR_NOMEM; } e->user_delete = false; e->slot = slot; e->url = nsurl_ref(url); e->t = data->last_visit; e->entry = NULL; e->next = NULL; e->prev = NULL; err = global_history_create_treeview_field_data(e, data); if (err != NSERROR_OK) { return err; } if (gh_list[slot] == NULL) { /* list empty */ gh_list[slot] = e; } else if (gh_list[slot]->t < e->t) { /* Insert at list head */ e->next = gh_list[slot]; gh_list[slot]->prev = e; gh_list[slot] = e; } else { struct global_history_entry *prev = gh_list[slot]; struct global_history_entry *curr = prev->next; while (curr != NULL) { if (curr->t < e->t) { break; } prev = curr; curr = curr->next; } /* insert after prev */ e->next = curr; e->prev = prev; prev->next = e; if (curr != NULL) curr->prev = e; } if (got_treeview) { err = global_history_entry_insert(e, slot); if (err != NSERROR_OK) { return err; } } return NSERROR_OK; } /** * Delete a global history entry * * This does not delete the treeview node, rather it should only be called from * the treeview node delete event message. * * \param e Entry to delete */ static void global_history_delete_entry_internal( struct global_history_entry *e) { assert(e != NULL); assert(e->entry == NULL); /* Unlink */ if (gh_list[e->slot] == e) { /* e is first entry */ gh_list[e->slot] = e->next; if (e->next != NULL) e->next->prev = NULL; } else if (e->next == NULL) { /* e is last entry */ e->prev->next = NULL; } else { /* e has an entry before and after */ e->prev->next = e->next; e->next->prev = e->prev; } if (e->user_delete) { /* User requested delete, so delete from urldb too. */ urldb_reset_url_visit_data(e->url); } /* Destroy fields */ free((void *)e->data[GH_TITLE].value); /* Eww */ free((void *)e->data[GH_LAST_VISIT].value); /* Eww */ free((void *)e->data[GH_VISITS].value); /* Eww */ nsurl_unref(e->url); /* Destroy entry */ free(e); } /** * Internal routine to actually perform global history addition * * \param url The URL to add * \param data URL data associated with URL * \return true (for urldb_iterate_entries) */ static bool global_history_add_entry(nsurl *url, const struct url_data *data) { int slot; time_t visit_date; time_t earliest_date = gh_ctx.today - (N_DAYS - 1) * N_SEC_PER_DAY; bool got_treeview = gh_ctx.tree != NULL; assert((url != NULL) && (data != NULL)); visit_date = data->last_visit; /* Find day array slot for entry */ if (visit_date >= gh_ctx.today) { slot = 0; } else if (visit_date >= earliest_date) { slot = (gh_ctx.today - visit_date) / N_SEC_PER_DAY + 1; } else { /* too old */ return true; } if (got_treeview == true) { /* The treeview for global history already exists */ struct global_history_entry *e; /* Delete any existing entry for this URL */ e = global_history_find(url); if (e != NULL) { treeview_delete_node(gh_ctx.tree, e->entry, TREE_OPTION_SUPPRESS_REDRAW | TREE_OPTION_SUPPRESS_RESIZE); } } if (global_history_add_entry_internal(url, slot, data, got_treeview) != NSERROR_OK) { return false; } return true; } /** * Initialise the treeview entry feilds * * \return NSERROR_OK on success, or appropriate error otherwise */ static nserror global_history_initialise_entry_fields(void) { int i; const char *label; for (i = 0; i < N_FIELDS; i++) gh_ctx.fields[i].field = NULL; gh_ctx.fields[GH_TITLE].flags = TREE_FLAG_DEFAULT; label = "TreeviewLabelTitle"; label = messages_get(label); if (lwc_intern_string(label, strlen(label), &gh_ctx.fields[GH_TITLE].field) != lwc_error_ok) { goto error; } gh_ctx.fields[GH_URL].flags = TREE_FLAG_NONE; label = "TreeviewLabelURL"; label = messages_get(label); if (lwc_intern_string(label, strlen(label), &gh_ctx.fields[GH_URL].field) != lwc_error_ok) { goto error; } gh_ctx.fields[GH_LAST_VISIT].flags = TREE_FLAG_SHOW_NAME; label = "TreeviewLabelLastVisit"; label = messages_get(label); if (lwc_intern_string(label, strlen(label), &gh_ctx.fields[GH_LAST_VISIT].field) != lwc_error_ok) { goto error; } gh_ctx.fields[GH_VISITS].flags = TREE_FLAG_SHOW_NAME; label = "TreeviewLabelVisits"; label = messages_get(label); if (lwc_intern_string(label, strlen(label), &gh_ctx.fields[GH_VISITS].field) != lwc_error_ok) { goto error; } gh_ctx.fields[GH_PERIOD].flags = TREE_FLAG_DEFAULT; label = "TreeviewLabelPeriod"; label = messages_get(label); if (lwc_intern_string(label, strlen(label), &gh_ctx.fields[GH_PERIOD].field) != lwc_error_ok) { return false; } return NSERROR_OK; error: for (i = 0; i < N_FIELDS; i++) if (gh_ctx.fields[i].field != NULL) lwc_string_unref(gh_ctx.fields[i].field); return NSERROR_UNKNOWN; } /** * Initialise the time * * \return NSERROR_OK on success, or appropriate error otherwise */ static nserror global_history_initialise_time(void) { struct tm *full_time; time_t t; /* get the current time */ t = time(NULL); if (t == -1) { LOG(("time info unaviable")); return NSERROR_UNKNOWN; } /* get the time at the start of today */ full_time = localtime(&t); full_time->tm_sec = 0; full_time->tm_min = 0; full_time->tm_hour = 0; t = mktime(full_time); if (t == -1) { LOG(("mktime failed")); return NSERROR_UNKNOWN; } gh_ctx.today = t; gh_ctx.weekday = full_time->tm_wday; return NSERROR_OK; } /** * Initialise the treeview entries * * \return NSERROR_OK on success, or appropriate error otherwise */ static nserror global_history_init_entries(void) { int i; nserror err; /* Itterate over all global history data, inserting it into treeview */ for (i = 0; i < N_DAYS; i++) { struct global_history_entry *l = NULL; struct global_history_entry *e = gh_list[i]; /* Insert in reverse order; find last */ while (e != NULL) { l = e; e = e->next; } /* Insert the entries into the treeview */ while (l != NULL) { err = global_history_entry_insert(l, i); if (err != NSERROR_OK) { return err; } l = l->prev; } } return NSERROR_OK; } static nserror global_history_tree_node_folder_cb( struct treeview_node_msg msg, void *data) { struct global_history_folder *f = data; switch (msg.msg) { case TREE_MSG_NODE_DELETE: f->folder = NULL; break; case TREE_MSG_NODE_EDIT: break; case TREE_MSG_NODE_LAUNCH: break; } return NSERROR_OK; } static nserror global_history_tree_node_entry_cb( struct treeview_node_msg msg, void *data) { struct global_history_entry *e = data; switch (msg.msg) { case TREE_MSG_NODE_DELETE: e->entry = NULL; e->user_delete = msg.data.delete.user; global_history_delete_entry_internal(e); break; case TREE_MSG_NODE_EDIT: break; case TREE_MSG_NODE_LAUNCH: { nserror error; struct browser_window *clone = NULL; enum browser_window_nav_flags flags = BROWSER_WINDOW_VERIFIABLE | BROWSER_WINDOW_HISTORY | BROWSER_WINDOW_TAB; /* TODO: Set clone window, to window that new tab appears in */ if (msg.data.node_launch.mouse & (BROWSER_MOUSE_MOD_1 | BROWSER_MOUSE_MOD_2) || clone == NULL) { /* Shift or Ctrl launch, open in new window rather * than tab. */ flags ^= BROWSER_WINDOW_TAB; } error = browser_window_create(flags, e->url, NULL, clone, NULL); if (error != NSERROR_OK) { warn_user(messages_get_errorcode(error), 0); } } break; } return NSERROR_OK; } struct treeview_callback_table gh_tree_cb_t = { .folder = global_history_tree_node_folder_cb, .entry = global_history_tree_node_entry_cb }; /* Exported interface, documented in global_history.h */ nserror global_history_init(struct core_window_callback_table *cw_t, void *core_window_handle) { nserror err; LOG(("Loading global history")); /* Init. global history treeview time */ err = global_history_initialise_time(); if (err != NSERROR_OK) { gh_ctx.tree = NULL; return err; } /* Init. global history treeview entry fields */ err = global_history_initialise_entry_fields(); if (err != NSERROR_OK) { gh_ctx.tree = NULL; return err; } /* Load the entries */ urldb_iterate_entries(global_history_add_entry); /* Create the global history treeview */ err = treeview_create(&gh_ctx.tree, &gh_tree_cb_t, N_FIELDS, gh_ctx.fields, cw_t, core_window_handle, TREEVIEW_NO_MOVES | TREEVIEW_DEL_EMPTY_DIRS); if (err != NSERROR_OK) { gh_ctx.tree = NULL; return err; } /* Ensure there is a folder for today */ err = global_history_create_dir(GH_TODAY); if (err != NSERROR_OK) { return err; } /* Add the history to the treeview */ err = global_history_init_entries(); if (err != NSERROR_OK) { return err; } /* History tree is built * We suppress the treeview height callback on entry insertion before * the treeview is built. */ gh_ctx.built = true; /* Expand the "Today" folder node */ err = treeview_node_expand(gh_ctx.tree, gh_ctx.folders[GH_TODAY].folder); if (err != NSERROR_OK) { return err; } LOG(("Loaded global history")); return NSERROR_OK; } /* Exported interface, documented in global_history.h */ nserror global_history_fini(void) { int i; nserror err; LOG(("Finalising global history")); gh_ctx.built = false; /* Destroy the global history treeview */ err = treeview_destroy(gh_ctx.tree); /* Free global history treeview entry fields */ for (i = 0; i < N_FIELDS; i++) if (gh_ctx.fields[i].field != NULL) lwc_string_unref(gh_ctx.fields[i].field); LOG(("Finalised global history")); return err; } /* Exported interface, documented in global_history.h */ nserror global_history_add(nsurl *url) { const struct url_data *data; /* If we don't have a global history at the moment, just return OK */ if (gh_ctx.tree == NULL) return NSERROR_OK; data = urldb_get_url_data(url); if (data == NULL) { LOG(("Can't add URL to history that's not present in urldb.")); return NSERROR_BAD_PARAMETER; } global_history_add_entry(url, data); return NSERROR_OK; } struct treeview_export_walk_ctx { FILE *fp; }; /** Callback for treeview_walk node entering */ static nserror global_history_export_enter_cb(void *ctx, void *node_data, enum treeview_node_type type, bool *abort) { struct treeview_export_walk_ctx *tw = ctx; if (type == TREE_NODE_ENTRY) { struct global_history_entry *e = node_data; utf8_convert_ret ret; char *t_text; char *u_text; ret = utf8_to_html(e->data[GH_TITLE].value, "iso-8859-1", e->data[GH_TITLE].value_len, &t_text); if (ret != UTF8_CONVERT_OK) return NSERROR_SAVE_FAILED; ret = utf8_to_html(e->data[GH_URL].value, "iso-8859-1", e->data[GH_URL].value_len, &u_text); if (ret != UTF8_CONVERT_OK) { free(t_text); return NSERROR_SAVE_FAILED; } fprintf(tw->fp, "
  • %s
  • \n", u_text, t_text); free(t_text); free(u_text); } else if (type == TREE_NODE_FOLDER) { struct global_history_folder *f = node_data; utf8_convert_ret ret; char *f_text; ret = utf8_to_html(f->data.value, "iso-8859-1", f->data.value_len, &f_text); if (ret != UTF8_CONVERT_OK) return NSERROR_SAVE_FAILED; fprintf(tw->fp, "
  • %s

    \n
      \n", f_text); free(f_text); } return NSERROR_OK; } /** Callback for treeview_walk node leaving */ static nserror global_history_export_leave_cb(void *ctx, void *node_data, enum treeview_node_type type, bool *abort) { struct treeview_export_walk_ctx *tw = ctx; if (type == TREE_NODE_FOLDER) { fputs("
  • \n", tw->fp); } return NSERROR_OK; } /* Exported interface, documented in global_history.h */ nserror global_history_export(const char *path, const char *title) { struct treeview_export_walk_ctx tw; nserror err; FILE *fp; fp = fopen(path, "w"); if (fp == NULL) return NSERROR_SAVE_FAILED; if (title == NULL) title = "NetSurf Browsing History"; fputs("\n", fp); fputs("\n\n", fp); fputs("\n", fp); fprintf(fp, "%s\n", title); fputs("\n\n
      \n", fp); tw.fp = fp; err = treeview_walk(gh_ctx.tree, NULL, global_history_export_enter_cb, global_history_export_leave_cb, &tw, TREE_NODE_ENTRY | TREE_NODE_FOLDER); if (err != NSERROR_OK) return err; fputs("
    \n\n\n", fp); fclose(fp); return NSERROR_OK; } /* Exported interface, documented in global_history.h */ void global_history_redraw(int x, int y, struct rect *clip, const struct redraw_context *ctx) { treeview_redraw(gh_ctx.tree, x, y, clip, ctx); } /* Exported interface, documented in global_history.h */ void global_history_mouse_action(browser_mouse_state mouse, int x, int y) { treeview_mouse_action(gh_ctx.tree, mouse, x, y); } /* Exported interface, documented in global_history.h */ void global_history_keypress(uint32_t key) { treeview_keypress(gh_ctx.tree, key); } /* Exported interface, documented in global_history.h */ bool global_history_has_selection(void) { return treeview_has_selection(gh_ctx.tree); } /* Exported interface, documented in global_history.h */ int global_history_get_height(void) { return treeview_get_height(gh_ctx.tree); }