/* 
 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2008 Sun Microsystems, Inc. All Rights Reserved 
 * Use is subject to license terms. 
 * Some preexisting portions Copyright 1997-2001 Netscape Communications Corp.
 * All rights reserved.
 */
/************************************************************

 testpostop.c

 This source file provides examples of post-operation plug-in 
 functions.  The server calls these plug-in functions after 
 executing certain LDAP operations:

 * testpostop_add    (called after an LDAP add        operation)  
 * testpostop_mod    (called after an LDAP modify     operation)
 * testpostop_del    (called after an LDAP delete     operation)
 * testpostop_modrdn (called after an LDAP modify RDN operation)

   Each functions writes the DN of the impacted entry to the server
   logs and log more information to a change log specific to this
   plug-in.

 ACTIVATING THIS PLUG-IN
 -----------------------
 1. Build the library containing the plug-in.

 2. Configure the server to log plug-in messages.

    $ dsconf set-log-prop error level:err-plugins

 3. Configure the server to load the plug-in.
     
    $ dsconf create-plugin -H <lib-path> -F testpostop_init -Y postoperation "Test Postop"
    $ dsconf set-plugin-prop "Test Postop" depends-on-type:database feature:test-postop version:7.0 desc:"Sample post-operation plug-in"
    $ dsconf enable-plugin "Test Postop"
    Directory Server needs to be restarted for changes to take effect
    $ dsadm restart <inst-path>

*************************************************************/
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#ifndef _WIN32
#include <libgen.h>
#endif
#include "slapi-plugin.h"

#define SLAPD_LOGGING 1
#define _ADD    0
#define _MOD    1
#define _DEL    2
#define _MODRDN 3

Slapi_PluginDesc postop_desc = {
    "test-postop",                     /* plug-in identifier      */
    "Oracle Corporation (test)",       /* vendor name             */
    "11.1.1.3.0",                      /* plug-in revision number */
    "Sample post-operation plug-in"    /* plug-in description     */
};

static Slapi_ComponentId * postop_id;  /* Used to set log            */

char * changelogfile = NULL;           /* Write changes to this file */
#define NULL_CHANGELOG_WARNING_ID             0L
#define CANNOT_APPEND_TO_CHANGELOG_WARNING_ID 1L
static void write_changelog(int optype, char *dn, void * change, int flag);

/* Current time is a function defined in the server.                 */
time_t current_time(void);

/* The change log resides next to the error log. We locate the error
 * log by reading the top level configuration entry, which has
 * dn: cn=config, then by checking the nsslapd-errorlog attribute
 * value to find the path.                                           */
