/*****************************************************************************\
*                                                                             *
* 			       browser-history                               *
* 		      logs all URLs acceded by browser                       *
*                                                                             *
\*****************************************************************************/
/* Copyright Colas Nahaboo 1996, http://www.inria.fr/koala/colas. 
 * See http://www.inria.fr/koala/colas/browser-history
 */
char *RCS_ID = "$Id: browser-history.c,v 2.8 2000/07/25 15:46:26 colas Exp $";
static char VERSION[8];
/* Compile with:
   gcc -ansi -Wall browser-history.c -o browser-history -lXmu -lX11
*/

#define _POSIX_SOURCE			/* for solaris (sigaction) */
#define __USE_FIXED_PROTOTYPES__	/* for sunos (stdio.h) */

#include <stdio.h>
#include <ctype.h>
#include <X11/Xos.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <time.h>
#include <string.h>
#include <signal.h>
#include <sys/stat.h>

#ifdef NO_STRSTR
char *strstr();
#endif

/********************************************************************* usage */
#define E stderr
#define P fprintf

void
usage()
{
    extern char *homedir, *logreldir;
    P(E, " browser-history v%s\n", VERSION);
    P(E, "    See full docs at the URL:  http://www.inria.fr/koala/colas/browser-history\n");
    P(E, "    USAGE: browser-history [options]\n");

    P(E, "    Logs in ~/%s/history-log.html all the URLs you went through\n",
      logreldir);
    P(E, "    You can then browse the log under Netscape or other browsers via the URL:\n");
    P(E, "    file:%s/%s/history-log.html\n", homedir, logreldir);
    P(E, "    Tracks automagically all already present browser windows, and all new\n");
    P(E, "    ones created in the future.\n");
    P(E, "    This version works with Netscape and Arena.\n");
    P(E, "    URLs can be excluded from logging by putting them, one per line\n");
    P(E, "    in the file ~/%s/history-log.exclude\n", logreldir);
    P(E, "    then, if an URL begins with a line from this file, it is not logged.\n");
    P(E, "    In this file, empty lines or lines beginning by # are comments\n");
    P(E, "    This file is read once at startup, and re-read when receiving the signal 1\n");

    P(E, " Options:\n");
    P(E, "    -display display_name\n");
    P(E, "         Specifies X display, otherwise contents of $DISPLAY is used\n");
    P(E, "    -verbose\n");
    P(E, "         outputs information on what it is doing. useful for debug.\n"); 
    P(E, "    -Version\n"); 
    P(E, "         prints version number and exit.\n"); 
    P(E, "    -logdir directory\n"); 
    P(E, "         which directory to store files into? defaults to ~/.browser-history\n"); 
    P(E, "    -gzip gzip_filename\n"); 
    P(E, "         the complete path to the gzip compressor. Defaults to gzip.\n"); 
    P(E, "         E.g: -gzip /usr/gnu/bin/gzip\n"); 
    P(E, "    -seconds delay\n"); 
    P(E, "         if two entries are made are more than delay seconds apart, an\n");
    P(E, "         horizontal rule will separate them, else just a simple line break.\n"); 
    P(E, "         Defaults to one hour (3600).\n"); 
    P(E, "    -replace\n"); 
    P(E, "         If there is an already running browser-history on the display, kills\n");
    P(E, "         it and take its place. Default is to replace it only if the version\n"); 
    P(E, "         is older than ours.\n"); 
    P(E, "    -noreplace\n"); 
    P(E, "         If there is an already running browser-history on the display, aborts.\n");
    P(E, "         Default is to replace it only if the version is older than ours.\n"); 
    P(E, "    -kill\n"); 
    P(E, "         If there is an already running browser-history on the display, kills\n");
    P(E, "         it, then terminates immediately in all cases.\n"); 
    P(E, "    -DontGrab\n"); 
    P(E, "         Never Grab the X Server, which might cause deadlocks while debugging,\n");
    P(E, "         when browser-history or gdb tries to print on the\n"); 
    P(E, "         grabbed xterm or emacs.\n"); 
    exit(1);
}

/********************************************************************* types */

typedef struct _BrowserDesc {
    char *name;
    int ID;
    char *xname;
    char *xclass;
} *BrowserDesc;

struct _BrowserDesc browsers[] = {
#define BT_NONE 0
    {"none", BT_NONE, "", ""},
#define BT_NETSCAPE 1
    {"netscape", BT_NETSCAPE, "Navigator", "Netscape"},
#define BT_ARENA 2
    {"arena", BT_ARENA, "Browser", "Arena"},
#define BT_AMAYA 3
    {"amaya", BT_AMAYA, "WWW  WYSIWYG_view", "Dialogue"},
#define BT_GENERIC 4
    {"generic", BT_GENERIC, ".", "."}
};
int browsers_size = 4;
	    
typedef struct _Browser_win {
    Window win;
    int browser_type;
    char *url;
    char *name;
    int url_changed;
} *Browser_win;

/******************************************************************* globals */

char *logreldir = ".browser-history";	/* relative to homedir */
char *logfilename = "history-log.html";
char *logfilebase = "history-log";
char *headerfilename = "header.html";
char *logfilepath;			/* expansed ~/logfilename */
char *headerfilepath;			/* expansed ~/headerfilename */
char *excludefilename = "history-log.exclude";
char *excludefilepath;			/* expansed ~/excludefilename */
char *gzippath = "gzip";
char *nilstring = "";			/* returned by get_prop on failure */
char *homedir;				/* ~ expansed */
long delay = 3600L;			/* if more seconds, <hr>  */
int tag_mode = 0;			/* if another browser-history exists:
					 -1 aborts, 1 kills it, 0 kills unless
					 we are an older version
					 2 kills and exit */
