/* *************************************************************
   RWhois Software

   Copyright (c) 1994 Scott Williamson and Mark Kosters
   Copyright (c) 1996-2000 Network Solutions, Inc.

   See the file LICENSE for conditions of use and distribution.
**************************************************************** */

#include "register.h"

#include "anon_record.h"
#include "client_msgs.h"
#include "delete.h"
#include "fileinfo.h"
#include "fileutils.h"
#include "guardian.h"
#include "index.h"
#include "index_file.h"
#include "log.h"
#include "main_config.h"
#include "misc.h"
#include "parse.h"
#include "records.h"
#include "reg_ext.h"
#include "search.h"
#include "state.h"
#include "validate_rec.h"

#define DB_FILE_TEMPLATE    "%s/%s.XXXXXX"

/* local prototypes */

/* returns a list of records that match the identity contained in the
   anonymous record.  The list is normally either empty, or of length
   1, but the use of a list allows us to handle cases where, due to
   database mismanagment, there are two copies of a record with the
   same identity */
static int get_real_object_from_anon PROTO((anon_record_struct *anon_rec,
                                            dl_list_type       *record_list));

/* checks the record for uniqueness in the database; basically, it
   searches on the primary keys (*not* ID) to determine this. */
static int check_uniq_record PROTO((record_struct *record));

/* determines if the new and old records essentially point to the same
   object, thus allowing the new object to replace the old object. It
   should probably check primary keys, but does not */
static int ensure_objects_match PROTO((record_struct *new_record,
                                       record_struct *old_record));

/* given a record, write it into the database and index it. */
static int index_new_record PROTO((record_struct *record));

/* checks the record contained in the record handle for suitability
   for addition to the database.  Since it contains a possible call to
   an application specific external parse routine, the add record may
   change, hence the handle and need of the email address */
static int check_add PROTO((record_struct  **record_p, char *reg_email));

/* checks the records contained in the old and new record handles for
   suitability for a database MOD operation.  The routine may change
   the new and old record specifications, and it returns a list of
   records to be deleted as part of the del-add operation that forms a
   mod. */
static int check_mod PROTO((record_struct      **new_record_p,
                            anon_record_struct **old_record_p,
                            dl_list_type       *old_record_list,
                            char               *reg_email));

/* checks the anonymous record for suitability for deletion.  It
   returns in 'record_list' a list of records to actually be deleted */
static int check_del PROTO((anon_record_struct *old_rec,
                           dl_list_type       *record_list,
                           char               *reg_email));

/* given a record, add it to the database */
static int add_record PROTO((record_struct *rec));

/* given a list of records (to deal with multiple copies of the same
   record), delete the records from the database.  The update_soa flag
   to allow the caller to not update the SOA record if it is
   unnessary. */
static int del_record PROTO((dl_list_type *record_list, int update_soa));

/* given a list of old records to delete (usually just one, though),
   and a record to replace it with, modify (del-add) the record */
static int mod_record PROTO((record_struct *new_record,
                             dl_list_type  *old_record_list));


/* print the add response (the assigned values for ID and Updated) */
static int print_add_result PROTO((record_struct *rec));

/* ------------------- Local Functions -------------------- */