int
testpostop_set_log(Slapi_PBlock * pb)
{
    Slapi_DN    * confdn = NULL;       /* DN for configuration entry */
    Slapi_Entry * config = NULL;       /* Configuration entry        */
    char        * errlog = NULL;       /* Errors log file name       */
    char        * logdir = NULL;       /* Errors log directory name  */
    FILE        * fp;
    int           rc = 0;

#ifdef _WIN32
    char drive[_MAX_DRIVE];            /* Windows disk drive         */
    char dir[_MAX_DIR];                /* Windows directory name     */
    char fname[_MAX_FNAME];            /* Windows file name          */
    char ext[_MAX_EXT];                /* Windows file extension     */
    char tmp_path[_MAX_PATH];          /* Working log directory name */
#endif

    /* Be sure to set the change log only once.                      */
    if (changelogfile != NULL) return (rc);

    confdn = slapi_sdn_new_dn_byval("cn=config");
    if (confdn == NULL) return (rc);
    rc |= slapi_search_internal_get_entry(confdn, NULL, &config, postop_id); 
    if (rc != 0) {
        slapi_log_info_ex(
            SLAPI_LOG_INFO_AREA_PLUGIN,
            SLAPI_LOG_INFO_LEVEL_DEFAULT,
            SLAPI_LOG_NO_MSGID,
            SLAPI_LOG_NO_CONNID,
            SLAPI_LOG_NO_OPID,
            "testpostop_init in test-postop plug-in",
            "Failed to read configuration entry: %d\n", rc
        );
        return (rc);
    }
    slapi_sdn_free(&confdn); 

    errlog = slapi_entry_attr_get_charptr(config, "nsslapd-errorlog");
    slapi_entry_free(config);
    config = NULL;

#ifdef _WIN32
    _splitpath(errlog, drive, dir, fname, ext);
    logdir = slapi_ch_strdup((char *)dir);
    _makepath(tmp_path, drive, logdir, "changelog", "txt");
    changelogfile = slapi_ch_strdup((char *)tmp_path);
#else
    logdir = slapi_ch_strdup((char *)dirname(errlog));
    changelogfile = slapi_ch_malloc(strlen(logdir)+strlen("/changelog.txt")+1);
    sprintf(changelogfile, "%s%s", logdir, "/changelog.txt");
#endif

    slapi_ch_free_string(&errlog);
    slapi_ch_free_string(&logdir);
    
    if (changelogfile == NULL) {       /* In case of emergency...    */
        slapi_log_warning_ex(
            NULL_CHANGELOG_WARNING_ID,
            SLAPI_LOG_NO_MSGID,
            SLAPI_LOG_NO_CONNID,
            SLAPI_LOG_NO_OPID,
            "write_changelog in test-postop plug-in",
            "No changelog file for test-postop plug-in",
            "Path to changelog file is NULL!\n"
        );
        return (-1);
    }

    if ((fp = fopen(changelogfile, "ab")) == NULL) {
        slapi_log_warning_ex(
            CANNOT_APPEND_TO_CHANGELOG_WARNING_ID,
            SLAPI_LOG_NO_MSGID,
            SLAPI_LOG_NO_CONNID,
            SLAPI_LOG_NO_OPID,
            "testpostop_set_log in test-postop plug-in",
            "Write error in test-postop plug-in",
            "Plug-in cannot create log file %s\n", changelogfile
        );
        return (-1);
    }
    fprintf(fp, "***** File generated by test-postop plug-in. *****\n");
    fclose(fp);

    return (rc);
}

/* Free the change log file name.                                    */
int
testpostop_free_log(Slapi_PBlock * pb)
{
    slapi_ch_free_string(&changelogfile);
    return 0;
}

/* Log the DN of the added entry. Write the entry to the change log. */
int
testpostop_add(Slapi_PBlock * pb)
{
    char        * dn;                  /* DN of entry to add         */
    Slapi_Entry * entry;               /* Entry to add               */
    int           is_repl = 0;         /* Is this replication?       */
    int           connId, opId, rc = 0;
    long          msgId;

    rc |= slapi_pblock_get(pb, SLAPI_ADD_TARGET,              &dn);
    rc |= slapi_pblock_get(pb, SLAPI_ADD_ENTRY,               &entry);
    rc |= slapi_pblock_get(pb, SLAPI_OPERATION_MSGID,         &msgId);
    rc |= slapi_pblock_get(pb, SLAPI_CONN_ID,                 &connId);
    rc |= slapi_pblock_get(pb, SLAPI_OPERATION_ID,            &opId);
    rc |= slapi_pblock_get(pb, SLAPI_IS_REPLICATED_OPERATION, &is_repl);

    if (rc != 0) return (rc);
    slapi_log_info_ex(
        SLAPI_LOG_INFO_AREA_PLUGIN,
        SLAPI_LOG_INFO_LEVEL_DEFAULT,
        msgId,
        connId,
        opId,
        "testpostop_add in test-postop plug-in",
        "Added entry (%s)\n", dn
    );

    /* In general, do not interfere in replication operations.       */
    /* Log the DN and the entry to the change log file.              */
    if (!is_repl) write_changelog(_ADD, dn, (void *) entry, 0);

    return (rc);
}

