/*
 * Tool to edit quota limits via editor
 */
#include "config.h"

/*
 * Disk quota editor.
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <pwd.h>
#include <grp.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <paths.h>
#include <stdlib.h>
#include <fcntl.h>
#include <getopt.h>
#include <libgen.h>

#include "pot.h"
#include "quotaops.h"
#include "quotasys.h"
#include "quotaio.h"
#include "common.h"

#define FL_EDIT_PERIOD 1
#define FL_EDIT_TIMES 2
#define FL_REMOTE 4
#define FL_NUMNAMES 8
#define FL_NO_MIXED_PATHS 16

char *progname;

static int flags, quotatype;
static int fmt = -1;
static char *protoname;
static char *dir_name;

static void usage(void)
{
#if defined(RPC_SETQUOTA)
	char *rpcflag = "[-rm] ";
#else
	char *rpcflag = "";
#endif
	errstr(_("Usage:\n\tedquota %1$s[-u] [-F formatname] [-p username] [-f filesystem] username ...\n\
\tedquota %1$s-g [-F formatname] [-p groupname] [-f filesystem] groupname ...\n\
\tedquota %1$s-P [-F formatname] [-p projectname] [-f filesystem] projectname ...\n\
\tedquota [-u|g|-P] [-F formatname] [-f filesystem] -t\n\
\tedquota [-u|g|-P] [-F formatname] [-f filesystem] -T username|groupname|projectname ...\n"), rpcflag);
	fputs(_("\n\
-u, --user                    edit user data\n\
-g, --group                   edit group data\n\
-P, --project                 edit project data\n\
"), stderr);
#if defined(RPC_SETQUOTA)
	fputs(_("-r, --remote                  edit remote quota (via RPC)\n\
-m, --no-mixed-pathnames      trim leading slashes from NFSv4 mountpoints\n"), stderr);
#endif
	fputs(_("-F, --format=formatname       edit quotas of a specific format\n\
-p, --prototype=name          copy data from a prototype user/group\n\
    --always-resolve          always try to resolve name, even if it is\n\
                              composed only of digits\n\
-f, --filesystem=filesystem   edit data only on a specific filesystem\n\
-t, --edit-period             edit grace period\n\
-T, --edit-times              edit grace time of a user/group\n\
-h, --help                    display this help text and exit\n\
-V, --version                 display version information and exit\n\n"), stderr);
	fprintf(stderr, _("Bugs to: %s\n"), PACKAGE_BUGREPORT);
	exit(1);
}

static int parse_options(int argc, char **argv)
{
	int ret;
	struct option long_opts[] = {
		{ "help", 0, NULL, 'h' },
		{ "version", 0, NULL, 'V' },
		{ "prototype", 1, NULL, 'p' },
		{ "user", 0, NULL, 'u' },
		{ "group", 0, NULL, 'g' },
		{ "project", 0, NULL, 'P' },
		{ "format", 1, NULL, 'F' },
		{ "filesystem", 1, NULL, 'f' },
#if defined(RPC_SETQUOTA)
		{ "remote", 0, NULL, 'r' },
		{ "no-mixed-pathnames", 0, NULL, 'm' },
#endif
		{ "always-resolve", 0, NULL, 256 },
		{ "edit-period", 0, NULL, 't' },
		{ "edit-times", 0, NULL, 'T' },
		{ NULL, 0, NULL, 0 }
	};

	if (argc < 2)
		usage();

	quotatype = USRQUOTA;
#if defined(RPC_SETQUOTA)
	while ((ret = getopt_long(argc, argv, "ugPhrmntTVp:F:f:", long_opts, NULL)) != -1) {
#else
	while ((ret = getopt_long(argc, argv, "ugPhtTVp:F:f:", long_opts, NULL)) != -1) {
#endif
		switch (ret) {
		  case 'p':
			  protoname = optarg;
			  break;
		  case 'g':
			  quotatype = GRPQUOTA;
			  break;
		  case 'P':
			  quotatype = PRJQUOTA;
			  break;
#if defined(RPC_SETQUOTA)
		  case 'n':
		  case 'r':
			  flags |= FL_REMOTE;
			  break;
		  case 'm':
			  flags |= FL_NO_MIXED_PATHS;
			  break;
#endif
		  case 'u':
			  quotatype = USRQUOTA;
			  break;
		  case 't':
			  flags |= FL_EDIT_PERIOD;
			  break;
		  case 'T':
			  flags |= FL_EDIT_TIMES;
			  break;
		  case 'F':
			  if ((fmt = name2fmt(optarg)) == QF_ERROR)	/* Error? */
				  exit(1);
			  break;
		  case 'f':
			  dir_name = optarg;
			  break;
		  case 256:
			  flags |= FL_NUMNAMES;
			  break;
		  case 'V':
			  version();
			  exit(0);
		  case 'h':
		  default:
			  usage();
		}
	}
	argc -= optind;

	if (((flags & FL_EDIT_PERIOD) && argc != 0) || ((flags & FL_EDIT_TIMES) && argc < 1))
		usage();
	if ((flags & (FL_EDIT_PERIOD | FL_EDIT_TIMES)) && protoname) {
		errstr(_("Prototype name does not make sense when editing grace period or times.\n"));
		usage();
	}
	if (flags & FL_REMOTE && (flags & (FL_EDIT_TIMES | FL_EDIT_PERIOD))) {
		errstr(_("Cannot change grace times over RPC protocol.\n"));
		usage();
	}
	return optind;
}

