/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "str.h"
#include "safe-mkstemp.h"
#include "istream.h"
#include "ostream.h"
#include "mail-namespace.h"
#include "mail-storage.h"
#include "acl-plugin.h"
#include "acl-cache.h"
#include "acl-lookup-dict.h"
#include "acl-backend-vfile.h"

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

struct acl_mailbox_list_context_vfile {
	struct acl_mailbox_list_context ctx;

	unsigned int idx;
};

static void
acllist_clear(struct acl_backend_vfile *backend, uoff_t file_size)
{
	if (backend->acllist_pool == NULL) {
		backend->acllist_pool =
			pool_alloconly_create("vfile acllist",
					      I_MAX(file_size / 2, 128));
		i_array_init(&backend->acllist, I_MAX(16, file_size / 60));
	} else {
		p_clear(backend->acllist_pool);
		array_clear(&backend->acllist);
	}
}

static bool acl_list_get_root_dir(struct acl_backend_vfile *backend,
				  const char **root_dir_r,
				  enum mailbox_list_path_type *type_r)
{
	struct mail_storage *storage;
	const char *rootdir, *maildir;
	enum mailbox_list_path_type type;

	if (backend->backend.set->acl_globals_only)
		return FALSE;

	storage = mailbox_list_get_namespace(backend->backend.list)->storage;
	type = mail_storage_get_acl_list_path_type(storage);
	if (!mailbox_list_get_root_path(backend->backend.list, type, &rootdir))
		return FALSE;
	*type_r = type;

	if (type == MAILBOX_LIST_PATH_TYPE_DIR &&
	    mail_storage_is_mailbox_file(storage)) {
		maildir = mailbox_list_get_root_forced(backend->backend.list,
						       MAILBOX_LIST_PATH_TYPE_MAILBOX);
		if (strcmp(maildir, rootdir) == 0) {
			/* dovecot-acl-list would show up as a mailbox if we
			   created it to root dir. since we don't really have
			   any other good alternatives, place it to control
			   dir */
			rootdir = mailbox_list_get_root_forced(backend->backend.list,
					MAILBOX_LIST_PATH_TYPE_CONTROL);
			*type_r = MAILBOX_LIST_PATH_TYPE_CONTROL;
		}
	}
	*root_dir_r = rootdir;
	return TRUE;
}

static bool acl_list_get_path(struct acl_backend_vfile *backend,
			      const char **path_r)
{
	enum mailbox_list_path_type type;
	const char *root_dir;

	if (!acl_list_get_root_dir(backend, &root_dir, &type))
		return FALSE;
	*path_r = t_strconcat(root_dir, "/"ACLLIST_FILENAME, NULL);
	return TRUE;
}

static int acl_backend_vfile_acllist_read(struct acl_backend_vfile *backend)
{
	struct event *event = backend->backend.event;
	struct acl_backend_vfile_acllist acllist;
	struct istream *input;
	struct stat st;
	const char *path, *line, *p;
	int fd, ret = 0;

	backend->acllist_last_check = ioloop_time;

	if (!acl_list_get_path(backend, &path)) {
		/* we're never going to build acllist for this namespace. */
		acllist_clear(backend, 0);
		return 0;
	}

	if (backend->acllist_mtime != 0) {
		/* see if the file's mtime has changed */
		if (stat(path, &st) < 0) {
			if (errno == ENOENT)
				backend->acllist_mtime = 0;
			else
				e_error(event, "stat(%s) failed: %m", path);
			return -1;
		}
		if (st.st_mtime == backend->acllist_mtime)
			return 0;
	}

	fd = open(path, O_RDONLY);
	if (fd == -1) {
		if (errno == ENOENT) {
			backend->acllist_mtime = 0;
			return -1;
		}
		e_error(event, "open(%s) failed: %m", path);
		return -1;
	}
	if (fstat(fd, &st) < 0) {
		e_error(event, "fstat(%s) failed: %m", path);
		i_close_fd(&fd);
		return -1;
	}
	backend->acllist_mtime = st.st_mtime;
	acllist_clear(backend, st.st_size);

	input = i_stream_create_fd(fd, SIZE_MAX);
	while ((line = i_stream_read_next_line(input)) != NULL) {
		acllist.mtime = 0;
		for (p = line; *p >= '0' && *p <= '9'; p++)
			acllist.mtime = acllist.mtime * 10 + (*p - '0');

		if (p == line || *p != ' ' || p[1] == '\0') {
			e_error(event, "Broken acllist file: %s", path);
			i_unlink_if_exists(path);
			i_close_fd(&fd);
			return -1;
		}
		acllist.name = p_strdup(backend->acllist_pool, p + 1);
		array_push_back(&backend->acllist, &acllist);
	}
	if (input->stream_errno != 0)
		ret = -1;
	i_stream_destroy(&input);

	if (close(fd) < 0)
		e_error(event, "close(%s) failed: %m", path);
	return ret;
}