int dontgrab = 0;			/* for debug */

char *displayname = 0;			/* the display name */
Display *dpy;				/* the display */
int verbose = 0;			/* verbose mode? */
int must_reinit = 0;			/* do we need to re-read init files? */

Browser_win browser_windows;		/* the list of existing N wins */
int browser_windows_size = 0;		/* its size */
Window tag_window;			/* ID of tag window */
int version_num;			/* major*1000 + minor */

struct tm last_times;			/* time of last entry in log file */
time_t last_time;			/* same in seconds */

char **excluded_URLs;			/* not logged URLs, array of strings */
int *excluded_URL_lengths;		/* array of the sizes of these strings */
int excluded_URLs_size = 0;		/* size of the array itself */

Atom WM_NAME;
Atom _MOZILLA_URL;
Atom ARENA_LOCATION;
Atom BROWSER_HISTORY_INFO;
Atom BROWSER_HISTORY;
Atom LAST_URL;
Atom LAST_POS;
Atom PROCESS;

char *dayname[] = {
    "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
    "Saturday"};
char *monthname[] = {
    "January", "February", "March", "April", "May", "June", "July", 
    "August", "September", "October", "November", "December"};

/******************************************************************* imports */
extern char *malloc(int bytes);		/* WARNING: Dont use these! */
extern char *realloc(void *ptr, int bytes);
extern void free(void *ptr);

extern Window XmuClientWindow (Display *dpy, Window win);
extern char *getenv(const char *);
extern char *strdup(const char *);
#define streq !strcmp
/* extern char *strchr(const char *s, int c); */
extern int atoi(char *s);
extern int gethostname(char *name, size_t len);
extern int system(char *command);

/********************************************************** forward declares */
void initialize(int argc, char **argv);
void detect_all_existing_browser_windows();
void mainloop();
void add_new_browser_window(Window win, int browser);
void remove_browser_window(Window win);
char *get_prop(Window win, Atom name, int offset);
void add_URL(int number);
void xfree(void *ptr, int offset);
char *concat_paths(char *path, char *name1, char *name2);
int not_excluded_URL(char *url);
void parse_excluded_URLs();
int index_of_win(Window win);
int URL_already_present(char *name, char *url);
void record_URL(int number, char *name, char *url);
void reinit_handler();
int verboseXHandler(Display *dpy, XErrorEvent *error);
int terseXHandler(Display *dpy, XErrorEvent *error);
int is_browser_window(Window win);
int decode_ARENA_LOCATION(char *full_name, char **namep, char **urlp);
void init_log_file(int first, FILE *fd);
long add_log_entry(char *name, char *url, unsigned int win,
		   char *datestring, struct tm *times, int separator);
void reinit();
Window create_tag_window(int mode, int version, char *machine, int pid);
char *date2string(struct tm *times);
void update_tag_window(Window w, char *name, char *url, unsigned int win,
		       char *datestring, int separator, long filepos);
void init_excludefile();
void check_daily_cleanup();
void init_last_entry();
FILE *add_log_day(FILE *fd, struct tm *times);

/* Use these to prevent abortion in case of memory full situations */
char *Malloc(int bytes);		
char *Realloc(void *ptr, int bytes);
char *Strdup(char *);
#define Free(x) free(x)			/* for coherence */

#ifdef BSD
extern int sv_sprintfd(char *buffer, char *format, int arg);
extern int sv_sprintfs(char *buffer, char *format, char *arg);
#else /* !BSD */
#define sv_sprintfs sprintf
#define sv_sprintfd sprintf
#endif /* !BSD */

/*****************************************************************************\
************************************ body *************************************
\*****************************************************************************/

int
main(int argc, char**argv)
{
    initialize(argc, argv);
    detect_all_existing_browser_windows();
    mainloop();
    return 0;
}

void
mainloop()
{
    int n;
    XEvent event;

    for (;;) {
	XNextEvent(dpy, &event);
	if (must_reinit)		/* signal 1 was sent */
	    reinit();
	switch (event.type) {

	case PropertyNotify:		/* prop change on tracked window */
	    if (-1 == (n = index_of_win(event.xproperty.window)))
		break;			/* window not tracked */
	    if (browser_windows[n].browser_type == BT_NETSCAPE) {
		/* Netscape changes first _MOZILLA_URL, then WM_NAME */
		if (event.xproperty.atom == WM_NAME) {
		    if (browser_windows[n].url_changed) {	/* ok */
			add_URL(n);
			browser_windows[n].url_changed = 0;
		    } 
		    /* else ignore, name changed but not due to URL visit */
		} else if (event.xproperty.atom == _MOZILLA_URL) {
		    browser_windows[n].url_changed = 1;	/* wait for the name */
		}
	    } else if (browser_windows[n].browser_type == BT_ARENA) {
		if (event.xproperty.atom == ARENA_LOCATION)
		    add_URL(n);
	    } else if (browser_windows[n].browser_type == BT_AMAYA) {
		if (event.xproperty.atom == BROWSER_HISTORY_INFO)
		    add_URL(n);
	    } else if (browser_windows[n].browser_type == BT_GENERIC) {
		if (event.xproperty.atom == BROWSER_HISTORY_INFO)
		    add_URL(n);
	    }
	    break;

	case CreateNotify:		/* new window created. track it? */
	{				/* is it a browser window? */
	    Window client = event.xcreatewindow.window;
	    if ((n = is_browser_window(client)))
		add_new_browser_window(client, n);
	}
	break;

	case DestroyNotify:		/* a tracked window disappear */
	    if (event.xdestroywindow.window == tag_window) {
		exit(0);
	    } else {
		remove_browser_window(event.xdestroywindow.window);
	    }
	    break;
	}
    }
}

