/* mod_cvs.c - Module for Apache which automatically updates files that are
               checked out from a CVS repository.

   Authors:      Martin Insulander (main@isk.kth.se)
   Contributors: Frank Patz (fp@contact.de)

   Copyright (C) 1999 Martin Insulander.

   LICENSE AGREEMENT

   This software is copyrighted (C) 1999 by Martin Insulander. You may use it
   freely for commercial or non-commercial use. Distributing and modifying the
   software is allowed, as long as this license agreement is distributed
   without modification. You may not make any profit by selling this software,
   unless agreement has been made with Martin Insulander. If you wish to include
   mod_cvs in some software package that you intend to charge money for, contact
   the author so that an agreement can be made.

   $Id: mod_cvs.c,v 1.24 1999/04/15 13:07:25 main Exp $
*/

#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_log.h"
#include "util_script.h"
#include "http_main.h"
#include "http_request.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <utime.h>
#include <errno.h>

#define BUFSIZE 256
#define CVS_REPFILE "/CVS/Repository"
#define DEFAULT_CVS_CMDLINE "cvs -q update -dP %s"
#define DEFAULT_CVS_DATECMDLINE "cvs -q update -fp -D %s %s"
#define DEFAULT_CVS_LOGCMDLINE "cvs log %s";
#define DEFAULT_CVS_LOCKPATH "./CVS"
#define DEFAULT_CVS_WAITTIMEOUT "30"
#define TMP_PREPEND "mod_cvs"
#define LOG_APPEND ".cvslog"
#define LOCKFILE "mod_cvs_lock"
#define MTIME_DIFFER 30 /* st_mtime can differ between the repository file and
			  the checked out copy, allow that to be this many
			  seconds */


module MODULE_VAR_EXPORT cvs_module;

/* Structure of configuration variables*/
typedef struct {
    int check_cvs;
    int allow_date;
    int allow_log;
    int allow_cvsfiles;
    int use_locking;
    int wait_for_lock;
    char *cvs_waittimeout;
    char *cvs_cmdline;
    char *cvs_datecmdline;
    char *cvs_logcmdline;
    char *cvs_lockpath;
} cvs_config;

/* Call cvs with locking, to avoid running multiple cvs's on the same
   requested file. */
static int call_cvs(request_rec *r, const char *cmd, const char *file)
{
    struct stat st;
    int result, timeout, count; 
    char *lockfile;
    cvs_config *cfg;

    /* Get config information */
    cfg = (cvs_config*) ap_get_module_config(r->per_dir_config, &cvs_module);

    /* Check wether we're supposed to use locking */
    if (!cfg->use_locking) {
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
		     "%s", cmd);
	return system(cmd);
    }

    /* Execute CVS with locking. If a lock file is found, another mod_cvs
       is already running, and then we'll return the old version. */

    lockfile = ap_pstrcat(r->pool, cfg->cvs_lockpath, "/", LOCKFILE, NULL);
    
    if (open(lockfile,O_CREAT|O_EXCL) != -1) {
	/* We've created a lockfile and are now updating the file */
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
		     "Lock aquired: %s", lockfile);
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
		     "%s", cmd);
	result = system(cmd);
	if (unlink(lockfile) == 0) {
	    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
			 "Lock released: %s", lockfile);
	} else {
		ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server,
			     "Couldn't release lock: %s", lockfile);
	}
    } else if (errno == EEXIST) {
	/* Found a lock file. We'll either wait or show old revision. */
	if (cfg->wait_for_lock) {
	    timeout = atoi(cfg->cvs_waittimeout);
	    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
			 "Lockfile found: %s, waiting (timeout=%d).", 
			 lockfile, timeout);
	    count = 0;
	    while(stat(lockfile,&st) == 0) {
		sleep(1);
		count++;
		if ( (timeout != 0) && (count >= timeout) ) {
		    /* Wait for lock timed out - we'll delete the lockfile */
		    unlink(lockfile);
		    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR,
				 r->server, "Timeout waiting for lockfile: %s (timeout=%d)", lockfile, timeout);
		}
	    }
	    
	} else {
	    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
			 "Lockfile found: %s, showing old revision", 
			 lockfile);
	}
	result = 0;
    } else {
	/* There was an error while creating the lockfile */
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server,
		     "Couldn't create lockfile: %s", lockfile);
	result = 1;
    }
    
    return result;
}