/* Log the DN of the modified entry. Write the entry to the change log. */
int
testpostop_mod(Slapi_PBlock * pb)
{
    char    *  dn;                     /* DN of entry to modify      */
    LDAPMod ** mods;                   /* Modifications to apply     */
    int        is_repl = 0;            /* Is this replication?       */
    int        connId, opId, rc = 0;
    long       msgId;

    rc |= slapi_pblock_get(pb, SLAPI_MODIFY_TARGET,           &dn);
    rc |= slapi_pblock_get(pb, SLAPI_MODIFY_MODS,             &mods);
    rc |= slapi_pblock_get(pb, SLAPI_OPERATION_MSGID,         &msgId);
    rc |= slapi_pblock_get(pb, SLAPI_CONN_ID,                 &connId);
    rc |= slapi_pblock_get(pb, SLAPI_OPERATION_ID,            &opId);
    rc |= slapi_pblock_get(pb, SLAPI_IS_REPLICATED_OPERATION, &is_repl);
    
    if (rc != 0) return (rc);
    slapi_log_info_ex(
        SLAPI_LOG_INFO_AREA_PLUGIN,
        SLAPI_LOG_INFO_LEVEL_DEFAULT,
        msgId,
        connId,
        opId,
        "testpostop_mod in test-postop plug-in",
        "Modified entry (%s)\n", dn
    );

    /* In general, do not interfere in replication operations.       */
    /* Log the DN and the modifications made to the change log file. */
    if (!is_repl) write_changelog(_MOD, dn, (void *) mods, 0);

    return (rc);
}

/* Log the DN of the deleted entry. Write the DN to the change log.  */
int
testpostop_del( Slapi_PBlock *pb )
{
    char * dn;                         /* DN of entry to delete      */
    int    is_repl = 0;                /* Is this replication?       */
    int    connId, opId, rc = 0;
    long   msgId;

    rc |= slapi_pblock_get(pb, SLAPI_DELETE_TARGET,           &dn);
    rc |= slapi_pblock_get(pb, SLAPI_OPERATION_MSGID,         &msgId);
    rc |= slapi_pblock_get(pb, SLAPI_CONN_ID,                 &connId);
    rc |= slapi_pblock_get(pb, SLAPI_OPERATION_ID,            &opId);
    rc |= slapi_pblock_get(pb, SLAPI_IS_REPLICATED_OPERATION, &is_repl);
    
    if (rc != 0) return (rc);
    slapi_log_info_ex(
        SLAPI_LOG_INFO_AREA_PLUGIN,
        SLAPI_LOG_INFO_LEVEL_DEFAULT,
        msgId,
        connId,
        opId,
        "testpostop_del in test-postop plug-in",
        "Deleted entry (%s)\n", dn
    );

    /* In general, do not interfere in replication operations.       */
    /* Log the DN of the deleted entry to the change log.            */
    if (!is_repl) write_changelog(_DEL, dn, NULL, 0);

    return (rc);
}

/* Log the DN of the renamed entry. Write more info to the change log. */
int
testpostop_modrdn( Slapi_PBlock *pb )
{
    char * dn;                         /* DN of entry to rename      */
    char * newrdn;                     /* New RDN                    */
    int    dflag;                      /* Delete the old RDN?        */
    int    is_repl = 0;                /* Is this replication?       */
    int    connId, opId, rc = 0;
    long   msgId;

    rc |= slapi_pblock_get(pb, SLAPI_MODRDN_TARGET,           &dn);
    rc |= slapi_pblock_get(pb, SLAPI_MODRDN_NEWRDN,           &newrdn);
    rc |= slapi_pblock_get(pb, SLAPI_MODRDN_DELOLDRDN,        &dflag);
    rc |= slapi_pblock_get(pb, SLAPI_OPERATION_MSGID,         &msgId);
    rc |= slapi_pblock_get(pb, SLAPI_CONN_ID,                 &connId);
    rc |= slapi_pblock_get(pb, SLAPI_OPERATION_ID,            &opId);
    rc |= slapi_pblock_get(pb, SLAPI_IS_REPLICATED_OPERATION, &is_repl);

    if (rc != 0) return (rc);
    slapi_log_info_ex(
        SLAPI_LOG_INFO_AREA_PLUGIN,
        SLAPI_LOG_INFO_LEVEL_DEFAULT,
        msgId,
        connId,
        opId,
        "testpostop_modrdn in test-postop plug-in",
        "Modrdn entry (%s)\n", dn
    );

    /* In general, do not interfere in replication operations.       */
    /* Log the DN of the renamed entry, its new RDN, and the flag
     * indicating whether the old RDN was removed to the change log. */
    if (!is_repl) write_changelog(_MODRDN, dn, (void *) newrdn, dflag);

    return (rc);
}

