/* $Cambridge: hermes/src/prayer/session/lookup.c,v 1.4 2008/09/16 09:59:58 dpc22 Exp $ */
/************************************************
 *    Prayer - a Webmail Interface              *
 ************************************************/

/* Copyright (c) University of Cambridge 2000 - 2008 */
/* See the file NOTICE for conditions of use and distribution. */

#include "prayer_session.h"

#ifdef LDAP_ENABLE
/* ldap_init() and a few others deprecated in latest versions? */
#ifndef LDAP_DEPRECATED
#  define LDAP_DEPRECATED 1
#endif
#include <ldap.h>
#endif

struct lookup *
lookup_create(struct session *session, struct pool *pool)
{
    struct lookup *result = pool_alloc(pool, sizeof(struct lookup));
    struct config *config = session->config;
    char *s;

    result->query   = NIL;
    result->entries = NIL;
    result->count   = 0;
    result->current = 0;

    result->master_pool = pool;
    result->pool = NIL;
    result->have_cancelled = NIL;

    result->session = session;
    result->local_fullname  = config->lookup_fullname;
    result->local_username  = config->lookup_username;
    result->local_rpasswd   = config->lookup_rpasswd;
    result->local_rusername = config->lookup_rusername;

    if (config->ldap_server) {
        result->ldap_server = pool_strdup(pool, config->ldap_server);

        if ((s=strchr(result->ldap_server, ':'))) {
            *s++ = '\0';
            result->ldap_port = atoi(s);
        } else
            result->ldap_port = LDAP_DEFAULT_PORT;
    } else {
        result->ldap_server = NIL;
        result->ldap_port   = 0;
    }

    result->ldap_base_dn    = config->ldap_base_dn;
    result->ldap_timeout    = config->ldap_timeout;

    return(result);
}

void
lookup_clear(struct lookup *lookup)
{
    if (lookup->pool)
        pool_free(lookup->pool);

    lookup->pool    = NIL;
    lookup->query   = NIL;
    lookup->entries = NIL;
    lookup->count   = 0;
    lookup->current = 0;
    lookup->have_cancelled = NIL;
    lookup->have_phone = NIL;
}

void
lookup_free(struct lookup *lookup)
{
    lookup_clear(lookup);

    if (!lookup->master_pool)
        free(lookup);
}

BOOL
lookup_local_available(struct lookup *lookup)
{
    if (lookup->local_rusername && lookup->local_rpasswd &&
        lookup->local_username  && lookup->local_fullname)
        return(T);

    return(NIL);
}

BOOL
lookup_ldap_available(struct lookup *lookup)
{
    if (lookup->ldap_server && lookup->ldap_base_dn)
        return(T);

    return(NIL);
}

/* ====================================================================== */

static int
make_list_compare(const void *s1, const void *s2)
{
    return((strcmp(*(char **)s1, *(char **)s2)));
}

static char **
make_string_list(struct pool *pool, char *text, unsigned long *countp)
{
    int count;
    char *s, *t;
    char **result;

    if (!(text && text[0])) {
        result = pool_alloc(pool, 1 * sizeof(char *));
        result[0] = NIL;
        *countp = 0;
        return(result);
    }

    for (count = 0 , s = text ; *s ; s++) {
        if (*s == ':')
            count++;
        else if (*s == ',')
            count++;
    }

    text   = pool_strdup(pool, text);   /* Scratch copy */
    result = pool_alloc(pool, (count+1) * sizeof(char *));

    count = 0;
    for (s = text ; s ; s = t) {
        if ((t = strpbrk(s, ":,")))
            *t++ = '\0';

        if (s[0])
            result[count++] = s;
    }
    result[count] = NIL;

    qsort(result, count, sizeof(const char *), make_list_compare);

    *countp = count;
    return(result);
}