/*****************************************************************************\
* 			       initialisations                                *
\*****************************************************************************/

void
initialize(int argc, char**argv)
{
    int i;
    char hostname[256];

    /* expands homedir first (for usage) */
    if (!(homedir = getenv("HOME"))) homedir = ".";

    /* version number, extracted from RCS Id string */
    {
	char *p = RCS_ID + 25, *q = p;
	while (*p && *p != ' ') p++;
	strncpy(VERSION, q, p - q);
	version_num = atoi(VERSION) * 1000 + 
	    atoi(strchr(VERSION, '.') + 1);
    }    

    /* parse options, only first letter of option is significant */
    for (i = 1; i < argc; i++) {
	char *arg = argv[i];

	if (arg[0] == '-') {
	    switch (arg[1]) {
	    case 'd':			/* -display dpyname */
		if (++i >= argc) usage ();
		displayname = argv[i];
		continue;
	    case 'v':			/* -verbose */
		verbose++;
		continue;
	    case 'V':			/* -Version */
		fprintf(stderr, "browser-history v%s\n", VERSION);
		exit(0);
		continue;
	    case 'l':			/* -logdir [.browser-history] */
		if (++i >= argc) usage ();
		logreldir = argv[i];
		continue;
	    case 's':			/* -seconds delay */
		if (++i >= argc) usage ();
		delay = (long) atoi(argv[i]);
		if (delay <=0) delay = 3600L;
		continue;
	    case 'r':			/* -replace */
		tag_mode = 1;
		continue;
	    case 'n':			/* -noreplace */
		tag_mode = -1;
		continue;
	    case 'k':			/* -kill */
		tag_mode = 2;
		continue;
	    case 'g':			/* -gzip path */
		if (++i >= argc) usage ();
		gzippath = argv[i];
		continue;
	    case 'D':			/* -DontGrab */
		dontgrab = 1;
		continue;
	    }
	}
	usage();			/* default: unknown option */
    }

    /* expands file names wrt homedir (may have been changed by options) */
    logfilepath = concat_paths(homedir, logreldir, logfilename);
    headerfilepath = concat_paths(homedir, logreldir, headerfilename);
    excludefilepath = concat_paths(homedir, logreldir, excludefilename);
    init_excludefile();

    /* init arrays */
    browser_windows = (Browser_win) Malloc(sizeof(struct _Browser_win) * 1);
    parse_excluded_URLs();

    {   /* trap signal 1, make it re-init from files */
	struct sigaction action;
	sigaction(SIGHUP, 0, &action);
#ifdef SA_RESTART
	action.sa_flags |= SA_RESTART;
	/* else if no SA_RESTART (SUNs) suppose the default behavior is Ok... */
#endif
	action.sa_handler = reinit_handler;
	sigaction(SIGHUP, &action, 0);
    }

    /* init X */
    dpy = XOpenDisplay (displayname);
    if (!dpy) {
	fprintf (stderr, "%s:  unable to open display \"%s\"\r\n",
		 argv[0], XDisplayName (displayname));
	exit (1);
    }
    WM_NAME = XInternAtom(dpy, "WM_NAME", False);
    _MOZILLA_URL = XInternAtom(dpy, "_MOZILLA_URL", False);
    ARENA_LOCATION = XInternAtom(dpy, "ARENA_LOCATION", False);
    BROWSER_HISTORY = XInternAtom(dpy, "BROWSER_HISTORY", False);
    BROWSER_HISTORY_INFO = XInternAtom(dpy, "BROWSER_HISTORY_INFO", False);
    LAST_URL = XInternAtom(dpy, "LAST_URL", False);
    LAST_POS = XInternAtom(dpy, "LAST_POS", False);
    PROCESS = XInternAtom(dpy, "PROCESS", False);

    /* dont bother users with errors. this is a deamon */
    XSetErrorHandler(verbose ? verboseXHandler : terseXHandler);

    /* create tag window, or abort if previous one runs */
    gethostname(hostname, 256);
    tag_window = create_tag_window(tag_mode, version_num, hostname, getpid());
    
    /* tell X we want to track window creation & destruction */
    XSelectInput(dpy, DefaultRootWindow(dpy), SubstructureNotifyMask);

    init_last_entry();			/* look what was the last entry */
}

/*****************************************************************************\
* 			    excluded URLs handling                            *
\*****************************************************************************/

void 
parse_excluded_URLs()
{					/* excluded URLs file parsing */
    FILE *fd = fopen(excludefilepath, "r");
    char lineraw[4000], *line, *end;
    excluded_URLs = (char **) Malloc(1);
    excluded_URL_lengths = (int *) Malloc(1);
    if (fd) {
	if (verbose)
	    P(E, "reading %s\n", excludefilepath);
	while (fgets(lineraw, 4000, fd)) {
	    /* trim spaces ate beginning and end */
	    for (line = lineraw; *line && isspace((int) *line); line++) ;
	    for (end = line + strlen(line); end > line && isspace((int) *(end-1));
		 end--) ;
	    *end = '\0';
	    if (line[0] != '\0' && line[0] != '#') {
		int len = end - line;
		    
		excluded_URLs = (char **) 
		    Realloc(excluded_URLs, 
			    (excluded_URLs_size+1) * sizeof(char *));
		excluded_URLs[excluded_URLs_size] = Strdup(line);
		excluded_URL_lengths = (int *) 
		    Realloc(excluded_URL_lengths, 
			    (excluded_URLs_size+1) * sizeof(int));
		excluded_URL_lengths[excluded_URLs_size] = len;
		excluded_URLs_size++;
	    }
	}
	fclose(fd);
    }
}