static void copy_prototype(int argc, char **argv, struct quota_handle **handles)
{
	int ret, protoid, id;
	struct dquot *protoprivs, *curprivs, *pprivs, *cprivs;
	
	ret = 0;
	protoid = name2id(protoname, quotatype, !!(flags & FL_NUMNAMES), NULL);
	protoprivs = getprivs(protoid, handles, 0);
	while (argc-- > 0) {
		id = name2id(*argv, quotatype, !!(flags & FL_NUMNAMES), NULL);
		curprivs = getprivs(id, handles, !dir_name);
		if (!curprivs)
			die(1, _("Cannot get quota information for user %s\n"), *argv);
		argv++;

		for (pprivs = protoprivs, cprivs = curprivs; pprivs && cprivs;
		     pprivs = pprivs->dq_next, cprivs = cprivs->dq_next) {
			if (!devcmp_handles(pprivs->dq_h, cprivs->dq_h)) {
				errstr(_("fsname mismatch\n"));
				continue;
			}
			cprivs->dq_dqb.dqb_bsoftlimit =
				pprivs->dq_dqb.dqb_bsoftlimit;
			cprivs->dq_dqb.dqb_bhardlimit =
				pprivs->dq_dqb.dqb_bhardlimit;
			cprivs->dq_dqb.dqb_isoftlimit =
				pprivs->dq_dqb.dqb_isoftlimit;
			cprivs->dq_dqb.dqb_ihardlimit =
				pprivs->dq_dqb.dqb_ihardlimit;
			update_grace_times(cprivs);
		}
		if (putprivs(curprivs, COMMIT_LIMITS) == -1)
			ret = -1;
		freeprivs(curprivs);
	}
	if (dispose_handle_list(handles) == -1)
		ret = -1;
	freeprivs(protoprivs);
	exit(ret ? 1 : 0);
}