static int
get_real_object_from_anon(anon_rec, record_list)
  anon_record_struct *anon_rec;
  dl_list_type       *record_list;
{
  query_struct   *query;
  record_struct  *rec;
  av_pair_struct *av;
  char           *id;
  char           *updated;
  int            num_recs;
  int            not_done;
  ret_code_type  ret_code;
  
  /* First, establish that the anonymous record has the minimum
     attribute set to identify it */
  if (!get_id_and_updated_from_anon(anon_rec, &id, &updated))
  {
    if (!id)
    {
      print_error(MISSING_REQ_ATTRIB, "ID");
      log(L_LOG_NOTICE, CLIENT, "delete/mod object missing ID");
      return FALSE;
    }
    else if (! updated)
    {
      print_error(MISSING_REQ_ATTRIB, "Updated");
      log(L_LOG_NOTICE, CLIENT, "delete/mod object missing Updated");
      /* for now, this routine is use in del and mod
         operations, where missing 'Updated' violates the protocol
         spec */
      return FALSE;
    }
  }
  
  /* Find the record referred to by the deletion criteria (at
     minimum ID and Updated).  This will both establish that there is
     something to delete as well as get the actual record in hand */

  query = xcalloc(1, sizeof(*query));

  /* find the object(s) just by ID, so we can tell the difference
     between objects that just aren't there, and attempts to
     modify/delete objects that have been modified in the meantime. */
  if (!build_object_query(query, id, NULL))
  {
    print_error(UNIDENT_ERROR, "could not generate query to find object");
    log(L_LOG_NOTICE, CLIENT, "could not generate query to find object");
    return FALSE;
  }

  num_recs = search(query, record_list, get_max_hits_ceiling(), &ret_code);
  destroy_query(query);
  
  if (num_recs == 0 || dl_list_empty(record_list))
  {
    print_error(NO_OBJECT_FOUND, "");
    log(L_LOG_INFO, CLIENT, "get_real_object: no object found");
    return FALSE;
  }

  /* it is possible that the search returned more than one result.  If
     that is true, then there is a duplication of data in the database */
  if (num_recs > 1)
  {
    log(L_LOG_WARNING, DIRECTIVES, "multiple objects found for ID: %s",
        id);
  }

  /* remove objects that don't match the 'Updated' criteria */
  not_done = dl_list_first(record_list);
  while (not_done)
  {
    rec = dl_list_value(record_list);
    av = find_attr_in_record_by_name(rec, "Updated");
    if (!STR_EQ(updated, (char *)av->value))
    {
      dl_list_delete(record_list);
    }
    not_done = dl_list_next(record_list);

  }

  /* at this point, if the list was empty then all the objects were
     outdated. */
  if (dl_list_empty(record_list))
  {
    print_error(OUTDATED_OBJ, "");
    log(L_LOG_NOTICE, CLIENT, "attempt to update/delete outdated object %s",
        id);
    return FALSE;
  }
  
  return TRUE;
}

/* generates a search str and validates that no record exists for this
     auth area. Returns TRUE if success, FALSE if failure. */
static int
check_uniq_record(record)
  record_struct *record;
{
  query_struct  *query;
  dl_list_type  record_list;
  int           num_recs;
  ret_code_type ret_code;
  
  query = xcalloc(1, sizeof(*query));
  dl_list_default(&record_list, FALSE, destroy_record_data);
  
  if (!build_primary_key_query(query, record))
  {
    return FALSE;
  }

  /* empty query structs are OK (class had no keys, which is allowed) */
  if (!query->query_tree)
  {
    return TRUE;
  }
  
  num_recs = search(query, &record_list, 1, &ret_code);
  destroy_query(query);
  
  if (num_recs == 0 || dl_list_empty(&record_list))
  {
    return TRUE;
  }

  /* we found something, so we have a duplicate key */
  dl_list_destroy(&record_list);
  return FALSE;
}


/* makes sure that the new record can legitimately replace the new
   record (i.e., ID and updated fields match).  Note that this should
   probably make sure that the other primary keys match, too, but
   doesn't. */