void
reinit()
{
    /* clears excluded_URLs, then reloads it */
    int i;
    for (i = 0; i < excluded_URLs_size; i++ )
	Free(excluded_URLs[i]);
    Free(excluded_URLs);
    Free(excluded_URL_lengths);
    excluded_URLs_size = 0;
    parse_excluded_URLs();
    must_reinit = 0;
}

int
not_excluded_URL(char *url)
{
    int i;
    for (i = 0; i < excluded_URLs_size; i++) {
	if (!strncmp(excluded_URLs[i], url, excluded_URL_lengths[i]))
	    return 0;
    }
    return 1;
}

void
init_excludefile()
{
    FILE *fd = fopen(excludefilepath, "a");
    if (!fd) {			/* cannot open? try to make dir */
	char logdir[1024];
	sprintf(logdir, "%s/%s", homedir, logreldir);
	mkdir(logdir, 0777);
	fd = fopen(excludefilepath, "a"); /* retry now */
    }
    if (fd && ftell(fd) == 0) {		/* exclude file is empty, fill it */
	if (verbose)
	    P(E, "initialising a new %s\n", excludefilepath);
	P(fd, "# Put here the start of URLs you want to exclude from the log\n");
	P(fd, "# by browser-history\n\n");
	P(fd, "# First, we exclude file accesses:\n");
	P(fd, "file:\n\n");
	P(fd, "# Then, we exclude common search engines...\n");
	P(fd, "http://home.netscape.com\n");
	P(fd, "http://guide.infoseek.com\n");
	P(fd, "http://altavista.digital.com\n");	
    }
    if (fd) {				/* cannot do anything */
	fclose(fd);
    }
}

/*****************************************************************************\
* 			     log file formatting                              *
\*****************************************************************************/

void
init_log_file(int first, FILE *fd)
{
    int i;
    FILE *fh = fopen(headerfilepath, "r");
    if (!fh) {				/* default header */
	fprintf(fd, "<head><title>Global history by browser-history</title></head>\n");
	fprintf(fd, "<h1>Global history by browser-history</h1>\n");
	fprintf(fd, "For more info, see the <a \n");
	fprintf(fd, "href=\"http://www.inria.fr/koala/colas/browser-history\">\n");
	fprintf(fd, "browser-history home page</a>\n");
	fprintf(fd, "<h1>This week</h1>\n");
	fprintf(fd, "See at end of file, or jump directly to:\n<ul>\n");
	for(i = 0; i < 7; i++) {
	    fprintf(fd, "  <li><a href=\"#%s\">%s</a>\n", dayname[i], dayname[i]);
	}
	fprintf(fd, "</ul>\n");
	fprintf(fd, "<h1>Past weeks</h1>\n"); /* end phrase default headers */
    } else {				/* user-provided header */
	char buffer[255];
	int nc, inc;
	/* copy header in log_file */
	while ((nc = fread(buffer, 1, 255, fh)) != 0) {
	    inc = fwrite(buffer, 1, nc, fd);
	    if (inc != nc) {
		P(E, "Warning: Invalid write (%i != %i) in %s!\n", 
		  inc, nc, logfilepath);
	    }
	}
	if (!feof(fh)) {
	    P(E, "Warning: Invalid read in %s!\n", headerfilepath);
	}
	if (fclose(fh) == EOF) {
	    P(E, "Warning: Can't close %s!\n", headerfilepath);
	}
    }
    if (first) {
        fprintf(fd, "<p>None available. This is the first week of usage</p>\n");
    }
    fprintf(fd, "<!-- EndOfHeader -->\n"); /* only present since v2.6 */
    fflush(fd);
}

long					/* current filepos in file, or -1 */
add_log_entry(char *name, char *url, unsigned int win,
	      char *datestring, struct tm *times, int separator)
{
    long filepos = -1;
    FILE *fd = fopen(logfilepath, "a");
    if (!fd) {			/* cannot open? try to make dir */
	char logdir[1024];
	sprintf(logdir, "%s/%s", homedir, logreldir);
	mkdir(logdir, 0777);
	fd = fopen(logfilepath, "a"); /* retry now */
    }
    if (!fd) {				/* cannot do anything */
	P(E, "Warning: could not open %s!\n", logfilepath);
	return filepos;
    }
    if (ftell(fd) == 0) {		/* init file if empty */
	init_log_file(1, fd);
	last_times = *times;
    }
    fd = add_log_day(fd, times);
    if (fd) {
	fprintf(fd, 
		"%s <b>%s</b> <a href=\"%s\">%s</a> %s <small>0x%x</small>\n",
		separator ? "<hr>" : "<br>",
		name, url, url, datestring, win);
	fflush(fd);
	filepos = ftell(fd);
	fclose(fd);
	update_tag_window(tag_window, name, url, win, datestring, 
			  separator, filepos);
    }
    return filepos;
}

