/* 
   ldb database library

   Copyright (C) Andrew Bartlett 2006

     ** NOTE! The following LGPL license applies to the ldb
     ** library. This does NOT imply that all of Samba is released
     ** under the LGPL
   
   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 3 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/

/*
 *  Name: ldb
 *
 *  Component: ad2oLschema
 *
 *  Description: utility to convert an AD schema into the format required by OpenLDAP
 *
 *  Author: Andrew Bartlett
 */

#include "includes.h"
#include "ldb_includes.h"
#include "system/locale.h"
#include "tools/cmdline.h"
#include "tools/convert.h"
#include "param/param.h"
#include "lib/cmdline/popt_common.h"

struct schema_conv {
	int count;
	int skipped;
	int failures;
};

enum convert_target {
	TARGET_OPENLDAP,
	TARGET_FEDORA_DS
};
	

static void usage(void)
{
	printf("Usage: ad2oLschema <options>\n");
	printf("\nConvert AD-like LDIF to OpenLDAP schema format\n\n");
	printf("Options:\n");
	printf("  -I inputfile     inputfile of mapped OIDs and skipped attributes/ObjectClasses");
	printf("  -H url           LDB or LDAP server to read schmea from\n");
	printf("  -O outputfile    outputfile otherwise STDOUT\n");
	printf("  -o options       pass options like modules to activate\n");
	printf("              e.g: -o modules:timestamps\n");
	printf("\n");
	printf("Converts records from an AD-like LDIF schema into an openLdap formatted schema\n\n");
	exit(1);
}

static int fetch_attrs_schema(struct ldb_context *ldb, struct ldb_dn *schemadn,
			      TALLOC_CTX *mem_ctx, 
			      struct ldb_result **attrs_res)
{
	TALLOC_CTX *local_ctx = talloc_new(mem_ctx);
	int ret;
	const char *attrs[] = {
		"lDAPDisplayName",
		"isSingleValued",
		"attributeID",
		"attributeSyntax",
		"description",		
		NULL
	};

	if (!local_ctx) {
		return LDB_ERR_OPERATIONS_ERROR;
	}
	
	/* Downlaod schema */
	ret = ldb_search(ldb, schemadn, LDB_SCOPE_SUBTREE, 
			 "objectClass=attributeSchema", 
			 attrs, attrs_res);
	if (ret != LDB_SUCCESS) {
		printf("Search failed: %s\n", ldb_errstring(ldb));
		return LDB_ERR_OPERATIONS_ERROR;
	}
	
	return ret;
}

static const char *oc_attrs[] = {
	"lDAPDisplayName",
	"mayContain",
	"mustContain",
	"systemMayContain",
	"systemMustContain",
	"objectClassCategory",
	"governsID",
	"description",
	"subClassOf",
	NULL
};

static int fetch_oc_recursive(struct ldb_context *ldb, struct ldb_dn *schemadn, 
			      TALLOC_CTX *mem_ctx, 
			      struct ldb_result *search_from,
			      struct ldb_result *res_list)
{
	int i;
	int ret = 0;
	for (i=0; i < search_from->count; i++) {
		struct ldb_result *res;
		const char *name = ldb_msg_find_attr_as_string(search_from->msgs[i], 
							       "lDAPDisplayname", NULL);

		ret = ldb_search_exp_fmt(ldb, mem_ctx, &res,
					schemadn, LDB_SCOPE_SUBTREE, oc_attrs,
					"(&(&(objectClass=classSchema)(subClassOf=%s))(!(lDAPDisplayName=%s)))",
					name, name);
		if (ret != LDB_SUCCESS) {
			printf("Search failed: %s\n", ldb_errstring(ldb));
			return ret;
		}
		
		res_list->msgs = talloc_realloc(res_list, res_list->msgs, 
						struct ldb_message *, res_list->count + 2);
		if (!res_list->msgs) {
			return LDB_ERR_OPERATIONS_ERROR;
		}
		res_list->msgs[res_list->count] = talloc_move(res_list, 
							      &search_from->msgs[i]);
		res_list->count++;
		res_list->msgs[res_list->count] = NULL;

		if (res->count > 0) {
			ret = fetch_oc_recursive(ldb, schemadn, mem_ctx, res, res_list); 
		}
		if (ret != LDB_SUCCESS) {
			return ret;
		}
	}
	return ret;
}