BOOL
lookup_local(struct lookup *lookup, char *query, char *default_domain)
{
    struct session *session = lookup->session;
    struct pool *pool;
    struct lookup_item *item;
    void *cdb, *cdb_username, *cdb_fullname;
    char *ruser, *rpass, *user, *full;
    char **list1, **list2;
    unsigned long count1, count2;
    char *userid, *registered_name, *affiliation, *s;
    int   cmp;
    BOOL  cancelled = NIL;

    lookup_clear(lookup);

    lookup->pool = pool = pool_create(1024);

    lookup->query          = pool_strdup(pool, query);
    lookup->have_cancelled = T;
    lookup->have_phone     = NIL;

    /* Create scratch copy and convert string to lower case */
    query = pool_strdup(pool, query);
    for (s = query; *s; s++)
        *s = Utolower(*s);

    if (!(lookup->local_rusername && lookup->local_rpasswd &&
          lookup->local_username  && lookup->local_fullname)) {
        session_message(session,
                        "Error: Local search database not configured");
        session_log(session,
                    "Error: Local search database not configured");
        return(NIL);
    }

    if (getpwnam(query)) {
        /* Given a valid CRSid */
        list1 = make_string_list(pool, query, &count1);
        list2 = make_string_list(pool, "", &count2);
    } else {
        /* Need to search reverse lookup tables */
        ruser = NIL;
        if ((cdb=cdb_open(lookup->local_rusername)) == NIL) {
            session_message(session,
                            "Error: Unable to access local search database");
            session_log(session,
                            "Error: Unable to access rusername database");
            return(NIL);
        }
        cdb_find(cdb, query, strlen(query), &ruser);
        cdb_close(cdb);

        rpass = NIL;
        if ((cdb=cdb_open(lookup->local_rpasswd)) == NIL) {
            session_message(session,
                            "Error: Unable to access local search database");
            session_log(session,
                        "Error: Unable to access rpasswd database");
            return(NIL);
        }
        cdb_find(cdb, query, strlen(query), &rpass);
        cdb_close(cdb);

        list1 = make_string_list(pool, ruser, &count1);
        list2 = make_string_list(pool, rpass, &count2);

        if (ruser) free(ruser);
        if (rpass) free(rpass);
    }

    if ((cdb_username=cdb_open(lookup->local_username)) == NIL) {
        session_message(session,
                        "Error: Unable to access local search database");
        session_log(session,
                    "Error: Unable to access username database");
        return(NIL);
    }

    if ((cdb_fullname=cdb_open(lookup->local_fullname)) == NIL) {
        session_message(session,
                        "Error: Unable to access local search database");
        session_log(session,
                    "Error: Unable to access fullname database");
        return(NIL);
    }

    lookup->entries
        = pool_alloc(lookup->pool,
                     (count1+count2+1) * sizeof(struct lookup_item *));

    /* Merge the two lists, removing duplicates */
    lookup->count = 0;
    while (*list1 || *list2) {
        if (*list1 && *list2) {
            cmp = strcmp(*list1, *list2);
            if (cmp > 0)
                userid = *list2++;
            else if (cmp < 0)
                userid = *list1++;
            else {
                userid = *list1++; list2++;
            }
        } else if (*list1)
            userid = *list1++;
        else
            userid = *list2++;
        
        user = NIL;
        cdb_find(cdb_username, userid, strlen(userid), &user);

        if (user) {
            registered_name = user;
            
            if ((affiliation = strchr(user, '|'))) {
                *affiliation++ = '\0';

                if ((s=strchr(affiliation, '|'))) {
                    *s = '\0';
                    cancelled = T;
                } else
                    cancelled = NIL;
            }
        } else {
            registered_name = affiliation = "Unknown";
            cancelled = NIL;
        }

        /* cdb_find here fails if the user is cancelled */
        full = NIL;
        cdb_find(cdb_fullname, userid, strlen(userid), &full);

        pool = lookup->pool;

        item = pool_alloc(pool, sizeof(struct lookup_item));
        item->userid          = pool_strdup(pool, userid);
        item->registered_name = pool_strdup(pool, registered_name);

        if (full && full[0])
            item->display_name = pool_strdup(pool, full);
        else
            item->display_name = pool_strdup(pool, registered_name);

        item->affiliation     = pool_strdup(pool, affiliation);
        item->email = pool_strcat3(pool, userid, "@", default_domain);
        item->phone     = NIL;
        item->cancelled = cancelled;
        lookup->entries[lookup->count++] = item;

        if (user) free(user);
        if (full) free(full);
    }
    lookup->entries[lookup->count] = NIL;

    lookup->current = (lookup->count > 0) ? 1 : 0;
    return(T);
}

/* ====================================================================== */

#ifdef LDAP_ENABLE

static int
lookup_ldap_sort(const void *p1, const void *p2)
{
    struct lookup_item *item1 = *(struct lookup_item **)p1;
    struct lookup_item *item2 = *(struct lookup_item **)p2;

    return (strcmp(item1->userid, item2->userid));
}

