/*
 * @(#) $Header: /home/stud/admin/cvs/mysql-admutils/mysql-dbadm.c,v 1.20 2007/06/04 08:40:54 geirha Exp $
 *
 * mysql-dbadm.c
 *
 */ 

#include "config.h"
#include "mysql-admutils.h"

#include <stdio.h>
#include <string.h>
#include <mysql.h>
#include <assert.h>
#include <stdarg.h>
#include <stdlib.h>
#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include <unistd.h>

/* New database names may only use these characters in their identifier */
const char dbname_validchars[] =
  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";


/* Returns true if dbname contains only characters in dbname_validchars. */
int dbname_isclean(char* dbname) {
  int reallen, cleanlen;
  reallen = strlen(dbname);
  cleanlen = strspn(dbname, dbname_validchars);
  return (reallen == cleanlen);
}

char *
strchr_whitespace(const char *s)
{
  char *sp, *tab;
  sp = strchr(s, ' ');
  tab = strchr(s, '\t');
  if (sp == NULL)
    return tab;
  if (tab == NULL)
    return sp;
  return sp < tab ? sp : tab; /* if both are found, return the first one */
}

char *
strtok_whitespace(char *s)
{
  static char *cp;
  char *r; 

  r = s ? s : cp;
  if (r == NULL)
    return NULL;
  cp = strchr_whitespace(r);
  if (cp == NULL)
    return r;
  while ((*cp == ' ') || (*cp == '\t')) 
    *cp++ = '\0';
  return r;
}


int
valid_priv(const char *s)
{
  if (s == NULL) return 0;
#define ACCEPT(x) if (strcmp(s, x) == 0) return 1
  ACCEPT("Y");
  ACCEPT("N");
  ACCEPT("y");
  ACCEPT("n");
#undef ACCEPT
  return 0; /* not a valid priv */
}


int
usage()
{
  printf("Usage: %s COMMAND [DATABASE]...\n", program_name);
  printf("Create, drop og edit permission for the DATABASE(s),\n");
  printf("as determined by the COMMAND.  Valid COMMANDs:\n");
  printf("\n");
  printf("  create     create the DATABASE(s).\n");
  printf("  drop       delete the DATABASE(s).\n");
  printf("  show       give information about the DATABASE(s), or, if\n");
  printf("             none are given, all the ones you own.\n");
  printf("  editperm   change permissions for the DATABASE(s).  Your\n");
  printf("             favorite editor will be started, allowing you\n");
  printf("             to make changes to the permission table.\n");
  printf("             Run '%s --help-editperm' for more\n"
         "             information.\n",program_name);
  printf("\n");
  printf("Report bugs to drift@stud.ntnu.no\n");
  return 0;
}

int 
usage_editperm()
{
  printf("Usage: %s editperm [DATABASE]\n", program_name);
  printf("Edit permissions for the DATABASE. Running this command will\n");
  printf("spawn the editor stored in the $EDITOR environment variable.\n");
  printf("(pico will be used if the variable is unset)\n");
  printf("\n");
  printf("The file should contain one line per user, starting with the\n");
  printf("username and followed by ten Y/N-values seperated by whitespace.\n");
  printf("Lines starting with # are ignored.\n");
  printf("\n");
  printf("The Y/N-values corresponds to the following mysql privileges:\n");
  printf("  Select - Enables use of SELECT\n");
  printf("  Insert - Enables use of INSERT\n");
  printf("  Update - Enables use of UPDATE\n");
  printf("  Delete - Enables use of DELETE\n");
  printf("  Create - Enables use of CREATE TABLE\n");
  printf("  Drop   - Enables use of DROP TABLE\n");
  printf("  Alter  - Enables use of ALTER TABLE\n");
  printf("  Index  - Enables use of CREATE INDEX and DROP INDEX\n");
  printf("  Temp   - Enables use of CREATE TEMPORARY TABLE\n");
  printf("  Lock   - Enables use of LOCK TABLE\n");
  printf("\n");
  printf("Report bugs to drift@stud.ntnu.no\n");
  return 0;
}