static int fetch_objectclass_schema(struct ldb_context *ldb, struct ldb_dn *schemadn, 
				    TALLOC_CTX *mem_ctx, 
				    struct ldb_result **objectclasses_res)
{
	TALLOC_CTX *local_ctx = talloc_new(mem_ctx);
	struct ldb_result *top_res, *ret_res;
	int ret;
	if (!local_ctx) {
		return LDB_ERR_OPERATIONS_ERROR;
	}
	
	/* Downlaod 'top' */
	ret = ldb_search(ldb, schemadn, LDB_SCOPE_SUBTREE, 
			 "(&(objectClass=classSchema)(lDAPDisplayName=top))", 
			 oc_attrs, &top_res);
	if (ret != LDB_SUCCESS) {
		printf("Search failed: %s\n", ldb_errstring(ldb));
		return LDB_ERR_OPERATIONS_ERROR;
	}

	talloc_steal(local_ctx, top_res);

	if (top_res->count != 1) {
		return LDB_ERR_OPERATIONS_ERROR;
	}

	ret_res = talloc_zero(local_ctx, struct ldb_result);
	if (!ret_res) {
		return LDB_ERR_OPERATIONS_ERROR;
	}

	ret = fetch_oc_recursive(ldb, schemadn, local_ctx, top_res, ret_res); 

	if (ret != LDB_SUCCESS) {
		printf("Search failed: %s\n", ldb_errstring(ldb));
		return LDB_ERR_OPERATIONS_ERROR;
	}

	*objectclasses_res = talloc_move(mem_ctx, &ret_res);
	return ret;
}

static struct ldb_dn *find_schema_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx) 
{
	const char *rootdse_attrs[] = {"schemaNamingContext", NULL};
	const char *no_attrs[] = { NULL };
	struct ldb_dn *schemadn;
	struct ldb_dn *basedn = ldb_dn_new(mem_ctx, ldb, NULL);
	struct ldb_result *rootdse_res;
	struct ldb_result *schema_res;
	int ldb_ret;
	if (!basedn) {
		return NULL;
	}
	
	/* Search for rootdse */
	ldb_ret = ldb_search(ldb, basedn, LDB_SCOPE_BASE, NULL, rootdse_attrs, &rootdse_res);
	if (ldb_ret != LDB_SUCCESS) {
		ldb_ret = ldb_search(ldb, basedn, LDB_SCOPE_SUBTREE, 
				 "(&(objectClass=dMD)(cn=Schema))", 
				 no_attrs, &schema_res);
		if (ldb_ret) {
			printf("cn=Schema Search failed: %s\n", ldb_errstring(ldb));
			return NULL;
		}

		talloc_steal(mem_ctx, schema_res);

		if (schema_res->count != 1) {
			printf("Failed to find rootDSE");
			return NULL;
		}
		
		schemadn = talloc_steal(mem_ctx, schema_res->msgs[0]->dn);
		talloc_free(schema_res);
		return schemadn;
		
	}
	
	talloc_steal(mem_ctx, rootdse_res);

	if (rootdse_res->count != 1) {
		printf("Failed to find rootDSE");
		return NULL;
	}
	
	/* Locate schema */
	schemadn = ldb_msg_find_attr_as_dn(ldb, mem_ctx, rootdse_res->msgs[0], "schemaNamingContext");
	if (!schemadn) {
		return NULL;
	}

	talloc_free(rootdse_res);
	return schemadn;
}

#define IF_NULL_FAIL_RET(x) do {     \
		if (!x) {		\
			ret.failures++; \
			return ret;	\
		}			\
	} while (0) 


static struct schema_conv process_convert(struct ldb_context *ldb, enum convert_target target, FILE *in, FILE *out) 
{
	/* Read list of attributes to skip, OIDs to map */
	TALLOC_CTX *mem_ctx = talloc_new(ldb);
	char *line;
	const char **attrs_skip = NULL;
	int num_skip = 0;
	struct oid_map {
		char *old_oid;
		char *new_oid;
	} *oid_map = NULL;
	int num_oid_maps = 0;
	struct attr_map {
		char *old_attr;
		char *new_attr;
	} *attr_map = NULL;
	int num_attr_maps = 0;	
	struct ldb_result *attrs_res, *objectclasses_res;
	struct ldb_dn *schemadn;
	struct schema_conv ret;

	int ldb_ret, i;

	ret.count = 0;
	ret.skipped = 0;
	ret.failures = 0;