static int
ensure_objects_match(new_record, old_record)
  record_struct *new_record;
  record_struct *old_record;
{
  av_pair_struct *new_av;
  av_pair_struct *old_av;

  log(L_LOG_INFO, UNKNOWN, "in ensure_objects_match()"); /* debug */
  if (!new_record || !old_record)
  {
    log(L_LOG_ERR, DIRECTIVES, "ensure_objects_match: null data detected");
    return FALSE;
  }

  /* First check the IDs */
  new_av = find_attr_in_record_by_name(new_record, "ID");
  old_av = find_attr_in_record_by_name(old_record, "ID");

  /* debug */
  if (new_av) {
    log(L_LOG_INFO, UNKNOWN, "ensure_objects_match: new id %s",
        (char *)new_av->value);
  }
  if (old_av) {
    log(L_LOG_INFO, UNKNOWN, "ensure_objects_match: old id %s",
        (char *)old_av->value);
  }
  /*end debug */
  
  /* don't need to check if the old record has an ID; that should have
     been checked much earlier */
  if (!new_av)
  {
    log(L_LOG_NOTICE, CLIENT,
        "modify object (new) missing ID; using ID from old");
    append_attribute_to_record(new_record, new_record->class, "ID",
                               old_av->value);
  }
  else if (!STR_EQ((char *)new_av->value, (char *)old_av->value))
  {
    print_error(INVALID_DIRECTIVE_PARAM, "ID's must match");
    log(L_LOG_NOTICE, CLIENT, "ensure_objects_match: ID's failed to match");
    return FALSE;
  }

  new_av = find_attr_in_record_by_name(new_record, "Updated");
  old_av = find_attr_in_record_by_name(old_record, "Updated");
  
  if (!new_av)
  {
    log(L_LOG_NOTICE, CLIENT,
        "modify object (new) missing updated; using old updated");
    append_attribute_to_record(new_record, new_record->class, "Updated",
                               old_av->value);
  }
  else if (!STR_EQ((char *)new_av->value, (char *)old_av->value))
  {
    print_error(OUTDATED_OBJ, "");
    log(L_LOG_NOTICE, CLIENT,
        "ensure_objects_match: new object was outdated");
    return FALSE;
  }

  return TRUE;
}

/* indexes the file from a new record structure. Returns TRUE if
     success, FALSE if failure. */
static int
index_new_record(record)
  record_struct *record;
{
  char             store_fname[MAX_FILE];
  dl_list_type     index_file_list;
  int              status;
  int              validate_flag;
  dl_list_type     dl_file_list;
  file_struct      *file_ptr;
  class_struct     *class;
  auth_area_struct *aa;
  FILE             *fp;

  validate_flag = encode_validate_flag(FALSE, TRUE, FALSE);

  class = record->class;
  aa = record->auth_area;
  
  /* generate the new filename */
  create_filename(store_fname, DB_FILE_TEMPLATE, class->db_dir);

  dl_list_default(&index_file_list, FALSE, destroy_index_fp_data);
  if (!build_index_list(class, aa, &index_file_list, class->db_dir,
                        "addind"))
  {
      log(L_LOG_ERR, MKDB,
          "index_files_by_name: could not generate list of index files");
    return FALSE;
  }

  /* open the DATA file and append the record to it. */
  strcat(store_fname, ".txt");
  fp = fopen(store_fname, "a");
  if (!fp)
  {
    log(L_LOG_ERR, DIRECTIVES, "failed to create data file '%s': %s",
        store_fname, strerror(errno));
    return FALSE;
  }
  mkdb_write_record(record, fp);
  fclose(fp);
  

  /* index the file */
  file_ptr = build_base_file_struct(store_fname, MKDB_DATA_FILE, 1);
  dl_list_default(&dl_file_list, FALSE, destroy_file_struct_data);
  dl_list_append(&dl_file_list, file_ptr);
  
  status = index_files(class, aa, &index_file_list, &dl_file_list,
                       validate_flag, FALSE);

  dl_list_destroy(&dl_file_list);
  dl_list_destroy(&index_file_list);

  return(status);
}