int
create(MYSQL *pmysql, char *db)
{
  // hvis man fors�ker � dra mysql_create_db() p� en database som
  // allerede finnes, s� henger bare hele sql-kallet. Vi m� derfor
  // fors�ke � selecte databasen f�rst. Ettersom man alltid er root,
  // g�r jo dette vanligvis bra.
  // finner ut om denne finnes fra f�r.
  if (!mysql_select_db(pmysql, db)) {
    return dberror(pmysql, "Database '%s' already exists.", db);
  } 
  mysql_select_db(pmysql, "mysql");
  // oppretter databasen.
  char query[1024], *end;
  end = strmov(query, "CREATE DATABASE `");
  end += mysql_real_escape_string(pmysql, end, db, strlen(db));
  *end++ = '`';
  *end = '\0';
#ifdef DEBUG
  printf("query: %s\n", query);
#endif
  if (mysql_query(pmysql, query))
    return dberror(pmysql, "Cannot create database '%s'.", db);
  fprintf(stderr, "Database '%s' created.\n", db);
  return 0;
}


int
drop(MYSQL *pmysql, char *db)
{
  char query[1024], *end;

  end = strmov(query, "DELETE FROM db WHERE db = '");
  end += mysql_real_escape_string(pmysql, end, db, strlen(db));
  *end++ = '\'';
  *end = '\0';

#ifdef DEBUG
  printf("query: %s\n", query);
#endif
  if (mysql_query(pmysql, query))
    dberror(pmysql, "Failed to delete permissions for database '%s'.", db);

  if (mysql_select_db(pmysql, db)) {
    dberror(pmysql, "Database '%s' doesn't exists.", db);
    return 0;
  } 
  mysql_select_db(pmysql, "mysql");

  end = strmov(query, "DROP DATABASE `");
  end += mysql_real_escape_string(pmysql, end, db, strlen(db));
  *end++ = '`';
  *end = '\0';

#ifdef DEBUG
  printf("query: %s\n", query);
#endif
  if (mysql_query(pmysql, query))
    return dberror(pmysql, "Cannot drop database '%s'.", db);

  fprintf(stderr, "Database '%s' dropped.\n", db);
  return 0;
}

/* return a list of the user's databases */
char **
list(MYSQL *pmysql)
{
  char *wild;
  char **usr_groups, **cp;  /* holds all group names this dude is a member of [tlan]*/
  MYSQL_RES *res;
  int rows, numgroups, numgroupdbs;
  MYSQL_ROW row;
  char **dblist;
  int i, counter;
  struct passwd *p;

  // variabler som brukes til � escape gruppenavnet til mysqlvennlig format.
  char escaped_user[64];
  char *cp_kopi;

  p = getpwuid(getuid());
  if (!p) {
    dberror(NULL, "Failed to lookup your UNIX username.");
    exit(1);
  }

  dblist = malloc(2 * sizeof(char*)); /* one for username (if used), rest is done below */

  numgroupdbs = 0;
  counter = 0;

  usr_groups = get_group_names(&numgroups);
  cp = usr_groups;
  while (*cp) { // itererer over alle grupper en person er med i
    if (*cp == NULL)
      break;

#ifdef DEBUG
    printf("cp er %s\n", *cp);
#endif

    escaped_user[0] = '\0';
    cp_kopi=*cp;

// itererer over bokstavene i gruppenavn, og escaper spesialtegn.
    for(i=0; i<=strlen(cp_kopi); i++) {
  // hvis % _ , s� skriv \ og s� tegn
      if ((cp_kopi[i] == '_') || (cp_kopi[i] == '%')) {
        strcat(escaped_user, "\\");
      }

      escaped_user[strlen(escaped_user) + 1] = '\0';
      escaped_user[strlen(escaped_user)] = cp_kopi[i];
    } // for
    
    wild = malloc(strlen(escaped_user)+4);
    sprintf(wild, "%s\\_%%", escaped_user);

#ifdef DEBUG
    printf("dbadm: wildcard: '%s'\n", wild);
#endif

    res = mysql_list_dbs(pmysql, wild);
    rows = mysql_num_rows(res);

    if (rows > 0) {
      numgroupdbs += rows;

      dblist = realloc(dblist, (numgroupdbs+2) * sizeof(char *));

      for (i = 0; i < rows; i++) {
        if ((row = mysql_fetch_row(res))) {
          dblist[counter++] = strdup(row[0]);
        }
      }
    }

    
    free(wild);
    mysql_free_result(res);
    cp++;
  }

  wild = malloc(strlen(p->pw_name) + 4);
  sprintf(wild, "%s\\_%%", p->pw_name);
#ifdef DEBUG
  printf("dbadm: wildcard: '%s'\n", wild);
#endif

  res = mysql_list_dbs(pmysql, wild);
  rows = mysql_num_rows(res);
  dblist = realloc(dblist, (numgroupdbs+rows+2) * sizeof(char *)); 

  if (!dblist) {
    dberror(NULL, "Out of memory.\n");
    free(wild);
    return NULL;
  }

  for (i = 0; i < rows; i++) {
    if ((row = mysql_fetch_row(res))) {
      dblist[counter++] = strdup(row[0]);
    }
  }
  mysql_free_result(res);

  res = mysql_list_dbs(pmysql, p->pw_name);
  rows = mysql_num_rows(res);
  if (rows == 1)
    dblist[counter++] = strdup(p->pw_name);

  dblist[counter] = NULL;
  
  mysql_free_result(res);
  free(wild);
  for (i=0;i<numgroups;i++)
    free(usr_groups[i]);
  free(usr_groups);
  return dblist;
}