	while ((line = afdgets(fileno(in), mem_ctx, 0))) {
		/* Blank Line */
		if (line[0] == '\0') {
			continue;
		}
		/* Comment */
		if (line[0] == '#') {
			continue;
		}
		if (isdigit(line[0])) {
			char *p = strchr(line, ':');
			IF_NULL_FAIL_RET(p);
			p[0] = '\0';
			p++;
			oid_map = talloc_realloc(mem_ctx, oid_map, struct oid_map, num_oid_maps + 2);
			trim_string(line, " ", " ");
			oid_map[num_oid_maps].old_oid = talloc_move(oid_map, &line);
			trim_string(p, " ", " ");
			oid_map[num_oid_maps].new_oid = p;
			num_oid_maps++;
			oid_map[num_oid_maps].old_oid = NULL;
		} else {
			char *p = strchr(line, ':');
			if (p) {
				/* remap attribute/objectClass */
				p[0] = '\0';
				p++;
				attr_map = talloc_realloc(mem_ctx, attr_map, struct attr_map, num_attr_maps + 2);
				trim_string(line, " ", " ");
				attr_map[num_attr_maps].old_attr = talloc_move(attr_map, &line);
				trim_string(p, " ", " ");
				attr_map[num_attr_maps].new_attr = p;
				num_attr_maps++;
				attr_map[num_attr_maps].old_attr = NULL;
			} else {
				/* skip attribute/objectClass */
				attrs_skip = talloc_realloc(mem_ctx, attrs_skip, const char *, num_skip + 2);
				trim_string(line, " ", " ");
				attrs_skip[num_skip] = talloc_move(attrs_skip, &line);
				num_skip++;
				attrs_skip[num_skip] = NULL;
			}
		}
	}

	schemadn = find_schema_dn(ldb, mem_ctx);
	if (!schemadn) {
		printf("Failed to find schema DN: %s\n", ldb_errstring(ldb));
		ret.failures = 1;
		return ret;
	}
	
	ldb_ret = fetch_attrs_schema(ldb, schemadn, mem_ctx, &attrs_res);
	if (ldb_ret != LDB_SUCCESS) {
		printf("Failed to fetch attribute schema: %s\n", ldb_errstring(ldb));
		ret.failures = 1;
		return ret;
	}
	
	switch (target) {
	case TARGET_OPENLDAP:
		break;
	case TARGET_FEDORA_DS:
		fprintf(out, "dn: cn=schema\n");
		break;
	}

	for (i=0; i < attrs_res->count; i++) {
		struct ldb_message *msg = attrs_res->msgs[i];

		const char *name = ldb_msg_find_attr_as_string(msg, "lDAPDisplayName", NULL);
		const char *description = ldb_msg_find_attr_as_string(msg, "description", NULL);
		const char *oid = ldb_msg_find_attr_as_string(msg, "attributeID", NULL);
		const char *syntax = ldb_msg_find_attr_as_string(msg, "attributeSyntax", NULL);
		bool single_value = ldb_msg_find_attr_as_bool(msg, "isSingleValued", false);
		const struct syntax_map *map = find_syntax_map_by_ad_oid(syntax);
		char *schema_entry = NULL;
		int j;

		if (!name) {
			printf("Failed to find lDAPDisplayName for schema DN: %s\n", ldb_dn_get_linearized(msg->dn));
			ret.failures++;
			continue;
		}

		/* We have been asked to skip some attributes/objectClasses */
		if (attrs_skip && str_list_check_ci(attrs_skip, name)) {
			ret.skipped++;
			continue;
		}

		/* We might have been asked to remap this oid, due to a conflict */
		for (j=0; oid && oid_map && oid_map[j].old_oid; j++) {
			if (strcasecmp(oid, oid_map[j].old_oid) == 0) {
				oid =  oid_map[j].new_oid;
				break;
			}
		}
		
		switch (target) {
		case TARGET_OPENLDAP:
			schema_entry = talloc_asprintf(mem_ctx, 
						       "attributetype (\n"
						       "  %s\n", oid);
			break;
		case TARGET_FEDORA_DS:
			schema_entry = talloc_asprintf(mem_ctx, 
						       "attributeTypes: (\n"
						       "  %s\n", oid);
			break;
		}
		IF_NULL_FAIL_RET(schema_entry);

		/* We might have been asked to remap this name, due to a conflict */
		for (j=0; name && attr_map && attr_map[j].old_attr; j++) {
			if (strcasecmp(name, attr_map[j].old_attr) == 0) {
				name =  attr_map[j].new_attr;
				break;
			}
		}
		
		schema_entry = talloc_asprintf_append(schema_entry, 
						      "  NAME '%s'\n", name);
		IF_NULL_FAIL_RET(schema_entry);

		if (description) {
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  DESC %s\n", description);
			IF_NULL_FAIL_RET(schema_entry);
		}

		if (map) {
			const char *syntax_oid;
			if (map->equality) {
				schema_entry = talloc_asprintf_append(schema_entry, 
								      "  EQUALITY %s\n", map->equality);
				IF_NULL_FAIL_RET(schema_entry);
			}
			if (map->substring) {
				schema_entry = talloc_asprintf_append(schema_entry, 
								      "  SUBSTR %s\n", map->substring);
				IF_NULL_FAIL_RET(schema_entry);
			}
			syntax_oid = map->Standard_OID;
			/* We might have been asked to remap this oid,
			 * due to a conflict, or lack of
			 * implementation */
			for (j=0; syntax_oid && oid_map && oid_map[j].old_oid; j++) {
				if (strcasecmp(syntax_oid, oid_map[j].old_oid) == 0) {
					syntax_oid =  oid_map[j].new_oid;
					break;
				}
			}
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  SYNTAX %s\n", syntax_oid);
			IF_NULL_FAIL_RET(schema_entry);
		}

		if (single_value) {
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  SINGLE-VALUE\n");
			IF_NULL_FAIL_RET(schema_entry);
		}
		
		schema_entry = talloc_asprintf_append(schema_entry, 
						      "  )");

		switch (target) {
		case TARGET_OPENLDAP:
			fprintf(out, "%s\n\n", schema_entry);
			break;
		case TARGET_FEDORA_DS:
			fprintf(out, "%s\n", schema_entry);
			break;
		}
		ret.count++;
	}