FILE *					/* can be modified... */
add_log_day(FILE *fd, struct tm *times)
{
    if (last_times.tm_year != times->tm_year
	|| last_times.tm_mon != times->tm_mon
	|| last_times.tm_mday != times->tm_mday) { 
	/* not same day as last entry: look if same week */
	if (last_times.tm_year != times->tm_year /* not same year */
	    || (times->tm_yday - last_times.tm_yday) >= 7 /* not same week */
	    || times->tm_wday < last_times.tm_wday) { /* a sunday has passed */
	    char command[1000];
	    char *dir = concat_paths(homedir, logreldir, ".");
	    fclose (fd);
	    if (verbose)
		P(E, "New week! Starting new log file, archiving previous\n");
	    sprintf(command, "cd %s; if test -f %s; then mv %s %s-%02d-%02d-%02d.html; %s -9 %s-%02d-%02d-%02d.html; fi",
		    dir, logfilename, logfilename, logfilebase,
		    times->tm_year+1900, times->tm_mon+1, times->tm_mday,
		    gzippath, logfilebase, times->tm_year+1900,
		    times->tm_mon+1, times->tm_mday);
	    system(command);
	    fd = fopen(logfilepath, "a");
	    if (!fd) return 0;
	    init_log_file(0, fd);
	    fflush(fd);
	    /* we close the file for not mixing data with the following shell 
	     * command on some OSes and NFS, we reopen afterwards
	     * fix courtesy of canon@ecoledoc.ibp.fr (Hubert CANON)
	     */
	    fclose(fd);			
	    sprintf(command, "cd %s; for i in `ls -1r %s-[0-9]*.html*`; do echo \"<a href=\\\"$i\\\">`basename $i .html.gz`</a><br>\">>%s; done; echo '<hr>' >> %s",
		    dir, logfilebase, logfilename, logfilename);
	    system(command);
	    Free(dir);
 	    fd = fopen(logfilepath, "a"); 
	    if (!fd) return 0;
	}
	if (verbose)
	    P(E, "New day! adding day chapter in log file\n");
	/* warning: previous to v2.6, there was no <a> in day names */
	fprintf(fd, "<hr><p><h1><a name=\"%s\">%s %d %s %d</a></h1>\n",
		dayname[times->tm_wday], dayname[times->tm_wday], 
		times->tm_mday, monthname[times->tm_mon], times->tm_year+1900);
    } /* same day, do nothing */
    return fd;
}

char *
date2string(struct tm *times)
{
    static char datestring[40];
    sprintf(datestring, "%d/%02d/%02d-%02d:%02d:%02d",
	    times->tm_year+1900, times->tm_mon + 1, times->tm_mday, 
	    times->tm_hour, times->tm_min, times->tm_sec);
    return datestring;
}

/*****************************************************************************\
* 			     callbacks & handlers                             *
\*****************************************************************************/

void 
reinit_handler(int sig, void *sip, void *uap)
{
    must_reinit = 1;
}

int
terseXHandler(Display *dpy, XErrorEvent *error) {return 0;}

int
verboseXHandler(Display *dpy, XErrorEvent *error) 
{
    char buffer[256];
    XGetErrorText(dpy, error->request_code, buffer, 255);
    P(E, "(verbose mode) browser-history X Error: %s\n", buffer);
    return 0;
}

/*****************************************************************************\
* 			       window handling                                *
\*****************************************************************************/

void
detect_all_existing_browser_windows()
{
    Window dummy, *children = NULL, client;
    unsigned int i, nchildren = 0;

    if (!XQueryTree (dpy, DefaultRootWindow(dpy), &dummy, &dummy,
		     &children, &nchildren)) {
	return;
    }
    for (i = 0; i < nchildren; i++) {
	int browser;
	client = XmuClientWindow (dpy, children[i]);
	if (client != None && (browser = is_browser_window(client))) {
	    add_new_browser_window(client, browser);
	}	    
    }
    if (children) XFree(children);
}

int
is_browser_window(Window win)
{
    XClassHint class_hints;
    char *prop;
    int result = 0;
    if (XGetClassHint(dpy, win, &class_hints)) {
	int b;
	for (b = 1; b < browsers_size; b++) {
	    /* class must match, name can be a substring */
	    if (streq(class_hints.res_class, browsers[b].xclass)
		&& strstr(class_hints.res_name, browsers[b].xname)) {
		result = b;
		goto found;
	    }
	}
    found:
	XFree(class_hints.res_name);
	XFree(class_hints.res_class);
    }
    if (!result && (nilstring  != (prop = get_prop(win, BROWSER_HISTORY_INFO,
						   0)))) {
	result = BT_GENERIC;
	xfree(prop, 0);
    }
	
    return result;
}

/*****************************************************************************\
* 			browser window list management                        *
\*****************************************************************************/

void 
add_new_browser_window(Window win, int browser)
{
    int i;
    /* check it is not already registered */
    for (i = 0; i < browser_windows_size; i++)
	if (browser_windows[i].win == win)
	    return;
    /* append and init new window to list of windows monitered  */
    browser_windows = (Browser_win) 
	Realloc(browser_windows, 
		sizeof(struct _Browser_win) * (browser_windows_size+1));
    browser_windows[browser_windows_size].win = win;
    browser_windows[browser_windows_size].browser_type = browser;
    browser_windows[browser_windows_size].url_changed = 0;
    browser_windows[browser_windows_size].url = Strdup("");
    browser_windows[browser_windows_size].name = Strdup("");
    browser_windows_size++;

    if (verbose)
	P(E, "monitoring %s window: 0x%x  [%d]\n", browsers[browser].name,
	  (unsigned int) win, browser_windows_size - 1);

    /* track URL changes and window destruction */
    XSelectInput(dpy, win, PropertyChangeMask|StructureNotifyMask);

    /* add the current URL to the log */
    add_URL(browser_windows_size - 1);
}

void 
remove_browser_window(Window win)
{
    int i, n;
    /* check it is already registered */
    for (i = 0; i < browser_windows_size; i++)
	if (browser_windows[i].win == win) {
	    /* deletes from list, compacts */
	    n = i;
	    Free(browser_windows[n].url);
	    Free(browser_windows[n].name);
	    browser_windows_size--;
	    for (; i< browser_windows_size; i++) {
		browser_windows[i] = browser_windows[i+1];
	    }
	    if (verbose)
		P(E, "%s window closed: 0x%x  [%d]\n",
		  browsers[browser_windows[n].browser_type].name,
		  (unsigned int) win, n);
	    return;
	}
}