/* Register the plug-in with the server. */
#ifdef _WIN32
__declspec(dllexport)
#endif
int
testpostop_init(Slapi_PBlock * pb)
{
    int rc = 0;                        /* 0 means success            */
    rc |= slapi_pblock_set(            /* Plug-in API version        */
        pb,
        SLAPI_PLUGIN_VERSION,
        SLAPI_PLUGIN_CURRENT_VERSION
    );
    rc |= slapi_pblock_set(            /* Plug-in description        */
        pb,
        SLAPI_PLUGIN_DESCRIPTION,
        (void *) &postop_desc
    );
    rc |= slapi_pblock_set(            /* Open log at startup        */
        pb,
        SLAPI_PLUGIN_START_FN,
        (void *) testpostop_set_log
    );
    rc |= slapi_pblock_set(            /* Post-op add function       */
        pb,
        SLAPI_PLUGIN_POST_ADD_FN,
        (void *) testpostop_add
    );
    rc |= slapi_pblock_set(            /* Post-op modify function    */
        pb,
        SLAPI_PLUGIN_POST_MODIFY_FN,
        (void *) testpostop_mod
    );
    rc |= slapi_pblock_set(            /* Post-op delete function    */
        pb,
        SLAPI_PLUGIN_POST_DELETE_FN,
        (void *) testpostop_del
    );
    rc |= slapi_pblock_set(            /* Post-op modrdn function    */
        pb,
        SLAPI_PLUGIN_POST_MODRDN_FN,
        (void *) testpostop_modrdn
    );
    rc |= slapi_pblock_set(            /* Close log on shutdown      */
        pb,
        SLAPI_PLUGIN_CLOSE_FN,
        (void *) testpostop_free_log
    );
    /* Plug-in identifier is required for internal search.           */
    rc |= slapi_pblock_get(pb, SLAPI_PLUGIN_IDENTITY, &postop_id);
    return (rc);
}

/* Function for generating a newly allocated string that contains the
   specified time.  The time is expressed as generalizedTime, except 
   without the time zone.                                            */