void acl_backend_vfile_acllist_refresh(struct acl_backend_vfile *backend)
{
	i_assert(!backend->iterating_acllist);

	if (backend->acllist_last_check +
	    (time_t)backend->cache_secs > ioloop_time)
		return;

	if (acl_backend_vfile_acllist_read(backend) < 0) {
		acllist_clear(backend, 0);
		if (!backend->rebuilding_acllist)
			(void)acl_backend_vfile_acllist_rebuild(backend);
	}
}

static int
acllist_append(struct acl_backend_vfile *backend, struct ostream *output,
	       const char *vname)
{
	struct acl_object *aclobj;
	struct acl_object_list_iter *iter;
	struct acl_rights rights;
	struct acl_backend_vfile_acllist acllist;
	const char *name;
	int ret;

	name = mailbox_list_get_storage_name(backend->backend.list, vname);
	acl_cache_flush(backend->backend.cache, name);
	aclobj = acl_object_init_from_name(&backend->backend, name);

	iter = acl_object_list_init(aclobj);
	while (acl_object_list_next(iter, &rights)) {
		if (acl_rights_has_nonowner_lookup_changes(&rights))
			break;
	}
	ret = acl_object_list_deinit(&iter);

	if (acl_backend_vfile_object_get_mtime(aclobj, &acllist.mtime) < 0)
		ret = -1;

	if (ret > 0) {
		acllist.name = p_strdup(backend->acllist_pool, name);
		array_push_back(&backend->acllist, &acllist);

		o_stream_nsend_str(output, t_strdup_printf(
			"%s %s\n", dec2str(acllist.mtime), name));
	}
	acl_object_deinit(&aclobj);
	return ret < 0 ? -1 : 0;
}