int
index_of_win(Window win)
{
    int i;
    for (i = 0; i < browser_windows_size; i++)
	if (browser_windows[i].win == win) 
	    return i;
    return -1;				/* -1 if not found */
}

/*****************************************************************************\
* 			   current URLs remembered                            *
\*****************************************************************************/

int
URL_already_present(char *name, char *url)
{
    int i;
    for (i = 0; i < browser_windows_size; i++)
	if (streq(name, browser_windows[i].name)
	    && streq(url, browser_windows[i].url))
	    return 1;
    return 0;
}

void 
record_URL(int n, char *name, char *url)
{
    Free(browser_windows[n].url);
    Free(browser_windows[n].name);
    browser_windows[n].url = Strdup(url);
    browser_windows[n].name = Strdup(name);
}

/*****************************************************************************\
* 				 log entries                                  *
\*****************************************************************************/

void
add_URL(int number)
{
    time_t seconds;
    struct tm *times;
    char *name, *url;
    char *full_name = 0, *ns_name = 0;
    Window win;

    if (number < 0) return;
    win = browser_windows[number].win;
    
    if (browser_windows[number].browser_type == BT_NETSCAPE) { /* netscape */
	ns_name = get_prop(win, WM_NAME, 0);	/* name */
	if (!strncmp("Netscape:", ns_name, 9)) { /* skip leading Netscape: */
	    if (ns_name[9] == ' ') {
		name = ns_name + 10;	/* skip also blank if there */
	    } else {
		name = ns_name + 9;
	    }
	} else {
	    name = ns_name;
	}
	url = get_prop(win, _MOZILLA_URL, 0);
    } else if (browser_windows[number].browser_type == BT_ARENA) { /* arena */
	if (nilstring == (full_name = get_prop(win, ARENA_LOCATION, 0)))
	    return;
	if (!decode_ARENA_LOCATION(full_name, &name, &url)) {
	    xfree(full_name, 0);
	    return;
	}
    } else if (browser_windows[number].browser_type == BT_AMAYA) { /* amaya */
	if (nilstring == (full_name = get_prop(win, BROWSER_HISTORY_INFO, 0)))
	    return;
	if (!decode_ARENA_LOCATION(full_name, &name, &url)) {
	    xfree(full_name, 0);
	    return;
	}
    } else if (browser_windows[number].browser_type == BT_GENERIC) { /* generic */
	if (nilstring == (full_name = get_prop(win, BROWSER_HISTORY_INFO, 0)))
	    return;
	if (!decode_ARENA_LOCATION(full_name, &name, &url)) {
	    xfree(full_name, 0);
	    return;
	}
    } else
	return;				/* unknown browser */
    if (url != nilstring && not_excluded_URL(url)
	&& !URL_already_present(name, url)) {
	char *datestring;
	time(&seconds);
	times = localtime(&seconds);
	datestring = date2string(times);
	add_log_entry(name, url, (unsigned int) win, datestring, times,
		      (((long) seconds - (long)last_time) > delay));
	last_times = *times;

	if (verbose) {
	    P(E, "Adding URL: %s\n", url); 
	    P(E, "     named: %s\n", name);
	    P(E, "      date: %s  -  Window: 0x%x  [%d]\n",
	      datestring, (unsigned int) win, number);
	    P(E, "   (debug): seconds: %ld, since_last: %ld\n",
	      (long) seconds, ((long) seconds - (long)last_time));
	}
	record_URL(number, name, url);
	last_time = seconds;
    }
    if (browser_windows[number].browser_type == BT_NETSCAPE)
	xfree(ns_name, 0), xfree(url, 0);
    else /* arena, amaya, generic */
	xfree(full_name, 0);
}

/*****************************************************************************\
* 				  Tag Window                                  *
\*****************************************************************************/
/* For identification purposes, browser-window maintains an unmapped 
 * "Tag Window" window whose ID is stored on the root window of each
 * managed screen in the BROWSER_HISTORY property
 * This window has the following properties on it:
 * - BROWSER_HISTORY    holding the window ID, to prove process is still alive
 * - LAST_URL    a text property, concatenation of null terminated strings
 *   prefixed by a tag=. e.g:
 *   URL=url_text\0TITLE=page_title\0DATE=date_string\0BROWSER_WINDOW_ID=id\0
 *   This property will be updated on each new entry in the log file.
 *   at start, it exists but is empty (length = 0)
 * - LAST_POS (since v2.7) a text property containing the index (ascii number)
 *   of the last updated position in the current log file. This allows 
 *   external processes to read the log file and stop at the logical end
 *   in case they read just while we are appending. Can be -1 if not defined
 * - PROCESS     a text property, concatenation of null terminated strings
 *   prefixed by a tag=, giving a description of the running process:
 *   version    a number (card32) made by 1000*major+minor: v3.4 => 3004
 *   date       date of launch of the process (string)
 *   machine    the name of the machine on which the process runs
 *   pid        its process id
 *   This property is updated once at process start
 *   e.g:   VERSION=1009\0DATE=96/05/16-09:17:08\0MACHINE=koala\0PID=4345\0
 *  
 * Destroying this window will terminate properly the currently running
 * browser-history process
 */