int main(int argc, char **argv)
{
	struct dquot *curprivs;
	int tmpfd, ret, id;
	struct quota_handle **handles;
	char *tmpfil, *tmpdir = NULL;
	struct stat st;
	struct timespec mtime;

	gettexton();
	progname = basename(argv[0]);
	ret = parse_options(argc, argv);
	argc -= ret;
	argv += ret;

	init_kernel_interface();
	handles = create_handle_list(dir_name ? 1 : 0, dir_name ? &dir_name : NULL, quotatype, fmt,
			(flags & FL_NO_MIXED_PATHS) ? 0 : IOI_NFS_MIXED_PATHS,
			(flags & FL_REMOTE) ? 0 : MS_LOCALONLY);
	if (!handles[0]) {
		dispose_handle_list(handles);
		fputs(_("No filesystems with quota detected.\n"), stderr);
		return 0;
	}
	if (protoname)
		copy_prototype(argc, argv, handles);

	umask(077);
	if (getuid() == geteuid() && getgid() == getegid())
		tmpdir = getenv("TMPDIR");
	if (!tmpdir)
		tmpdir = _PATH_TMP;
	tmpfil = smalloc(strlen(tmpdir) + strlen("/EdP.aXXXXXX") + 1);
	strcpy(tmpfil, tmpdir);
	strcat(tmpfil, "/EdP.aXXXXXX");
	tmpfd = mkstemp(tmpfil);
	if (tmpfd < 0) {
		errstr(_("Cannot create temporary file: %s\n"), strerror(errno));
		ret = -1;
		goto out;
	}
	if (fchown(tmpfd, getuid(), getgid()) < 0) {
		errstr(_("Cannot change owner of temporary file: %s\n"), strerror(errno));
		ret = -1;
		goto out;
	}
	ret = 0;
	if (flags & FL_EDIT_PERIOD) {
		if (writetimes(handles, tmpfd) < 0) {
			errstr(_("Cannot write grace times to file.\n"));
			ret = -1;
			goto out;
		}
		if (stat(tmpfil, &st) < 0) {
			errstr(_("Cannot stat file with times.\n"));
			ret = -1;
			goto out;
		}
		mtime = st.st_mtim;
		if (editprivs(tmpfil) < 0) {
			errstr(_("Error while editing grace times.\n"));
			ret = -1;
			goto out;
		}
		close(tmpfd);
		/*
		 * Reopen the file since editor may have written the
		 * file in a new place. Open in rw mode because we can
		 * reuse the file for editting the next user as well.
		 */
		if ((tmpfd = open(tmpfil, O_RDWR)) < 0)
			die(1, _("Cannot reopen!\n"));
		if (stat(tmpfil, &st) < 0) {
			errstr(_("Cannot stat file with times.\n"));
			ret = -1;
			goto out;
		}
		/* File not modified? */
		if (timespec_cmp(&mtime, &st.st_mtim) == 0)
			goto out;
		if (readtimes(handles, tmpfd) < 0) {
			errstr(_("Failed to parse grace times file.\n"));
			ret = -1;
			goto out;
		}
	}
	else {
		for (; argc > 0; argc--, argv++) {
			id = name2id(*argv, quotatype, !!(flags & FL_NUMNAMES), NULL);
			curprivs = getprivs(id, handles, !dir_name);
			if (!curprivs)
				die(1, _("Cannot get quota information for user %s.\n"), *argv);
			if (flags & FL_EDIT_TIMES) {
				if (writeindividualtimes(curprivs, tmpfd, *argv, quotatype) < 0) {
					errstr(_("Cannot write individual grace times to file.\n"));
					ret = -1;
					goto next_user;
				}
			} else {
				if (writeprivs(curprivs, tmpfd, *argv, quotatype) < 0) {
					errstr(_("Cannot write quotas to file.\n"));
					ret = -1;
					goto next_user;
				}
			}
			if (stat(tmpfil, &st) < 0) {
				errstr(_("Cannot stat file with times.\n"));
				ret = -1;
				goto out;
			}
			mtime = st.st_mtim;
			if (editprivs(tmpfil) < 0) {
				errstr(_("Error while editing quotas.\n"));
				ret = -1;
				goto next_user;
			}
			close(tmpfd);
			/*
			 * Reopen the file since editor may have written the
			 * file in a new place. Open in rw mode because we can
			 * reuse the file for editting the next user as well.
			 */
			if ((tmpfd = open(tmpfil, O_RDWR)) < 0)
				die(1, _("Cannot reopen!\n"));
			if (stat(tmpfil, &st) < 0) {
				errstr(_("Cannot stat file with times.\n"));
				ret = -1;
				goto next_user;
			}
			/* File not modified? */
			if (timespec_cmp(&mtime, &st.st_mtim) == 0)
				goto next_user;
			if (flags & FL_EDIT_TIMES) {
				if (readindividualtimes(curprivs, tmpfd) < 0) {
					errstr(_("Cannot read individual grace times from file.\n"));
					ret = -1;
					goto next_user;
				}
			} else {
				if (readprivs(curprivs, tmpfd) < 0) {
					errstr(_("Cannot read quotas from file.\n"));
					ret = -1;
					goto next_user;
				}
			}
			if (putprivs(curprivs,
			    (flags & FL_EDIT_TIMES) ? COMMIT_TIMES : COMMIT_LIMITS) == -1)
				ret = -1;
next_user:
			freeprivs(curprivs);
		}
	}
out:
	if (dispose_handle_list(handles) == -1)
		ret = -1;

	close(tmpfd);
	unlink(tmpfil);
	free(tmpfil);
	return ret ? 1 : 0;
}