static int
acl_backend_vfile_acllist_try_rebuild(struct acl_backend_vfile *backend)
{
	struct mailbox_list *list = backend->backend.list;
	struct event *event = backend->backend.event;
	struct mail_namespace *ns;
	struct mailbox_list_iterate_context *iter;
	enum mailbox_list_path_type type;
	const struct mailbox_info *info;
	const char *rootdir, *acllist_path;
	struct ostream *output;
	struct stat st;
	struct mailbox_permissions perm;
	string_t *path;
	int fd, ret;

	i_assert(!backend->rebuilding_acllist);

	if (!acl_list_get_root_dir(backend, &rootdir, &type))
		return 0;

	ns = mailbox_list_get_namespace(list);
	if ((ns->flags & NAMESPACE_FLAG_UNUSABLE) != 0) {
		/* we can't write anything here */
		return 0;
	}

	path = t_str_new(256);
	str_printfa(path, "%s/%s", rootdir, mailbox_list_get_temp_prefix(list));

	/* Build it into a temporary file and rename() over. There's no need
	   to use locking, because even if multiple processes are rebuilding
	   the file at the same time the result should be the same. */
	mailbox_list_get_root_permissions(list, &perm);
	fd = safe_mkstemp_group(path, perm.file_create_mode,
				perm.file_create_gid,
				perm.file_create_gid_origin);
	if (fd == -1 && errno == ENOENT) {
		if (mailbox_list_mkdir_root(backend->backend.list,
					    rootdir, type) < 0)
			return -1;
		fd = safe_mkstemp_group(path, perm.file_create_mode,
					perm.file_create_gid,
					perm.file_create_gid_origin);
	}
	if (fd == -1) {
		if (ENOACCESS(errno)) {
			/* Ignore silently if we can't create it */
			return 0;
		}
		e_error(event, "dovecot-acl-list creation failed: "
			"safe_mkstemp(%s) failed: %m", str_c(path));
		return -1;
	}
	output = o_stream_create_fd_file(fd, 0, FALSE);
	o_stream_cork(output);

	ret = 0;
	acllist_clear(backend, 0);

	backend->rebuilding_acllist = TRUE;
	iter = mailbox_list_iter_init(list, "*",
				      MAILBOX_LIST_ITER_RAW_LIST |
				      MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
	while (ret == 0 && (info = mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
		ret = acllist_append(backend, output, info->vname);
	} T_END;

	if (o_stream_finish(output) < 0) {
		e_error(event, "write(%s) failed: %s", str_c(path),
			o_stream_get_error(output));
		ret = -1;
	}
	if (mailbox_list_iter_deinit(&iter) < 0)
		ret = -1;
	o_stream_destroy(&output);

	if (ret == 0) {
		if (fstat(fd, &st) < 0) {
			e_error(event, "fstat(%s) failed: %m", str_c(path));
			ret = -1;
		}
	}
	if (close(fd) < 0) {
		e_error(event, "close(%s) failed: %m", str_c(path));
		ret = -1;
	}

	if (ret == 0) {
		if (!acl_list_get_path(backend, &acllist_path))
			i_unreached();
		if (rename(str_c(path), acllist_path) < 0) {
			e_error(event, "rename(%s, %s) failed: %m",
				str_c(path), acllist_path);
			ret = -1;
		}
	}
	if (ret == 0) {
		struct acl_user *auser = ACL_USER_CONTEXT(ns->user);
		i_assert(auser != NULL);
		backend->acllist_mtime = st.st_mtime;
		backend->acllist_last_check = ioloop_time;
		(void)acl_lookup_dict_rebuild(auser->acl_lookup_dict,
			backend->backend.set->acl_dict_index);
	} else {
		acllist_clear(backend, 0);
		i_unlink_if_exists(str_c(path));
	}
	backend->rebuilding_acllist = FALSE;
	return ret;
}

int acl_backend_vfile_acllist_rebuild(struct acl_backend_vfile *backend)
{
	const char *acllist_path;

	if (acl_backend_vfile_acllist_try_rebuild(backend) == 0)
		return 0;
	else {
		/* delete it to make sure it gets rebuilt later */
		if (!acl_list_get_path(backend, &acllist_path))
			i_unreached();
		i_unlink_if_exists(acllist_path);
		return -1;
	}
}

static const struct acl_backend_vfile_acllist *
acl_backend_vfile_acllist_find(struct acl_backend_vfile *backend,
			       const char *name)
{
	const struct acl_backend_vfile_acllist *acllist;

	array_foreach(&backend->acllist, acllist) {
		if (strcmp(acllist->name, name) == 0)
			return acllist;
	}
	return NULL;
}

void acl_backend_vfile_acllist_verify(struct acl_backend_vfile *backend,
				      const char *name, time_t mtime)
{
	const struct acl_backend_vfile_acllist *acllist;

	if (backend->rebuilding_acllist || backend->iterating_acllist)
		return;

	acl_backend_vfile_acllist_refresh(backend);
	acllist = acl_backend_vfile_acllist_find(backend, name);
	if (acllist != NULL && acllist->mtime != mtime)
		(void)acl_backend_vfile_acllist_rebuild(backend);
}

struct acl_mailbox_list_context *
acl_backend_vfile_nonowner_iter_init(struct acl_backend *_backend)
{
	struct acl_backend_vfile *backend =
		(struct acl_backend_vfile *)_backend;
	struct acl_mailbox_list_context_vfile *ctx;

	acl_backend_vfile_acllist_refresh(backend);

	ctx = i_new(struct acl_mailbox_list_context_vfile, 1);
	ctx->ctx.backend = _backend;
	backend->iterating_acllist = TRUE;
	return &ctx->ctx;
}

bool acl_backend_vfile_nonowner_iter_next(struct acl_mailbox_list_context *_ctx,
					 const char **name_r)
{
	struct acl_mailbox_list_context_vfile *ctx =
		(struct acl_mailbox_list_context_vfile *)_ctx;
	struct acl_backend_vfile *backend =
		(struct acl_backend_vfile *)_ctx->backend;
	const struct acl_backend_vfile_acllist *acllist;
	unsigned int count;

	if (_ctx->failed)
		return FALSE;

	acllist = array_get(&backend->acllist, &count);
	if (count == 0)
		_ctx->empty = TRUE;
	if (ctx->idx == count)
		return FALSE;

	*name_r = acllist[ctx->idx++].name;
	return TRUE;
}

int
acl_backend_vfile_nonowner_iter_deinit(struct acl_mailbox_list_context *ctx)
{
	struct acl_backend_vfile *backend =
		(struct acl_backend_vfile *)ctx->backend;
	int ret;

	backend->iterating_acllist = FALSE;
	if (ctx->failed)
		ret = -1;
	else if (ctx->empty)
		ret = 0;
	else
		ret = 1;
	i_free(ctx);
	return ret;
}

int acl_backend_vfile_nonowner_lookups_rebuild(struct acl_backend *_backend)
{
	struct acl_backend_vfile *backend =
		(struct acl_backend_vfile *)_backend;

	return acl_backend_vfile_acllist_rebuild(backend);
}