Window
create_tag_window(int mode, int version, char *machine, int pid)
{
    XSetWindowAttributes wa;
    Window w;
    char string[1024];
    int len = 0;
    Atom actual_type;
    int actual_format;
    unsigned long nitems;
    unsigned long bytes_after_return;
    unsigned char *property;
    int result;
    char *datestring;
    time_t seconds;
    struct tm *times;
 
    time(&seconds);
    times = localtime(&seconds);
    datestring = date2string(times);

    if (!dontgrab)
	XGrabServer(dpy);			/* to avoid race conditions */
    result = 
	XGetWindowProperty(dpy, DefaultRootWindow(dpy), BROWSER_HISTORY, 0, 1, 0,
			   XA_WINDOW, &actual_type, &actual_format, &nitems,
			   &bytes_after_return, &property);
    if (result != Success || actual_type != XA_WINDOW) {
	if (result == Success) XFree(property);
	goto OK;			/* no previous property */
    }
    w = ((long *) property)[0];
    XFree(property);
    result = 
	XGetWindowProperty(dpy, w, BROWSER_HISTORY, 0, 1, 0,
			   XA_WINDOW, &actual_type, &actual_format, &nitems,
			   &bytes_after_return, &property);
    if (result != Success || actual_type != XA_WINDOW
	|| w != ((long *) property)[0]) {
	if (result == Success) XFree(property);
	goto OK;			/* not a browser-history window */
    }
    XFree(property);
    if (mode == -1) {
	XUngrabServer(dpy);
	if (verbose)
	    P(E, "a previous browser-history is running, aborting\n");
	exit(0);
    }
    if (mode == 1 || mode == 2) {
	if (verbose)
	    P(E, "killing previous browser-history\n");
	XDestroyWindow(dpy, w);
    }
    if (mode == 2)
	exit(0);
    if (mode == 0) {			/* check version */
	int old_version;
	result = XGetWindowProperty(dpy, w, PROCESS, 0, 63, 0,
				    XA_STRING, &actual_type, &actual_format,
				    &nitems, &bytes_after_return, &property);
	old_version = atoi(strchr(property, '=') + 1);
	XFree(property);
	if (version > old_version) {
	    if (verbose)
		P(E, "killing previous version (%d.%d) of browser-history\n", 
		  old_version/1000, old_version%1000);
	    XKillClient(dpy, w);
	} else {
	    XUngrabServer(dpy);
	    if (verbose)
		P(E, "same or more recent version (%d.%d) of browser-history running, aborting\n", old_version/1000, old_version%1000);
	    exit(0);
	}
    }
    
OK:
    wa.override_redirect = True;
    w = XCreateWindow (dpy, DefaultRootWindow(dpy), -100, -100, 10, 10, 0, 0,
		       InputOnly, CopyFromParent, CWOverrideRedirect, &wa);
    /* set pointer to the tag window on root */
    XChangeProperty(dpy, DefaultRootWindow(dpy), BROWSER_HISTORY, XA_WINDOW,
		    32, PropModeReplace, (unsigned char *) &w, 1);
    /* set properties on the tag window */
    XChangeProperty(dpy, w, BROWSER_HISTORY, XA_WINDOW,
		    32, PropModeReplace, (unsigned char *) &w, 1);
    XChangeProperty(dpy, w, LAST_URL, XA_STRING,
		    8, PropModeReplace, (unsigned char *) "", 0);
    XChangeProperty(dpy, w, LAST_POS, XA_STRING,
		    8, PropModeReplace, (unsigned char *) "", 0);
    len += sv_sprintfd(string, "VERSION=%d", version) + 1; /* must be 1st */
    len += sv_sprintfs(string + len, "DATE=%s", datestring) + 1;
    len += sv_sprintfs(string + len, "MACHINE=%s", machine) + 1;
    len += sv_sprintfd(string + len, "PID=%d", pid) + 1;
    XChangeProperty(dpy, w, PROCESS, XA_STRING, 8, PropModeReplace, 
		    (unsigned char *) string, len);
    XUngrabServer(dpy);

    return w;
}

void
update_tag_window(Window w, char *name, char *url, unsigned int win,
		  char *datestring, int separator, long filepos)
{
    char string[8000];
    int len = 0;
    len += sv_sprintfs(string, "URL=%s", url) + 1; /* must be 1st */
    len += sv_sprintfs(string + len, "TITLE=%s", name) + 1;
    len += sv_sprintfs(string + len, "DATE=%s", datestring) + 1;
    len += sv_sprintfd(string + len, "BROWSER_WINDOW_ID=%d", win) + 1;
    XChangeProperty(dpy, w, LAST_URL, XA_STRING, 8, PropModeReplace, 
		    (unsigned char *) string, len);

    len = 0;
    len += sv_sprintfs(string, "%ld", filepos) + 1;
    XChangeProperty(dpy, w, LAST_POS, XA_STRING, 8, PropModeReplace, 
		    (unsigned char *) string, len);
}

/*****************************************************************************\
* 			    browser-specific stuff                            *
\*****************************************************************************/

int					/* 0=error */
decode_ARENA_LOCATION(char *full_name, char **namep, char **urlp)
{
    char *p = full_name, *end;
    *urlp = *namep = 0;
    while ((end = strchr(p, '='))) {
	if (!strncmp("URL", p, 3)) {
	    *urlp = end + 1;
	} else if (!strncmp("TITLE", p, 5)) {
	    *namep = end + 1;
	}
	p = end + strlen(end) + 1;
    }
    return (*urlp && *namep) ? 1 : 0;
}

/*****************************************************************************\
* 				 safe-mallocs                                 *
\*****************************************************************************/
/* package used to run even in low-memory situations:
 * Malloc, Realloc, Strdup waits 10s and retry in case of no more available
 *  memory 
 */

#define GUARD_FUNCTION(original, guarded, ret_type, arg_types, args) \
    ret_type guarded arg_types {void *retvalue; \
    while(!(retvalue = original args)) sleep(10); \
    return (ret_type) retvalue;}