/* cvs_log - print output of "cvs log" of the requested file */
static int cvs_log(request_rec *r, char *dir, char *file) {
    char *pstr, *ps, *c;
    struct stat st;
    cvs_config *cfg;

    /* Get config information */
    cfg = (cvs_config*) ap_get_module_config(r->per_dir_config, &cvs_module);

    if ((!cfg->allow_log) && (r->prev == NULL)) {
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server,
		     "CVS log denied: %s", r->uri);
        return FORBIDDEN;
    }
    /* pstr = command line string */
    ps = ap_pstrcat(r->pool, file, " > '", TMP_PREPEND, file, 
		      LOG_APPEND, "'",NULL);
    pstr = ap_psprintf(r->pool, cfg->cvs_logcmdline, ps);
    /* Run CVS */
    chdir(dir);
    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server,
		 "CVS Log on %s", r->filename);
    if (call_cvs(r, pstr, file) != 0) {
	/* CVS returned non-zero which means the user probably specified
	   an erraneous date format, we'll return NOT_FOUND */
	return NOT_FOUND;
    }
    /* Check if the file is there and size > 0 (the user could have 
       entered the wrong filename) */
    pstr=ap_pstrcat(r->pool, TMP_PREPEND, file, LOG_APPEND, NULL);
    if ((stat(pstr, &st) != 0) || (st.st_size == 0)) {
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
		     "CVS log gave me nothing in %s", pstr);
	return NOT_FOUND;
    }

    pstr = ap_pstrdup(r->pool, r->uri);
    c = strrchr(pstr, '/')+1;
    if (c != NULL)
	*c = '\0';
    pstr = ap_pstrcat(r->pool, pstr, TMP_PREPEND, file, LOG_APPEND, NULL);
	
    r->args = NULL;
    stat(r->filename,&(r->finfo)); 
    ap_internal_redirect(pstr, r);

    pstr = ap_pstrcat(r->pool, TMP_PREPEND, file, LOG_APPEND, NULL);
    if (unlink(pstr) != 0) 
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server,
		     "Couldn't unlink temporary file: %s", pstr);
    return DONE;
}