BOOL
lookup_ldap(struct lookup *lookup, char *query)
{
    struct session *session = lookup->session;
    struct pool    *pool;
    LDAP   *ld = NULL;
    char   *attrs[] = { "uid", "displayName", "cn", "ou", "mail",
                        "telephoneNumber", NULL };
    int     rc = LDAP_SUCCESS;
    int     version;
    LDAPMessage *result, *entry;
    char **names;
    struct timeval timeout;
    unsigned long count;
    char *filter;
    struct lookup_item *item;

    lookup->pool = pool = pool_create(1024);
    lookup->query          = pool_strdup(pool, query);
    lookup->have_cancelled = NIL;
    lookup->have_phone     = T;

    filter = pool_printf(pool,
                         "(&(|(surname=*%s*)(mail=*%s*))(mail=*))",
                         query, query);

    if (!(lookup->ldap_server && lookup->ldap_base_dn)) {
        session_message(session, "Error: Directory server not configured");
        session_log(session, "Error: LDAP server not configured");
        return(NIL);
    }

    ld = ldap_init(lookup->ldap_server, lookup->ldap_port);

    if ( ld == NULL ) {
        session_message(session, "Error: Failed to initialise directory");
        session_log(session, "Error: Failed to initialise LDAP");
        return(NIL);
    }

    version = LDAP_VERSION3;
    ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version); 

    rc = ldap_bind_s(ld, NULL, NULL, LDAP_AUTH_SIMPLE);
    if ( rc != LDAP_SUCCESS ) {
        session_message(session, "Error: Failed to bind to directory server");
        session_log(session, "Error: Failed to bind LDAP");
        return(NIL);
    }

    /* Synchronous search with timeout */
    timeout.tv_sec  = (lookup->ldap_timeout) ? lookup->ldap_timeout : 30;
    timeout.tv_usec =  0;

    rc = ldap_search_st( ld, lookup->ldap_base_dn, LDAP_SCOPE_ONELEVEL,
                         filter, attrs, 0, &timeout, &result );
    if ( rc != LDAP_SUCCESS ) {
        session_message(session,
                        "Error: Directory search failed or timed out");
        session_log(session, "Error: LDAP search failed or timed out");
        return(NIL);
    }

    if ((lookup->count = ldap_count_entries(ld, result)) < 0) {
        lookup->count = 0;
        session_message(session, "Error: Invalid response from directory");
        session_log(session, "Error: ldap_count_entries() negative");
        return(NIL);
    }

    lookup->entries
        = pool_alloc(lookup->pool,
                     (lookup->count+1) * sizeof(struct lookup_item *));

    lookup->entries[lookup->count] = NIL;

    count = 0;
    entry = ldap_first_entry(ld, result);
    while (entry) {
        lookup->entries[count++] 
            = item = pool_alloc(pool, sizeof(struct lookup_item));

        item->userid = "";
        item->registered_name = "";
        item->display_name = "";
        item->affiliation = "";
        item->email = "";
        item->phone = "";
        item->cancelled = NIL;

        for (names = attrs ; *names ; names++) {
            char **vals = ldap_get_values(ld, entry, *names);
            char **a;

            if (!vals)
                continue;

            for (a = vals ; *a ; a++) {
                char *key   = *names;
                char *value = *a;

                if (!value[0])
                    continue;

                if (!strcmp(key, "uid")) {
                    if (item->userid[0] == '\0')
                        item->userid = pool_strdup(pool, value);
                } else if (!strcmp(key, "cn")) {
                    if (item->registered_name[0] == '\0')
                        item->registered_name = pool_strdup(pool, value);
                } else if (!strcmp(key, "displayName")) {
                    if (item->display_name[0] == '\0')
                        item->display_name = pool_strdup(pool, value);
                } else if (!strcmp(key, "ou")) {
                    if (item->affiliation[0] == '\0')
                        item->affiliation = pool_strdup(pool, value);
                } else if (!strcmp(key, "telephoneNumber")) {
                    if (item->phone[0] == '\0')
                        item->phone = pool_strdup(pool, value);
                } else if (!strcmp(key, "mail")) {
                    if (item->email[0] == '\0')
                        item->email = pool_strdup(pool, value);
                }
            }
            ldap_value_free(vals);
        }

        entry = ldap_next_entry(ld, entry);
    }
    ldap_msgfree(result);
    ldap_unbind(ld);

    qsort(lookup->entries, lookup->count, sizeof(struct lookup_item *),
          lookup_ldap_sort);

    lookup->current = (lookup->count > 0) ? 1 : 0;
    return(T);
}
#else

BOOL
lookup_ldap(struct lookup *lookup, char *query)
{
    session_message(lookup->session, "Directory is not enabled");
    session_log(lookup->session, "LDAP is not enabled");
    return(NIL);
}

#endif