	ldb_ret = fetch_objectclass_schema(ldb, schemadn, mem_ctx, &objectclasses_res);
	if (ldb_ret != LDB_SUCCESS) {
		printf("Failed to fetch objectClass schema elements: %s\n", ldb_errstring(ldb));
		ret.failures = 1;
		return ret;
	}
	
	for (i=0; i < objectclasses_res->count; i++) {
		struct ldb_message *msg = objectclasses_res->msgs[i];
		const char *name = ldb_msg_find_attr_as_string(msg, "lDAPDisplayName", NULL);
		const char *description = ldb_msg_find_attr_as_string(msg, "description", NULL);
		const char *oid = ldb_msg_find_attr_as_string(msg, "governsID", NULL);
		const char *subClassOf = ldb_msg_find_attr_as_string(msg, "subClassOf", NULL);
		int objectClassCategory = ldb_msg_find_attr_as_int(msg, "objectClassCategory", 0);
		struct ldb_message_element *must = ldb_msg_find_element(msg, "mustContain");
		struct ldb_message_element *sys_must = ldb_msg_find_element(msg, "systemMustContain");
		struct ldb_message_element *may = ldb_msg_find_element(msg, "mayContain");
		struct ldb_message_element *sys_may = ldb_msg_find_element(msg, "systemMayContain");
		char *schema_entry = NULL;
		int j;

		if (!name) {
			printf("Failed to find lDAPDisplayName for schema DN: %s\n", ldb_dn_get_linearized(msg->dn));
			ret.failures++;
			continue;
		}

		/* We have been asked to skip some attributes/objectClasses */
		if (attrs_skip && str_list_check_ci(attrs_skip, name)) {
			ret.skipped++;
			continue;
		}

		/* We might have been asked to remap this oid, due to a conflict */
		for (j=0; oid_map && oid_map[j].old_oid; j++) {
			if (strcasecmp(oid, oid_map[j].old_oid) == 0) {
				oid =  oid_map[j].new_oid;
				break;
			}
		}
		
		switch (target) {
		case TARGET_OPENLDAP:
			schema_entry = talloc_asprintf(mem_ctx, 
						       "objectclass (\n"
						       "  %s\n", oid);
			break;
		case TARGET_FEDORA_DS:
			schema_entry = talloc_asprintf(mem_ctx, 
						       "objectClasses: (\n"
						       "  %s\n", oid);
			break;
		}
		IF_NULL_FAIL_RET(schema_entry);
		if (!schema_entry) {
			ret.failures++;
			break;
		}

		/* We might have been asked to remap this name, due to a conflict */
		for (j=0; name && attr_map && attr_map[j].old_attr; j++) {
			if (strcasecmp(name, attr_map[j].old_attr) == 0) {
				name =  attr_map[j].new_attr;
				break;
			}
		}
		
		schema_entry = talloc_asprintf_append(schema_entry, 
						      "  NAME '%s'\n", name);
		IF_NULL_FAIL_RET(schema_entry);

		if (!schema_entry) return ret;

		if (description) {
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  DESC %s\n", description);
			IF_NULL_FAIL_RET(schema_entry);
		}

		if (subClassOf) {
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  SUP %s\n", subClassOf);
			IF_NULL_FAIL_RET(schema_entry);
		}
		
		switch (objectClassCategory) {
		case 1:
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  STRUCTURAL\n");
			IF_NULL_FAIL_RET(schema_entry);
			break;
		case 2:
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  ABSTRACT\n");
			IF_NULL_FAIL_RET(schema_entry);
			break;
		case 3:
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  AUXILIARY\n");
			IF_NULL_FAIL_RET(schema_entry);
			break;
		}

#define APPEND_ATTRS(attributes) \
		do {						\
			int k;						\
			for (k=0; attributes && k < attributes->num_values; k++) { \
				int attr_idx; \
				const char *attr_name = (const char *)attributes->values[k].data;  \
				/* We might have been asked to remap this name, due to a conflict */ \
				for (attr_idx=0; attr_name && attr_map && attr_map[attr_idx].old_attr; attr_idx++) { \
					if (strcasecmp(attr_name, attr_map[attr_idx].old_attr) == 0) { \
						attr_name =  attr_map[attr_idx].new_attr; \
						break;			\
					}				\
				}					\
									\
				schema_entry = talloc_asprintf_append(schema_entry, \
								      " %s", \
								      attr_name); \
				IF_NULL_FAIL_RET(schema_entry);		\
				if (k != (attributes->num_values - 1)) { \
					schema_entry = talloc_asprintf_append(schema_entry, \
									      " $"); \
					IF_NULL_FAIL_RET(schema_entry);	\
					if (target == TARGET_OPENLDAP && ((k+1)%5 == 0)) { \
						schema_entry = talloc_asprintf_append(schema_entry, \
										      "\n  "); \
						IF_NULL_FAIL_RET(schema_entry);	\
					}				\
				}					\
			}						\
		} while (0)

		if (must || sys_must) {
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  MUST (");
			IF_NULL_FAIL_RET(schema_entry);

			APPEND_ATTRS(must);
			if (must && sys_must) {
				schema_entry = talloc_asprintf_append(schema_entry, \
								      " $"); \
			}
			APPEND_ATTRS(sys_must);
			
			schema_entry = talloc_asprintf_append(schema_entry, 
							      " )\n");
			IF_NULL_FAIL_RET(schema_entry);
		}

		if (may || sys_may) {
			schema_entry = talloc_asprintf_append(schema_entry, 
							      "  MAY (");
			IF_NULL_FAIL_RET(schema_entry);

			APPEND_ATTRS(may);
			if (may && sys_may) {
				schema_entry = talloc_asprintf_append(schema_entry, \
								      " $"); \
			}
			APPEND_ATTRS(sys_may);
			
			schema_entry = talloc_asprintf_append(schema_entry, 
							      " )\n");
			IF_NULL_FAIL_RET(schema_entry);
		}

		schema_entry = talloc_asprintf_append(schema_entry, 
						      "  )");

		switch (target) {
		case TARGET_OPENLDAP:
			fprintf(out, "%s\n\n", schema_entry);
			break;
		case TARGET_FEDORA_DS:
			fprintf(out, "%s\n", schema_entry);
			break;
		}
		ret.count++;
	}

	return ret;
}