/* Fixup function - this is what actually gets done */
static int cvs_fixup(request_rec *r)
{
    char s[BUFSIZE];
    char *pstr, *ps, *ps2, *c, *dir, *file, *date;
    FILE *repfile;
    struct stat st;
    int retval = DECLINED;
    cvs_config *cfg;
    struct utimbuf newutime;

    /* Check request method, only GET allowed */
    if (r->method_number != M_GET) {
	goto QUIT;
    }

    /* Get config information */
    cfg = (cvs_config*) ap_get_module_config(r->per_dir_config, &cvs_module);

    /* Check wether we're supposed to do a cvs update (if needed) or not */
    if (! cfg->check_cvs) {
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
		     "CVSCheck is off here: %s", r->uri);
	goto QUIT;
    } 

    /* Check if the requested file is within the CVS/ dir, and deny req if
       we're supposed to. */
    if ( (!cfg->allow_cvsfiles) && (strstr(r->uri,"/CVS/") != NULL) ) {
	retval = FORBIDDEN;
	goto QUIT;
    }

    /* If we're the first request (not internally redirected), set up a
       timeout in case mod_cvs screws up */
    if (r->prev == NULL) {
	ap_soft_timeout("mod_cvs timeout", r);
    }

    /* Some string handling... */
    dir = ap_pstrdup(r->pool, r->filename);
    c = strrchr(dir,'/');
    if (c != NULL)
	*c = '\0';
    file = ap_pstrdup(r->pool, (char*) rindex(r->filename,'/')+1);



    /* ---- DATE/LOG ARG ---- */
    /* Check if there was a check-out date specified in the query args */
    if (r->args != NULL) {
	if (r->finfo.st_mode & S_IFDIR) {
	    /* A ?DATE=<date> arg is specified with a directory, we'll
	       decline this and handle index files later instead */
	    retval = DECLINED;
	    goto QUIT;
	}
	pstr = ap_pstrdup(r->pool, r->args);
	c = strrchr(pstr, '=');
	if (c != NULL)
	    *c = '\0';
	if (strcmp(pstr,"DATE") == 0) {
	    /* There was a ?DATE=<date> arg, we need to redirect it back
	       to the /DATE=<date>/ format */
	    c++;
	    date = ap_pstrdup(r->pool, c);
	    pstr = ap_pstrcat(r->pool, "/DATE=", date, r->uri, NULL);
	    ap_internal_redirect(pstr, r);
	    retval = DONE;
	    goto QUIT;
	} else if (strcmp(pstr,"LOG") == 0) {
	    /* LOG parameter passed */
	    retval = cvs_log(r,dir,file);
	    goto QUIT;
	}
    }

    /* ---- DATE PATH ---- */
    /* Check if there was a check-out date specified in the request */
    pstr = ap_pstrdup(r->pool, file);
    c = strrchr(pstr,'=');
    if (c != NULL)
	*c = '\0';
    if (strcmp(pstr,"DATE") == 0) {
	/* DATE specified in URI, we're checking out an old version... */
	if ((!cfg->allow_date) && (r->prev == NULL)) {
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server,
		     "Date checkout denied: %s", r->uri);
	    retval = FORBIDDEN;
	    goto QUIT;
	}
	c++;
	date = ap_pstrdup(r->pool, c);
	pstr = ap_pstrdup(r->pool, r->path_info);
	c = strrchr(pstr,'/');
	if (c != NULL) {
	    *c = '\0';
	    dir = ap_pstrcat(r->pool, dir, pstr, NULL);
	    file = c+1;
	} else {
	    file = "";
	}

	chdir(dir);

	if (strcmp(file, "") == 0) {
	    if ((stat(dir,&st) == 0) && (st.st_mode & S_IFDIR)) {
		/* The request was for a directory, we need to redirect this
		   with an arg (?DATE=<date>) instead */
		pstr = ap_pstrcat(r->pool, r->path_info, "?DATE=", date, NULL);
		ap_internal_redirect(pstr, r);
		retval = DONE;
	    } else {
		retval = NOT_FOUND;
	    }
	    goto QUIT;
	}

	if ((stat(file, &st) != 0) && (st.st_mode & S_IFDIR)) {
	    /* Directory specified without trailing /. We have to do an
	       external redirect. */
	    pstr = ap_pstrcat(r->pool, r->uri, "/", NULL);
	    ap_table_add(r->headers_out, "Location", pstr);
	    retval = REDIRECT;
	    goto QUIT;
	}

	/* pstr = command line string */
	ps = ap_pstrcat(r->pool, "\"", date, "\"", NULL);
	ps2 = ap_pstrcat(r->pool, file, " > '", TMP_PREPEND, date, file, 
			 "'", NULL);
	pstr = ap_psprintf(r->pool, cfg->cvs_datecmdline, ps, ps2);
	/* Run CVS */
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server,
		 "CVS Date Checkout on %s", r->filename);
	if (call_cvs(r, pstr, file) != 0) {
	    /* CVS returned non-zero which means the user probably specified
	       an erraneous date format, we'll return NOT_FOUND */
	    retval = NOT_FOUND;
	    goto QUIT;
	}
	
	/* Check if the file is there and size > 0 (the user could have 
	   entered the wrong filename) */
	pstr=ap_pstrcat(r->pool, TMP_PREPEND, date, file, NULL);
	if ((stat(pstr, &st) != 0) || (st.st_size == 0)) {
	    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
			 "Date checkout gave me nothing in %s", pstr);
	    retval = NOT_FOUND;
	    goto QUIT;
	}

	pstr = ap_pstrdup(r->pool, r->path_info);
	c = strrchr(pstr, '/')+1;
	if (c != NULL)
	    *c = '\0';

	pstr = ap_pstrcat(r->pool, pstr, TMP_PREPEND, 
			  ap_escape_path_segment(r->pool, date), file, NULL);
	
	stat(r->filename,&(r->finfo)); 
	ap_internal_redirect(pstr, r);

	pstr = ap_pstrcat(r->pool, TMP_PREPEND, date, file, NULL);
	if (unlink(pstr) != 0) 
	    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server,
			 "Couldn't unlink temporary file: %s", pstr);
	retval = DONE;
	goto QUIT;
    }
	
    /* ---- NO DATE/LOG REQUEST ---- */
    pstr = ap_pstrcat(r->pool, dir, CVS_REPFILE, NULL);
    if (!(repfile = fopen(pstr, "r"))) {
	ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
		     "Repository file not found: %s", pstr);
    } else {
	/* Read repository file for location of the repository */
	fgets(s,BUFSIZE,repfile);
	s[strlen(s)-1] = '\0';
	fclose(repfile);

	/* If it's not one of our own temporary files, check if we're
	 supposed to update and if so, run cvs and do lots of magic */
	if (strncmp(TMP_PREPEND, file, strlen(TMP_PREPEND)) != 0) {
	    /* Let's stat the file(s) in the repository */
	    pstr = ap_pstrcat(r->pool, s, "/", file, ",v", NULL);
	    if (stat(pstr,&st) != 0) {
		/* ,v file not found, is it a directory? */
		pstr = ap_pstrcat(r->pool, s, "/", file, NULL);
		if (stat(pstr,&st) != 0) {
		    /* no, the file might be in the attic (deleted) */
		    pstr = ap_pstrcat(r->pool, s, "/Attic/", file, ",v", NULL);
		    if (stat(pstr, &st) != 0) {
			/* nope, but let's just log a debug message */
			ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, 
				     r->server, "No %s/(Attic/)%s(,v)", s, 
				     file);
			retval = DECLINED;
			goto QUIT;
		    }
		} 
	    }

	    if ((r->finfo.st_mode == 0) || 
		((st.st_mtime - MTIME_DIFFER) > r->finfo.st_mtime)) {
		/* The file has changed, we're updating */
		
		chdir(dir); /* weird CVS behaviour makes this better */

		/* pstr = command line string */
		pstr = ap_psprintf(r->pool, cfg->cvs_cmdline, file);

		/* Run CVS */
		ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r->server,
			     "CVS Update on %s", r->filename);
		if (call_cvs(r, pstr, file) != 0) {
		    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR,
				 r->server, "CVS update failed.");
		    retval = SERVER_ERROR;
		    goto QUIT;
		}
		
		ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
			     "Update done.");
		
		if (st.st_mode & S_IFDIR) {
		    /* We know the dir is updated - let's set the mtime, just
		       to be sure (KLUDGE correct of some weird bug.. cause
		       of strange CVS behaviour? */
		    newutime.actime = st.st_atime;
		    newutime.modtime = st.st_mtime;
		    if (utime(file, &newutime) != 0) {
			ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR,r->server,
				     "Couldn't reset atime/mtime for: %s", file);
			retval = SERVER_ERROR;
			goto QUIT;
		    }
		    /* The user requested a directory - we need to redirect, so
		       that we can find the indexfile and stuff */
		    r->path_info = "";
		    ap_internal_redirect(r->uri, r);
		    return DONE;
		}
		
		/* Pass on info to the next handlers */
		/* When Apache doesn't find the file it sets r->filename to be
		   the closest directory found, and r->path_info to be the part
		   that was not found. Now, we'll append path_info to
		   filename, because now r->filename + "/" + r->path_info does
		   exist (hopefully). */
		if ((r->path_info != NULL) && (strcmp(r->path_info,"") != 0)) {
		    r->filename = ap_pstrcat(r->pool, r->filename,
					     r->path_info, NULL);
		    r->path_info = "";
		}
		/* We also have to re-stat the file for the next handlers to
		   get the picture. */
		if (stat(r->filename,&(r->finfo)) != 0) {
		    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server,
				 "File not found: %s", 
				 r->filename);
		    retval = NOT_FOUND;
		    goto QUIT;
		}
		
	    } else {
		ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r->server,
			     "No update needed on: %s", r->filename);
	    }
	}
    }
    
    
 QUIT:
    /* No freeing needed since we use resource pool provided by server */
    ap_kill_timeout(r);
    return retval;
}