int
writeperm(FILE *f, MYSQL *pmysql, const char *db)
{
  char query[2048], *end;
  MYSQL_RES *res;
  int rows, i;
  MYSQL_ROW row;

  end = strmov(query, "SELECT user,select_priv,insert_priv,update_priv,"
                             "delete_priv,create_priv,drop_priv,"
                             "alter_priv,index_priv,"
                             "create_tmp_table_priv,lock_tables_priv "
                      "FROM db WHERE db = '");
  end += mysql_real_escape_string(pmysql, end, db, strlen(db));
  *end++ = '\'';
  *end = '\0';

#ifdef DEBUG
  printf("query: %s\n", query);
#endif
  if (mysql_query(pmysql, query))
    return dberror(pmysql, "Query for permissions failed.");
  res = mysql_store_result(pmysql);
  rows = mysql_num_rows(res);
  fprintf(f, "# User                " 
    "Select  Insert  Update  Delete  Create   Drop   Alter   Index   "
    " Temp    Lock\n");
  fprintf(f, "# ----------------    "
    "------  ------  ------  ------  ------   ----   -----   -----   "
    " ----    ----\n");

  if (rows == 0)
    fprintf(f, "# (no permissions currently granted to any users)\n");
  else {
    for (i = 0; i < rows; i++) {
      row = mysql_fetch_row(res);
      fprintf(f, "  %-16s      %-7s %-7s %-7s %-7s %-7s %-7s %-7s %-7s %-7s %s\n",
      row[0], row[1],  row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9], row[10]);
    }
  }
  mysql_free_result(res);
  return 0;
}


/* show information about the database */
int
show(MYSQL *pmysql, const char *db)
{
  printf("Database '%s':\n", db);

  writeperm(stdout, pmysql, db);
  return 0;
}


#define MAX_GRANTS 1024