/* check the add record for validity */
static int
check_add(record_p, reg_email)
  record_struct **record_p;
  char          *reg_email;
{
  record_struct           *rec;
  ext_parse_response_type resp      = EXT_PARSE_OK;
  int                     validate_flag;
  
  if (!record_p || !*record_p || !(*record_p)->auth_area)
  {
    log(L_LOG_ERR, DIRECTIVES, "check_add: null data detected");
    return FALSE;
  }

  /* quiet mode OFF, protocol errors ON, find all flag OFF */
  validate_flag = encode_validate_flag(FALSE, TRUE, FALSE);
  rec = *record_p;

  /* check the authority area to see if we can modify it */
  if (rec->auth_area->type != AUTH_AREA_PRIMARY)
  {
    print_error(NOT_MASTER_AUTH_AREA, rec->auth_area->name);
    log(L_LOG_ERR, CLIENT, "check_add: not master auth area (%s)",
        rec->auth_area->name);
    return FALSE;
  }
  
  /* remove any IDs, as it *must* be assigned by the server */
  delete_attribute_from_record(rec, "ID");
  
  if (!check_record(rec, validate_flag))
  {
    return FALSE;
  }

  /* make sure that the record isn't duplicate */
  if (!check_uniq_record(rec))
  {
    print_error(NON_UNIQ_KEY, "");
    return FALSE;
  }
  
  /* possibly rewrite some guardian-specific parameters */
  transform_guardian_record(rec);

  /* if there is a external parser, run it */
  if (rec->class->parse_program && *rec->class->parse_program)
  {
    resp = run_external_parser(rec->class->parse_program,
                               ADD, reg_email,
                               NULL, record_p);
    if (resp != EXT_PARSE_OK)
    {
      /* external parse routine should have already issued the error
         code */
      return FALSE;
    }
  }

  return TRUE;
}

/* check the mod records for validity */
static int
check_mod(new_record_p, old_record_p, old_record_list, reg_email)
  record_struct      **new_record_p;
  anon_record_struct **old_record_p;
  dl_list_type       *old_record_list;
  char               *reg_email;
{
  int                     validate_flag;
  record_struct           *new_rec;
  record_struct           *old_rec;
  anon_record_struct      *old_anon_rec;
  ext_parse_response_type resp = EXT_PARSE_OK;
  
  
  /* quiet mode OFF, protocol errors ON, find all errors OFF */
  validate_flag = encode_validate_flag(FALSE, TRUE, FALSE);
  
  if (!new_record_p || !*new_record_p || !old_record_p || !*old_record_p)
  {
    log(L_LOG_ERR, DIRECTIVES, "check_mod: null data detected");
    return FALSE;
  }

  new_rec = *new_record_p; old_anon_rec = *old_record_p;

  /* make sure we can modify in this auth area */
  if (new_rec->auth_area->type != AUTH_AREA_PRIMARY)
  {
    print_error(NOT_MASTER_AUTH_AREA, new_rec->auth_area->name);
    log(L_LOG_NOTICE, CLIENT,
        "check_mod: cannot modify in secondary auth-area (%s)",
        new_rec->auth_area->name);
    return FALSE;
  }

  /* get the actual old record */
  if (! get_real_object_from_anon(old_anon_rec, old_record_list))
  {
    return FALSE;
  }

  if (!old_record_list || dl_list_empty(old_record_list))
  {
    return FALSE;
  }
  
  /* get the first record in the list */
  dl_list_first(old_record_list);
  old_rec = dl_list_value(old_record_list);

  /* validate new record first before calling delete */
  if (!check_record(new_rec, validate_flag) || 
      !ensure_objects_match(new_rec, old_rec))
  {
    dl_list_destroy(old_record_list);
    return FALSE;
  }

  /* now that we've determined that there is something to mod, lets
     check for permission, shall we? (we could just let the delete
     action handle it but for the external parse step) */
  if (!check_guardian(old_rec))
  {
    print_error(UNAUTH_REGIST, "");
    dl_list_destroy(old_record_list);
    return FALSE;
  }

  /* possibly rewrite some guardian-specific parameters; if the
     guard-scheme and guard-info parameters haven't changed, don't
     transform them.  This protects us from possibly encrypting a
     password twice, or something. */
  if (!compare_record_attr_by_name(old_rec, new_rec, "Guard-Scheme") ||
      !compare_record_attr_by_name(old_rec, new_rec, "Guard-Info"))
  {
    transform_guardian_record(new_rec);
  }
  
  if (new_rec->class->parse_program && *new_rec->class->parse_program)
  {
    resp = run_external_parser(new_rec->class->parse_program, MOD,
                               reg_email, old_rec, new_record_p);
  }

  if (resp != EXT_PARSE_OK)
  {
    return FALSE;
  }

  return TRUE;
}