GUARD_FUNCTION(malloc, Malloc, char *, (int bytes), (bytes))
   
GUARD_FUNCTION(realloc, Realloc, char *, (void *ptr, int bytes), (ptr, bytes))

GUARD_FUNCTION(strdup, Strdup, char *, (char *string), (string))

/*****************************************************************************\
* 				 string utils                                 *
\*****************************************************************************/

/* creates a memory leak. to be called only on inits */
char *
concat_paths(char *path, char *name1, char *name2)
{
    char *new = (char *) Malloc(strlen(path) + strlen(name1) + 
				strlen(name2) + 3);
    strcpy(new, path);
    strcat(new, "/");
    strcat(new, name1);
    strcat(new, "/");
    strcat(new, name2);
    return new;
}

#ifdef BSD
/* sysv sprintf emulation */
int
sv_sprintfd(char *buffer, char *format, int arg)
{
    sprintf(buffer, format, arg);
    return strlen(buffer);
}

int
sv_sprintfs(char *buffer, char *format, char *arg)
{
    sprintf(buffer, format, arg);
    return strlen(buffer);
}

#endif /* BSD */

/*****************************************************************************\
* 				   X utils                                    *
\*****************************************************************************/

char *
get_prop(Window win, Atom name, int offset)
{
    Atom            actual_type;
    int             actual_format;
    unsigned long   nitems;
    unsigned long   bytes_after;
    unsigned char  *buffer = 0;
    
    if (Success == XGetWindowProperty(dpy, win, name, 0, 8000, 0,
				      AnyPropertyType, &actual_type,
				      &actual_format, &nitems, &bytes_after,
				      &buffer)
	&& buffer && nitems) {
	buffer[nitems] = '\0';
	if (!(actual_type == XA_STRING 
	      && (strlen((char *)buffer) >= offset))) {
	    XFree(buffer);
	    buffer = 0;
	} /* else ok */
    } else {
	if (buffer) XFree(buffer);
	buffer = 0;
    }
    return buffer ? (char *) buffer + offset : nilstring;
}

void
xfree(void *ptr, int offset)
{
    if (ptr && (ptr != nilstring))
	XFree(((char *) ptr) - offset);
}

/*****************************************************************************\
* 			      date & time utils                               *
\*****************************************************************************/

/* on startup, look in last 60 chars when was last entry added */

void
init_last_entry()
{
    FILE *fd;
    /* on failure to find date, set time to 1970/01/01 */
    last_times.tm_year = 70;
    last_times.tm_mon = 0;
    last_times.tm_mday = 1;
    last_times.tm_hour = last_times.tm_min = last_times.tm_sec = 0;
    last_time = 0;

    if ((fd = fopen(logfilepath, "r"))) {
	char buffer[61], *line = buffer;
	int y,m,d,h,mm,s;
	fseek(fd, 0L, SEEK_END);	/* goto end, fetch start of line */
	fseek(fd, -60L, SEEK_CUR);	/* move back at start of buffer */
	fread(buffer, 60, 1, fd);	/* read them into buffer */
	buffer[60] = '\0';
	line = strstr(buffer, "</a>");	/* look for occurence of </a> */
	if (line && 6 == sscanf(line, "</a> %d/%02d/%02d-%02d:%02d:%02d",
				&y,&m,&d,&h,&mm,&s)) {
	    if (y < 100)			/* Fix Y2K bug */
	      last_times.tm_year = y;		/* Assume before 2000 */
	    else
	      last_times.tm_year = y - 1900;	/* y contains full year */
	    last_times.tm_mon = m - 1;
	    last_times.tm_mday = d;
	    last_times.tm_hour = h;
	    last_times.tm_min = mm;
	    last_times.tm_sec = s;
	    last_time = mktime(&last_times); /* compute day of week */
	    last_times = *localtime(&last_time);
	}
	fclose(fd);
    }
    if (!last_time)
	last_time = mktime(&last_times);
    if (verbose)
	P(E, "Last entry was at: %d/%02d/%02d-%02d:%02d:%02d (day of week: %d (%s), day of year: %d)\n",
	  last_times.tm_year+1900, last_times.tm_mon + 1, last_times.tm_mday, 
	  last_times.tm_hour, last_times.tm_min, last_times.tm_sec,
	  last_times.tm_wday, dayname[last_times.tm_wday], last_times.tm_yday);
}

/*****************************************************************************\
* 				 portability                                  *
\*****************************************************************************/
#ifdef NO_STRSTR
/*
 *----------------------------------------------------------------------
 *
 * strstr --
 *
 *	Locate the first instance of a substring in a string.
 *
 * Results:
 *	If string contains substring, the return value is the
 *	location of the first matching instance of substring
 *	in string.  If string doesn't contain substring, the
 *	return value is 0.  Matching is done on an exact
 *	character-for-character basis with no wildcards or special
 *	characters.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

char *
strstr(string, substring)
    register char *string;	/* String to search. */
    char *substring;		/* Substring to try to find in string. */
{
    register char *a, *b;

    /* First scan quickly through the two strings looking for a
     * single-character match.  When it's found, then compare the
     * rest of the substring.
     */

    b = substring;
    if (*b == 0) {
	return string;
    }
    for ( ; *string != 0; string += 1) {
	if (*string != *b) {
	    continue;
	}
	a = string;
	while (1) {
	    if (*b == 0) {
		return string;
	    }
	    if (*a++ != *b++) {
		break;
	    }
	}
	b = substring;
    }
    return (char *) 0;
}

#endif /* NO_STRSTR */