int
editperm(MYSQL *pmysql, const char *db)
{
  char fn[] = "/tmp/mysql-dbadm.tmp.XXXXXX";
  FILE *f;
  char *editor;
  char cmd[1024]; /* shell command for editing the file */
  char line[1024]; /* buffer to hold one line */
  char *cp; /* used to interate through a line */
  char *user, *select_priv, *insert_priv, *update_priv, *delete_priv, 
    *create_priv, *drop_priv, *alter_priv, *index_priv, *create_tmp_table_priv,
    *lock_tables_priv;
  char query[4096], *end; /* used to build a query */
  char *queries[MAX_GRANTS]; /* insert queries */
  int lines; /* number of grant lines processed */
  int i; /* iterate through lines[] */

  int fd = mkstemp(fn);

  if (fd == -1)
    return dberror(NULL, "Cannot create a unique temporary file name.");

  f = fdopen(fd, "w");
  if (f == NULL)
    return dberror(NULL, "Failed to open temporary file %s.", fn);
  writeperm(f, pmysql, db);
  fclose(f);

  editor = getenv("EDITOR");
  if (!editor)
    editor = "pico"; /* OK since editor won't be freed */
  strcpy(cmd, editor);
  strcat(cmd, " ");
  strcat(cmd, fn);
  if (system(cmd) == -1) {
    dberror(NULL, "Failed to execute '%s'\n", cmd);
    perror("system");
    return 1;
  }

  /* parse */
  f = fopen(fn, "r");
  lines = 0;
  while (fgets(line, sizeof(line), f)) {
    cp = strchr(line, '\n');
    if (cp)
      *cp = '\0';

    cp = line;
    while (*cp && ((*cp == ' ') || (*cp == '\t'))) cp++;
    if (*cp == '\0') continue;
    if (*cp == '#') continue;
    if (*cp == '\n') continue;

    #define STRTOK_WHITESPACE(res, start) \
    {  if (!(res = strtok_whitespace(start))) continue; }

    STRTOK_WHITESPACE(user, cp);
    if (strlen(user) < 1)
      return dberror(NULL, "Invalid user '%s' in grant line %d.", user, 
           lines + 1);
    if (strcmp(user, "%") == 0)
      *user = '\0'; /* ugly, but it works... */

      #define CHECK_PRIV(PRIV) \
      if (!valid_priv(PRIV)) return dberror(NULL, "Invalid value '%s' " \
      "in grant line %d.\nMake sure you fill in a value (Y or N) for " \
      "all ten privileges.\nRun '%s --help-editerm' for more " \
      "information.\nNo permissions have been set.", PRIV, lines + 1, \
      program_name)

    STRTOK_WHITESPACE(select_priv, NULL);
    CHECK_PRIV(select_priv);
    STRTOK_WHITESPACE(insert_priv, NULL);
    CHECK_PRIV(insert_priv);
    STRTOK_WHITESPACE(update_priv, NULL);
    CHECK_PRIV(update_priv);
    STRTOK_WHITESPACE(delete_priv, NULL);
    CHECK_PRIV(delete_priv);
    STRTOK_WHITESPACE(create_priv, NULL);
    CHECK_PRIV(create_priv);
    STRTOK_WHITESPACE(drop_priv, NULL);
    CHECK_PRIV(drop_priv);
    STRTOK_WHITESPACE(alter_priv, NULL);
    CHECK_PRIV(alter_priv);
    STRTOK_WHITESPACE(index_priv, NULL);
    CHECK_PRIV(index_priv);
    STRTOK_WHITESPACE(create_tmp_table_priv, NULL);
    CHECK_PRIV(create_tmp_table_priv);
    STRTOK_WHITESPACE(lock_tables_priv, NULL);
    CHECK_PRIV(lock_tables_priv);

      #undef STRTOK_WHITESPACE
      #undef CHECK_PRIV

    end = strmov(query, "INSERT INTO db ("
                            "host,db,user,select_priv,insert_priv,"
                            "update_priv,delete_priv,create_priv,"
                            "drop_priv,alter_priv,index_priv,"
                            "create_tmp_table_priv,lock_tables_priv"
                        ") VALUES (");

    end = strmov(end, "'%'");

    #define APPEND(VAR) {\
    *end++ = ',';\
    *end++ = '\'';\
    end += mysql_real_escape_string(pmysql, end, VAR, strlen(VAR));\
    *end++ = '\'';\
    }

    APPEND(db);
    APPEND(user);
    APPEND(select_priv);
    APPEND(insert_priv);
    APPEND(update_priv);
    APPEND(delete_priv);
    APPEND(create_priv);
    APPEND(drop_priv);
    APPEND(alter_priv);
    APPEND(index_priv);
    APPEND(create_tmp_table_priv);
    APPEND(lock_tables_priv);
    *end++ = ')';
    *end = '\0';

    #undef APPEND
    
    queries[lines] = strdup(query);
    lines++;
    if (lines >= MAX_GRANTS)
    {
      dberror(NULL, "Warning: Maximum of %d grants reached.\n", 
        MAX_GRANTS);
      continue;
    }
  } /* while fgets ... */
  
  unlink(fn);
  fclose(f);

  /* now that we have checked the input for errors, we can safely
     delete the old grants from the database and insert the new ones. */

  end = strmov(query, "DELETE FROM db WHERE db = '");
  end += mysql_real_escape_string(pmysql, end, db, strlen(db));
  *end++ = '\'';
  *end = '\0';
#ifdef DEBUG
  printf("query: %s\n", query);
#endif
  if (mysql_query(pmysql, query))
    dberror(pmysql, "Failed to delete old grants for '%s'.", db);

  for (i = 0; i < lines; i++)
  {
#ifdef DEBUG
    puts(queries[i]);
    putchar('\n');
#endif
    if (mysql_query(pmysql, queries[i]))
      dberror(pmysql, "Failed to insert grant line %d.", i + 1);
    free(queries[i]);
  }
  
  return 0;
}