/* checks the delete record for validity (i.e., does the record
   exists, do you have permission to delete it, etc.). Returns TRUE if
   deletion is valid.  Also fills 'record_list' with the actual
   record(s) matching the delete criteria. */
static int
check_del(old_rec, record_list, reg_email)
  anon_record_struct *old_rec;
  dl_list_type       *record_list;
  char               *reg_email;
{
  record_struct           *rec;
  ext_parse_response_type resp = EXT_PARSE_OK;

  if (! get_real_object_from_anon(old_rec, record_list))
  {
    /* logging done internal to the routine */
    return FALSE;
  }

  if (!record_list || dl_list_empty(record_list))
  {
    return FALSE;
  }
  
  /* we will perform the remaining checks on the first in the list,
     and assume, because getting more than one in a database error,
     that the others are equivalent */
  dl_list_first(record_list);
  rec = dl_list_value(record_list);

  /* check to see if we can work on this auth-area at all */
  if (rec->auth_area->type != AUTH_AREA_PRIMARY)
  {
    print_error(NOT_MASTER_AUTH_AREA, rec->auth_area->name);
    dl_list_destroy(record_list);
    return FALSE;
  }
  
  /* now that we've determined that there is something to delete, lets
     check for permission, shall we? */
  if (!check_guardian(rec))
  {
    print_error(UNAUTH_REGIST, "");
    dl_list_destroy(record_list);
    return FALSE;
  }

  /* if update_soa is FALSE, this is being called as part of a mod
     operation, thus the external parser would have already been
     run. */
  if (rec->class->parse_program && *rec->class->parse_program)
  {
    resp = run_external_parser(rec->class->parse_program, DEL,
                               reg_email, rec, NULL);
  }     

  if (resp != EXT_PARSE_OK)
  {
    dl_list_destroy(record_list);
    return FALSE;
  }

  return TRUE;
}


static int 
add_record(rec)
  record_struct  *rec;
{
  av_pair_struct *av;
  int            status;
  char           *id;
  char           *updated;

  if (!rec)
  {
    log(L_LOG_ERR, DIRECTIVES, "add_record: null data detected");
    return FALSE;
  }
  
  /* Since this action used both in add and mod operations, only add
     an ID if there isn't one */
  av = find_attr_in_record_by_name(rec, "ID");
  if (!av)
  {
    id = generate_id(rec->auth_area->name);
    append_attribute_to_record(rec, rec->class, "ID", id);
  }

  /* create/replace the 'Updated' attribute in the object itself, and
     update the SOA record */
  updated = generate_updated();
  set_updated_attr(rec, updated);
  update_soa_record(rec->auth_area, updated);
  free(updated);
  
  status = index_new_record(rec);

  return(status);
}


/* deletes a record from a file.  Returns TRUE if success, FALSE if
     failure */
static int
del_record(record_list, update_soa)
  dl_list_type *record_list;
  int          update_soa;
{
  auth_area_struct  *aa;
  record_struct     *rec;
  char              *updated;
  
  if (dl_list_empty(record_list))
  {
    return TRUE;
  }

  dl_list_first(record_list);
  rec = dl_list_value(record_list);
  aa = rec->auth_area;
  
  mkdb_delete_record_list(record_list);
  
  dl_list_destroy(record_list);

  if (update_soa)
  {
    updated = generate_updated();
    update_soa_record(aa, updated);
    free(updated);
  }

  return TRUE;
}