 int main(int argc, const char **argv)
{
	TALLOC_CTX *ctx;
	struct ldb_cmdline *options;
	FILE *in = stdin;
	FILE *out = stdout;
	struct ldb_context *ldb;
	struct schema_conv ret;
	const char *target_str;
	enum convert_target target;

	ctx = talloc_new(NULL);
	ldb = ldb_init(ctx, NULL);

	options = ldb_cmdline_process(ldb, argc, argv, usage);

	if (options->input) {
		in = fopen(options->input, "r");
		if (!in) {
			perror(options->input);
			exit(1);
		}
	}
	if (options->output) {
		out = fopen(options->output, "w");
		if (!out) {
			perror(options->output);
			exit(1);
		}
	}

	target_str = lp_parm_string(cmdline_lp_ctx, NULL, "convert", "target");

	if (!target_str || strcasecmp(target_str, "openldap") == 0) {
		target = TARGET_OPENLDAP;
	} else if (strcasecmp(target_str, "fedora-ds") == 0) {
		target = TARGET_FEDORA_DS;
	} else {
		printf("Unsupported target: %s\n", target_str);
		exit(1);
	}

	ret = process_convert(ldb, target, in, out);

	fclose(in);
	fclose(out);

	printf("Converted %d records (skipped %d) with %d failures\n", ret.count, ret.skipped, ret.failures);

	return 0;
}