static char*
format_localTime( time_t timeval )
{
    char* into;
    struct tm t;
#ifdef _WIN32
    memcpy (&t, localtime (&timeval), sizeof(t));
#else
    localtime_r (&timeval, &t);
#endif
    
    /* Allocate memory for the formatted string.  (slapi_ch_malloc()
       should be used in server plug-ins instead of malloc().)
       This string is freed by the calling function write_changelog(). */
    into = slapi_ch_malloc(15);
    sprintf (into, "%.4li%.2i%.2i%.2i%.2i%.2i",
             1900L + t.tm_year, 1 + t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
    return into;
}

/* Logs information on an operation to a change log file. 
   Parameters;
   - optype is type of LDAP operation to record:
     - _ADD for LDAP add operations
     - _MOD for LDAP modify operations
     - _DEL for LDAP delete operations
     - _MODRDN for LDAP modify RDN operations
   - dn is DN of the entry affected by the operation.
   - change is information about the operation performed.
     The type of information depends on the value of optype:
     - For _ADD, it is the newly added entry (Slapi_Entry).
     - For _MOD, it is the list of modifications made (array of
       LDAPMod).
     - For _DEL, it is NULL.
     - For _MODRDN, it is the new RDN of the entry.
   - flag is only used for LDAP modify RDN operations.
     It represents the flag that indicates whether or not
     the old RDN has been removed.
*/
static void
write_changelog(
    int                 optype,
    char                *dn,
    void                *change,
    int                 flag
)
{
    LDAPMod     **mods;
    Slapi_Entry *e;
    char        *newrdn, *tmp, *tmpsave;
    int         len, i, j;
    FILE        *fp;
    char*       timestr;

    /* Open the change log file */
    if (changelogfile == NULL) {
        slapi_log_warning_ex(
            CANNOT_APPEND_TO_CHANGELOG_WARNING_ID,
            SLAPI_LOG_NO_MSGID,
            SLAPI_LOG_NO_CONNID,
            SLAPI_LOG_NO_OPID,
            "write_changelog in test-postop plug-in",
            "Write error in test-postop plug-in",
            "Plug-in cannot append to log file\n"
        );
        return;
    }

    if ((fp = fopen(changelogfile, "ab")) == NULL) {
        slapi_log_warning_ex(
            CANNOT_APPEND_TO_CHANGELOG_WARNING_ID,
            SLAPI_LOG_NO_MSGID,
            SLAPI_LOG_NO_CONNID,
            SLAPI_LOG_NO_OPID,
            "testpostop_set_log in test-postop plug-in",
            "Write error in test-postop plug-in",
            "Plug-in cannot create log file %s\n", changelogfile
        );
        return;
    }

    /* Log the current time of the operation in generalizedTime form */
    timestr = format_localTime( current_time() );
    fprintf( fp, "time: %s\n", timestr );
    slapi_ch_free( ( void ** ) &timestr);

    /* Print the DN of the entry affected by the operation. */
    fprintf( fp, "dn: %s\n", dn );

    /* Log information about the operation */
    switch ( optype ) {
    case _MOD:
        /* For modify operations, log the attribute type
           that has been added, replaced, or deleted. */
        fprintf( fp, "changetype: modify\n" );
        mods = (LDAPMod **)change;
        for ( j = 0; mods[j] != NULL; j++ ) {
            switch ( mods[j]->mod_op & ~LDAP_MOD_BVALUES ) {
            case LDAP_MOD_ADD:
                fprintf( fp, "add: %s\n", mods[j]->mod_type );
                break;

            case LDAP_MOD_DELETE:
                fprintf( fp, "delete: %s\n", mods[j]->mod_type );
                break;

            case LDAP_MOD_REPLACE:
                fprintf( fp, "replace: %s\n", mods[j]->mod_type );
                break;
            }

            for ( i = 0; mods[j]->mod_bvalues != NULL &&
                    mods[j]->mod_bvalues[i] != NULL; i++ ) {
                /* XXX should handle binary values XXX */
                fprintf( fp, "%s: %s\n", mods[j]->mod_type,
                    mods[j]->mod_bvalues[i]->bv_val );
            }
            fprintf( fp, "-\n" );
        }
        break;

    case _ADD:
        /* For LDAP add operations, log the newly added entry. */
        e = (Slapi_Entry *)change;
        fprintf( fp, "changetype: add\n" );
        /* Get the LDIF string representation of the entry. */
        tmp = slapi_entry2str( e, &len );
        tmpsave = tmp;
        /* Skip the first line, which is the dn: line */
        while (( tmp = strchr( tmp, '\n' )) != NULL ) {
            tmp++;
            if ( !isspace( *tmp )) {
                break;
            }
        }
        fprintf( fp, "%s", tmp );
        slapi_ch_free( (void **)&tmpsave );
        break;

    case _DEL:
        /* The DN has already been logged, so just log the type
         * of operation performed. */
        fprintf( fp, "changetype: delete\n" );
        break;

    case _MODRDN:
        /* For the LDAP modify RDN operation, log the new RDN
           and the flag indicating whether or not the old RDN
           was removed. */
        newrdn = (char *)change;
        fprintf( fp, "changetype: modrdn\n" );
        fprintf( fp, "newrdn: %s\n", newrdn );
        fprintf( fp, "deleteoldrdn: %d\n", flag ? 1 : 0 );
    }
    fprintf( fp, "\n" );

    fclose(fp);
}