/* modifies a record from a file. Returns TRUE if success, FALSE if
     failure */
static int 
mod_record(new_record, old_record_list)
  record_struct    *new_record;
  dl_list_type     *old_record_list;
{
  char *updated_str;
  
  /* call delete record, if cool, call add record delete record, if
     cool, call add record delete record, if cool, call add record */
  if (!del_record(old_record_list, FALSE))
  {
    return FALSE;
  }

  /* deal with the 'Updated' attribute and SOA */
  updated_str = generate_updated();
  set_updated_attr(new_record, updated_str);
  update_soa_record(new_record->auth_area, updated_str);
  free(updated_str);
  
  return(add_record(new_record));
}

static int
print_add_result(rec)
  record_struct  *rec;
{
  av_pair_struct *av;
  char           *id;
  char           *updated;
  
  if (!rec) return FALSE;

  av = find_attr_in_record_by_name(rec, "ID");
  if (!av || NOT_STR_EXISTS((char *)av->value)) return FALSE;
  id = (char *)av->value;

  av = find_attr_in_record_by_name(rec, "Updated");
  if (!av || NOT_STR_EXISTS((char *)av->value)) return FALSE;
  updated = (char *)av->value;

  
  print_response(RESP_REGISTER, "ID: %s", id);
  print_response(RESP_REGISTER, "Updated: %s", updated);

  return TRUE;
}

/* ------------------- Public Functions -------------------- */

int
process_registration(reg_email, action)
  char                 *reg_email;
  register_action_type action;
{
  FILE                 *spool_fp;
  anon_record_struct   *old_rec = NULL;
  record_struct        *new_rec = NULL;
  dl_list_type         del_record_list;
  int                  status;
  
  /* READ phase -- read the spool file into the appropriate data structures.
     some contextual syntax checking is done in this phase */

  if (! (spool_fp = open_spool_file("r")) )
  {
    print_error(UNIDENT_ERROR, "");
    return FALSE;
  }

  switch (action)
  {
  case ADD:
    status = read_add_spool(spool_fp, &new_rec);
    break;
  case MOD:
    status = read_mod_spool(spool_fp, &new_rec, &old_rec);
    break;
  case DEL:
    status = read_del_spool(spool_fp, &old_rec);
    break;
  default:
    log(L_LOG_WARNING, DIRECTIVES, "unknown action: %s",
        action_to_string(action));
    status = FALSE;
    break;
  }

  close_spool_file();

  if (!status)
  {
    if (new_rec) destroy_record_data(new_rec);
    if (old_rec) destroy_anon_record_data(old_rec);
    return FALSE;
  }
  /* CHECK phase -- check for syntactical and usage problems */

  dl_list_default(&del_record_list, FALSE, destroy_record_data);
  
  switch (action)
  {
  case ADD:
    status = check_add(&new_rec, reg_email);
    break;
  case MOD:
    status = check_mod(&new_rec, &old_rec, &del_record_list, reg_email);
    break;
  case DEL:
    status = check_del(old_rec, &del_record_list, reg_email);
    break;
  default:
    break;
  }
  
  if (!status)
  {
    if (new_rec) destroy_record_data(new_rec);
    if (old_rec) destroy_anon_record_data(old_rec);
    dl_list_destroy(&del_record_list);
    return FALSE;
  }     
  
  /* COMMIT phase -- apply the changes to the database */

  switch (action)
  {
  case ADD:
    status = add_record(new_rec);
    if (status)
    {
      status = print_add_result(new_rec);
    }
    break;
  case MOD:
    status = mod_record(new_rec, &del_record_list);
    break;
  case DEL:
    status = del_record(&del_record_list, TRUE);
    break;
  default:
    break;
  }

  if (new_rec) destroy_record_data(new_rec);
  if (old_rec) destroy_anon_record_data(old_rec);
  dl_list_destroy(&del_record_list);
  
  if (!status) return FALSE;
  
  return TRUE;
}