int
main(int argc, char *argv[])
{
  int i;
  enum { c_create, c_drop, c_editperm, c_show } command;
  MYSQL mysql;
  mysql_init(&mysql);
  char **dblist, **p;
  char *db;

  program_name = argv[0];

  for (i = 1; i < argc; i++)
    if (strcmp(argv[i], "--help") == 0)
      return usage();
  for (i = 1; i < argc; i++)
    if (strcmp(argv[i], "--help-editperm") == 0)
      return usage_editperm();
  for (i = 1; i < argc; i++)
    if (strcmp(argv[i], "--version") == 0)
      return version();
  
  if (argc < 2)
    return wrong_use(NULL);

#ifdef DEBUG
  printf("NB NB NB: denne versjonen av programmet er kompilert med -DDEBUG, og\n");
  printf("kan komme til � skrive ut ekstra informasjon. Dette er ikke farlig,\n");
  printf("og programmet b�r virke som vanlig.\n");
#endif


  /* check that the supplied command is valid */

  if (strcmp(argv[1], "create") == 0)
    command = c_create;
  else if (strcmp(argv[1], "drop") == 0)
    command = c_drop;
  else if (strcmp(argv[1], "editperm") == 0)
    command = c_editperm;
  else if (strcmp(argv[1], "show") == 0)
    command = c_show;
  else
    return wrong_use("unrecognized command"); /* XXX */

  /* all other than show requires at least one DATABASE argument. */
  if ((command != c_show) && (argc < 3))
    return wrong_use(NULL);

  read_config_file();

  /* connect to the database server and select the mysql database */
  if (!mysql_real_connect(&mysql, db_server, db_user, db_passwd, db_name, 0, NULL, 0))
    return dberror(&mysql, "Cannot connect to database server '%s'.", 
       db_server);
  if (mysql_select_db(&mysql, db_name))
    return dberror(&mysql, "Cannot select database '%s'.", db_name);

  if ((command == c_show) && (argc == 2)) {
    dblist = list(&mysql);
    p = dblist;
    while (*p) {
      show(&mysql, *p);
      free(*p);
      p++;
    }
    free(*p);
    free(dblist);
  }
  else {
    db = malloc(64); 
    /* for each supplied database name, perform the requested action */
    for (i = 2; i < argc; i++) {
      // HE HE
      strncpy(db, argv[i], 32);
      db[32] = '\0';

      if (! (owner(db) || member(db))) {
          if (command == c_create)
              dberror(NULL,"Unable to create mysql-database '%s'.\n"
                 "A mysql-database must start with either '%s_' or "
                 "'groupname_', where groupname is a unix group you are a "
                 "member of. Type \"groups\" to see which groups you are a "
                 "member of.\n",
                 db, getpwuid(getuid())->pw_name);
          else
              dberror(NULL, "You are not in charge of mysql-database: '%s'.  Skipping.", db);
          continue;
      }

      switch (command) {
        case c_create:
        // We only check newly created databases. Many old ("unclean") databases are still in use.
          if(dbname_isclean(db)) {
            create(&mysql, db);
          } else {
            dberror(NULL, "Database name '%s' contains invalid characters.\n"
                        "Only A-Z, a-z, 0-9, _ (underscore) and - (dash) permitted. Skipping.", db);
          }
          break;
        case c_drop:
          if(dbname_isclean(db)) {
            drop(&mysql, db);
          } else {
            dberror(NULL, "Database name '%s' contains invalid characters.\n"
                        "Only A-Z, a-z, 0-9, _ (underscore) and - (dash) permitted. Skipping.", db);
          }
          break;
        case c_editperm:
          if(dbname_isclean(db)) {
            editperm(&mysql, db);
          } else {
            dberror(NULL, "Database name '%s' contains invalid characters.\n"
                        "Only A-Z, a-z, 0-9, _ (underscore) and - (dash) permitted. Skipping.", db);
          }
          break;
        case c_show:
          if(dbname_isclean(db)) {
            show(&mysql, db);
          } else {
            dberror(NULL, "Database name '%s' contains invalid characters.\n"
                        "Only A-Z, a-z, 0-9, _ (underscore) and - (dash) permitted. Skipping.", db);
          }
          break;
        default:
          return dberror(NULL, "This point should never be reached!");
      }
    } // for
    free(db);
  }  // else

  mysql_reload(&mysql);
  mysql_close(&mysql);

  return 0;
}