/* Module initialization - called when server starts */
void cvs_init(server_rec *s, pool *p) {
    ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_INFO, s,
		 "mod_cvs $Id: mod_cvs.c,v 1.24 1999/04/15 13:07:25 main Exp $");
}

/* Configuration creation - creates config structure */
void *create_cvs_config(pool *p, char *dummy) {
    cvs_config *new = (cvs_config*) ap_palloc(p, sizeof(cvs_config));
    new->check_cvs = 0;
    new->allow_date = 0;
    new->allow_date = 0;
    new->allow_cvsfiles = 0;
    new->use_locking = 1;
    new->wait_for_lock = 1;
    new->cvs_cmdline = DEFAULT_CVS_CMDLINE;
    new->cvs_datecmdline = DEFAULT_CVS_DATECMDLINE;
    new->cvs_logcmdline = DEFAULT_CVS_LOGCMDLINE;
    new->cvs_lockpath = DEFAULT_CVS_LOCKPATH;
    new->cvs_waittimeout = DEFAULT_CVS_WAITTIMEOUT;
    return new;
}

/* Command record - list of commands to corresponding directives */
command_rec cvs_cmds[] = {
    { "CVSCheck", ap_set_flag_slot, (void*) XtOffsetOf(cvs_config, check_cvs),
      OR_FILEINFO, FLAG, "On or Off" },
    { "CVSAllowDateCheckout", ap_set_flag_slot, (void*) 
      XtOffsetOf(cvs_config, allow_date), OR_FILEINFO, FLAG, "On or Off" },
    { "CVSAllowLog", ap_set_flag_slot, (void*) 
      XtOffsetOf(cvs_config, allow_log), OR_FILEINFO, FLAG, "On or Off" },
    { "CVSAllowCVSFiles", ap_set_flag_slot, (void*) 
      XtOffsetOf(cvs_config, allow_cvsfiles), OR_FILEINFO, FLAG, "On or Off" },
    { "CVSUseLocking", ap_set_flag_slot, (void*)
      XtOffsetOf(cvs_config, use_locking), OR_FILEINFO, FLAG, "On or Off" },
    { "CVSWaitForLock", ap_set_flag_slot, (void*)
      XtOffsetOf(cvs_config, wait_for_lock), OR_FILEINFO, FLAG, 
      "On or Off" },
    { "CVSWaitTimeout", ap_set_string_slot,
      (void*) XtOffsetOf(cvs_config, cvs_waittimeout), OR_FILEINFO, TAKE1,
      "Number of seconds to wait for lockfile to disappear." },
    { "CVSCmdline", ap_set_string_slot, 
      (void*) XtOffsetOf(cvs_config, cvs_cmdline), OR_FILEINFO, TAKE1,
      "The commandline invoked (filename is appended) when updating." },
    { "CVSDateCmdline", ap_set_string_slot,
      (void*) XtOffsetOf(cvs_config, cvs_datecmdline), OR_FILEINFO, TAKE1,
      "The commandline invoked (date and filename appended) at date checkout." },
    { "CVSLogCmdline", ap_set_string_slot,
      (void*) XtOffsetOf(cvs_config, cvs_logcmdline), OR_FILEINFO, TAKE1,
      "The commandline invoked (filename appended) for log output." },
    { "CVSLockPath", ap_set_string_slot,
      (void*) XtOffsetOf(cvs_config, cvs_lockpath), OR_FILEINFO, TAKE1,
      "The path (relative to the requested file) where mod_cvs puts its lockfiles." },
    { NULL }
};


/* Module record, list of functions for server to call */
module MODULE_VAR_EXPORT cvs_module =
{
    STANDARD_MODULE_STUFF,
    cvs_init,			/* initializer */
    create_cvs_config,		/* create per-directory config structure */
    NULL,		        /* merge per-directory config structures */
    NULL,			/* create per-server config structure */
    NULL,			/* merge per-server config structures */
    cvs_cmds,			/* command table */
    NULL,		        /* handlers */
    NULL,		        /* translate_handler */
    NULL,			/* check_user_id */
    NULL,			/* check auth */
    NULL,			/* check access */
    NULL,			/* type_checker */
    cvs_fixup,		        /* pre-run fixups */
    NULL,			/* logger */
    NULL,			/* header parser */
    NULL,			/* child_init */
    NULL,			/* child_exit */
    NULL			/* post read-request */
};
