sync-procs.tcl

Does not contain a contract.

Location:
/packages/acs-authentication/tcl/sync-procs.tcl

Related Files

[ hide source ] | [ make this the default ]

File Contents

ad_library {
    API for managing synchronization of user data.

    @creation-date 2003-09-05
    @author Lars Pind (lars@collaboraid.biz)
    @cvs-id $Id: sync-procs.tcl,v 1.44.2.7 2024/08/20 08:58:02 gustafn Exp $
}

namespace eval auth {}
namespace eval auth::sync {}
namespace eval auth::sync::job {}
namespace eval auth::sync::get_doc {}
namespace eval auth::sync::get_doc::http {}
namespace eval auth::sync::get_doc::file {}
namespace eval auth::sync::entry {}
namespace eval auth::sync::process_doc {}
namespace eval auth::sync::process_doc::ims {}

#####
#
# auth::sync::job namespace
#
#####

d_proc -public auth::sync::job::get {
    {-job_id:required}
    {-array:required}
} {
    Get information about a batch job in an array.

    @param job_id        The ID of the batch job you're ending.
    @param array         Name of an array into which you want the information.
    @author Lars Pind (lars@collaboraid.biz)
} {
    upvar 1 $array row

    db_1row select_job {} -column_array row

    set row(log_url) [export_vars -base "[ad_url]/acs-admin/auth/batch-job" { job_id }]
}

d_proc -public auth::sync::job::get_entries {
    {-job_id:required}
} {
    Get a list of entry_ids of the job log entries, ordered by entry_time.

    @param job_id        The ID of the batch job you're ending.

    @author Lars Pind (lars@collaboraid.biz)
} {
    return [db_list select_entries { select entry_id from auth_batch_job_entries where job_id = :job_id order by entry_time }]
}

d_proc -public auth::sync::job::get_authority_id {
    {-job_id:required}
} {
    Get the authority_id from a job_id. Cached.

    @param job_id        The ID of the batch job you're ending.
    @author Lars Pind (lars@collaboraid.biz)
} {
    return [util_memoize [list auth::sync::job::get_authority_id_not_cached $job_id]]
}

d_proc -private auth::sync::job::get_authority_id_flush {
    {-job_id ""}
} {
    Flush cache

    @param job_id        The ID of the batch job you're ending.
    @author Lars Pind (lars@collaboraid.biz)
} {
    if { $job_id ne "" } {
        util_memoize_flush [list auth::sync::job::get_authority_id_not_cached $job_id]
    } else {
        util_memoize_flush_regexp [list auth::sync::job::get_authority_id_not_cached .*]
    }
}

d_proc -private auth::sync::job::get_authority_id_seed {
    {-job_id:required}
    {-authority_id:required}
} {
    Flush cache

    @param job_id        The ID of the batch job you're ending.
    @author Lars Pind (lars@collaboraid.biz)
} {
    util_memoize_seed [list auth::sync::job::get_authority_id_not_cached $job_id$authority_id
}

d_proc -private auth::sync::job::get_authority_id_not_cached {
    job_id
} {
    Get the authority_id from a job_id. Not cached.

    @param job_id        The ID of the batch job you're ending.
    @author Lars Pind (lars@collaboraid.biz)
    @see auth::sync::job::get_authority_id
} {
    return [db_string select_auth_id { select authority_id from auth_batch_jobs where job_id = :job_id }]
}

d_proc -public auth::sync::job::start {
    {-job_id ""}
    {-authority_id:required}
    {-interactive:boolean}
    {-creation_user ""}
} {
    Record the beginning of a job.

    @param authority_id      The ID of the authority you're trying to sync
    @param interactive       Set this if this is an interactive job, i.e. it's initiated by a user.
    @return job_id           An ID for the new batch job. Used when calling other procs in this API.
    @author Lars Pind (lars@collaboraid.biz)
} {
    db_transaction {
        if { $job_id eq "" } {
            set job_id [db_nextval "auth_batch_jobs_job_id_seq"]
        }

        if { $interactive_p && $creation_user eq "" } {
            set creation_user [ad_conn user_id]
        }

        set interactive_p [db_boolean $interactive_p]

        db_dml job_insert {
            insert into auth_batch_jobs
            (job_id, interactive_p, creation_user, authority_id)
            values
            (:job_id, :interactive_p, :creation_user, :authority_id)
        }

    }

    # See the cache, we're going to need it shortly
    auth::sync::job::get_authority_id_seed -job_id $job_id -authority_id $authority_id

    return $job_id
}

d_proc -public auth::sync::job::end {
    {-job_id:required}
    {-message ""}
} {
    Record the end of a batch job. Closes out the transaction
    log and sends out notifications.

    @param job_id        The ID of the batch job you're ending.
    @return array list with result of auth::sync::job::get.
    @see auth::sync::job::get
    @author Lars Pind (lars@collaboraid.biz)
} {
    db_dml update_job_end {}

    # interactive_p, run_time_seconds, num_actions, num_problems
    get -job_id $job_id -array job

    set email_p [parameter::get_from_package_key \
                     -parameter SyncEmailConfirmationP \
                     -package_key "acs-authentication" \
                     -default 0]

    if { ![string is true -strict $job(interactive_p)] && $email_p } {
        # Only send out email if not an interactive job

        ad_try {
            acs_mail_lite::send -send_immediately \
                -to_addr [ad_system_owner] \
                -from_addr [ad_system_owner] \
                -subject "Batch user synchronization for $job(authority_pretty_name) complete" \
                -body "Batch user synchronization for $job(authority_pretty_name) is complete.

Authority         : $job(authority_pretty_name)
Running time      : $job(run_time_seconds) seconds
Number of actions : $job(num_actions)
Number of problems: $job(num_problems)
Job message       : $job(message)

To view the complete log, please visit\n$job(log_url)"
        } on error {errorMsg} {
            # We don't fail hard here, just log an error
            ad_log Error "Error sending registration confirmation to [ad_system_owner]: $errorMsg"
        }
    }

    return [array get job]
}

d_proc -public auth::sync::job::start_get_document {
    {-job_id:required}
} {
    Record the time that we're starting to get the document.

    @param job_id The ID of the batch job you're ending.
} {
    db_dml update_doc_start_time {}
}

d_proc -public auth::sync::job::end_get_document {
    {-job_id:required}
    {-doc_status:required}
    {-doc_message ""}
    {-document ""}
    {-snapshot:boolean}
} {
    Record the time that we've finished getting the document, and record the status.

    @param job_id The ID of the batch job you're ending.
    @param snapshot          Set this if this is a snapshot job, as opposed to an incremental ('event driven') job.
} {
    set snapshot_p [db_boolean $snapshot_p]

    db_dml update_doc_end {} -clobs [list $document]
}

d_proc -public auth::sync::job::create_entry {
    {-job_id:required}
    {-operation:required}
    {-username:required}
    {-user_id ""}
    {-success:boolean}
    {-message ""}
    {-element_messages ""}
} {
    Record a batch job entry.

    @param job_id The ID of the batch job you're ending.
    @param operation One of 'insert', 'update', or 'delete'.
    @param username The username of the user being inserted/updated/deleted.
    @param user_id The user_id of the local user account, if known.
    @param success Whether or not the operation went well.
    @param message Any error message to stick into the log.
    @return entry_id
} {
    set success_p_db [expr {$success_p ? "t" : "f"}]

    set entry_id [db_nextval "auth_batch_job_entry_id_seq"]

    db_dml insert_entry {} -clobs [list $element_messages]

    return $entry_id
}

d_proc -public auth::sync::job::get_entry {
    {-entry_id:required}
    {-array:required}
} {
    Get information about a log entry
} {
    upvar 1 $array row

    db_1row select_entry {
        select e.entry_id,
               e.job_id,
               e.entry_time,
               e.operation,
               j.authority_id,
               e.username,
               e.user_id,
               e.success_p,
               e.message,
               e.element_messages
        from   auth_batch_job_entries e,
               auth_batch_jobs j
        where  e.entry_id = :entry_id
        and    j.job_id = e.job_id
    } -column_array row
}


d_proc -public auth::sync::job::action {
    {-job_id:required}
    {-operation:required}
    {-username:required}
    {-array ""}
} {
    Inserts/updates/deletes a user, depending on the operation.

    @param job_id        The job which this is part of for logging purposes.
    @param operation     'insert', 'update', 'delete', or 'snapshot'.
    @param username      The username which this action refers to.
    @param array         Name of an array containing the relevant registration elements.
                         Not required if this is a delete operation.
    @return entry_id of newly created entry
} {
    if { $operation ne "delete" && $array eq "" } {
        error "Switch -array is required when operation is not delete"
    }
    upvar 1 $array user_info

    set entry_id {}
    set user_id {}

    set authority_id [get_authority_id -job_id $job_id]

    db_transaction {
        set user_id [acs_user::get_by_username \
                         -authority_id $authority_id \
                         -username $username]

        set success_p 1
        array set result {
            message {}
            element_messages {}
        }

        switch $operation {
            snapshot {
                if { $user_id ne "" } {
                    # user exists, it's an update
                    set operation "update"
                } else {
                    # user does not exist, it's an insert
                    set operation "insert"
                }
            }
            update - delete {
                if { $user_id eq "" } {
                    # Updating/deleting a user that doesn't exist
                    set success_p 0
                    set result(message) "A user with username '$username' does not exist"
                } else {
                    acs_user::get -user_id $user_id -array existing_user_info
                    if {$existing_user_info(member_state) eq "banned"} {
                        # Updating/deleting a user that's already deleted
                        set success_p 0
                        set result(message) "The user with username '$username' has been deleted (banned)"
                    }
                }
            }
            insert {
                if { $user_id ne "" } {
                    acs_user::get -user_id $user_id -array existing_user_info
                    if { $existing_user_info(member_state) ne "banned" } {
                        # Inserting a user that already exists (and is not deleted)
                        set success_p 0
                        set result(message) "A user with username '$username' already exists"
                    }
                }
            }
        }

        # Only actually perform the action if we didn't already encounter a problem
        if { $success_p } {
            ad_try {
                switch $operation {
                    "insert" {
                        # We set email_verified_p to 't', because we trust the email we get from the remote system
                        set user_info(email_verified_p) t

                        array set result [auth::create_local_account \
                                              -authority_id $authority_id \
                                              -username $username \
                                              -array user_info]

                        if { $result(creation_status) ne "ok" } {
                            set result(message) $result(creation_message)
                            set success_p 0
                        } else {
                            set user_id $result(user_id)

                            set add_to_dotlrn_p [parameter::get_from_package_key \
                                                     -parameter SyncAddUsersToDotLrnP \
                                                     -package_key "acs-authentication" \
                                                     -default 0]

                            if { $add_to_dotlrn_p } {
                                # Add user to .LRN
                                # Beware that this creates a portal and lots of other things for each user

                                set type [parameter::get_from_package_key \
                                              -parameter SyncDotLrnUserType \
                                              -package_key "acs-authentication" \
                                              -default "student"]

                                set can_browse_p [parameter::get_from_package_key \
                                                      -parameter SyncDotLrnAccessLevel \
                                                      -package_key "acs-authentication" \
                                                      -default 1]

                                set read_private_data_p [parameter::get_from_package_key \
                                                             -parameter SyncDotLrnReadPrivateDataP \
                                                             -package_key "acs-authentication" \
                                                             -default 1]

                                dotlrn::user_add \
                                    -id $user_info(email) \
                                    -type $type \
                                    -can_browse=$can_browse_p \
                                    -user_id $user_id

                                dotlrn_privacy::set_user_is_non_guest \
                                    -user_id $user_id \
                                    -value $read_private_data_p

                            }
                        }

                        # We ignore account_status
                    }
                    "update" {
                        # We set email_verified_p to 't', because we trust the email we get from the remote system
                        set user_info(email_verified_p) t

                        array set result [auth::update_local_account \
                                              -authority_id $authority_id \
                                              -username $username \
                                              -array user_info]

                        if { $result(update_status) ne "ok" } {
                            set result(message) $result(update_message)
                            set success_p 0
                        } else {
                            set user_id $result(user_id)
                        }
                    }
                    "delete" {
                        array set result [auth::delete_local_account \
                                              -authority_id $authority_id \
                                              -username $username]

                        if { $result(delete_status) ne "ok" } {
                            set result(message) $result(delete_message)
                            set success_p 0
                        } else {
                            set user_id $result(user_id)
                        }
                    }
                }
            } on error {errorMsg} {
                # Get errorInfo and log it
                ad_log Error "Error during batch synchronization job: $errorMsg"
                set success_p 0
                set result(message) $::errorInfo
            }
        }

        # Make a log entry
        set entry_id [auth::sync::job::create_entry \
                          -job_id $job_id \
                          -operation $operation \
                          -username $username \
                          -user_id $user_id \
                          -success=$success_p \
                          -message $result(message) \
                          -element_messages $result(element_messages)]
    }

    return $entry_id
}

d_proc -public auth::sync::job::snapshot_delete_remaining {
    {-job_id:required}
} {
    Deletes the users that weren't included in the snapshot.
} {
    set authority_id [get_authority_id -job_id $job_id]

    set usernames [db_list select_user_ids {
        select username
        from   cc_users
        where  authority_id = :authority_id
        and    user_id not in (select user_id from auth_batch_job_entries where job_id = :job_id and authority_id = :authority_id)
        and    member_state != 'banned'
    }]

    foreach username $usernames {
        auth::sync::job::action \
            -job_id $job_id \
            -operation "delete" \
            -username $username
    }
}


#####
#
# auth::sync namespace
#
#####

d_proc -public auth::sync::purge_jobs {
    {-num_days ""}
} {
    Purge jobs that are older than KeepBatchLogDays days.
} {
    if { $num_days eq "" } {
        set num_days [parameter::get_from_package_key \
                          -parameter KeepBatchLogDays \
                          -package_key "acs-authentication" \
                          -default 0]
    }

    if {![string is integer -strict $num_days]} {
        error "num_days ($num_days) has to be an integer"
    }

    if { $num_days > 0 } {
        db_dml purge_jobs {}
    }
}

d_proc -private auth::sync::get_sync_elements {
    {-user_id ""}
    {-authority_id ""}
} {
    Get a Tcl list of the user profile elements controlled by the batch synchronization.
    These should not be editable by the user. Supply either user_id or authority_id.
    Authority_id is the most efficient.
} {
    if { $authority_id eq "" } {
        if { $user_id eq "" } {
            error "You must supply either user_id or authority_id"
        }
        set authority_id [acs_user::get_element -user_id $user_id -element authority_id]
    }

    #
    # Try to sync. Many authorities do no support auth_sync_process,
    # but these will issue an exception below.
    #
    # TODO: using a different error-code could make the code saver, by
    # just ignoring such cases.
    #
    set elms [list]
    ad_try  {
        set elms [auth::sync::GetElements -authority_id $authority_id]
    } trap {AD EXCEPTION NO_AUTH_SYNC} {} {
        # authentication does not support auth_sync_process"
        ns_log notice "authentication of authority_id $authority_id does not support auth_sync_process"
    } on error {errorMsg dict} {
        ad_log error "auth::sync::GetElements raised: $errorMsg ($dict)"
    }

    return $elms
}

ad_proc -private auth::sync::sweeper {} {
    db_foreach select_authorities {
        select authority_id
        from   auth_authorities
        where  enabled_p = 't'
        and    batch_sync_enabled_p = 't'
    } {
        auth::authority::batch_sync \
            -authority_id $authority_id
    }
}

d_proc -private auth::sync::GetDocument {
    {-authority_id:required}
} {
    Wrapper for the GetDocument operation of the auth_sync_retrieve service contract.
} {
    set impl_id [auth::authority::get_element -authority_id $authority_id -element "get_doc_impl_id"]

    if { $impl_id eq "" } {
        # No implementation of GetDocument
        set authority_pretty_name [auth::authority::get_element -authority_id $authority_id -element "pretty_name"]
        error "The authority '$authority_pretty_name' doesn't support GetDocument"
    }

    set parameters [auth::driver::get_parameter_values \
                        -authority_id $authority_id \
                        -impl_id $impl_id]

    return [acs_sc::invoke \
                -error \
                -contract "auth_sync_retrieve" \
                -impl_id $impl_id \
                -operation GetDocument \
                -call_args [list $parameters]]
}

d_proc -private auth::sync::ProcessDocument {
    {-authority_id:required}
    {-job_id:required}
    {-document:required}
} {
    Wrapper for the ProcessDocument operation of the auth_sync_process service contract.
} {
    set impl_id [auth::authority::get_element -authority_id $authority_id -element "process_doc_impl_id"]

    if { $impl_id eq "" } {
        # No implementation of auth_sync_process
        set authority_pretty_name [auth::authority::get_element -authority_id $authority_id -element "pretty_name"]
        error "The authority '$authority_pretty_name' doesn't support auth_sync_process"
    }

    set parameters [auth::driver::get_parameter_values \
                        -authority_id $authority_id \
                        -impl_id $impl_id]

    return [acs_sc::invoke \
                -error \
                -contract "auth_sync_process" \
                -impl_id $impl_id \
                -operation ProcessDocument \
                -call_args [list $job_id $document $parameters]]
}

d_proc -private auth::sync::GetAcknowledgementDocument {
    {-authority_id:required}
    {-job_id:required}
    {-document:required}
} {
    Wrapper for the GetAckDocument operation of the auth_sync_process service contract.
} {
    set impl_id [auth::authority::get_element -authority_id $authority_id -element "process_doc_impl_id"]

    if { $impl_id eq "" } {
        # No implementation of auth_sync_process
        set authority_pretty_name [auth::authority::get_element -authority_id $authority_id -element "pretty_name"]
        ad_raise NO_AUTH_SYNC "The authority '$authority_pretty_name' doesn't support auth_sync_process"
    }

    set parameters [auth::driver::get_parameter_values \
                        -authority_id $authority_id \
                        -impl_id $impl_id]

    return [acs_sc::invoke \
                -error \
                -contract "auth_sync_process" \
                -impl_id $impl_id \
                -operation GetAcknowledgementDocument \
                -call_args [list $job_id $document $parameters]]
}

d_proc -private auth::sync::GetElements {
    {-authority_id:required}
} {
    Wrapper for the GetElements operation of the auth_sync_process service contract.
} {
    set impl_id [auth::authority::get_element -authority_id $authority_id -element "process_doc_impl_id"]

    if { $impl_id eq "" } {
        # No implementation of auth_sync_process
        set authority_pretty_name [auth::authority::get_element -authority_id $authority_id -element "pretty_name"]
        ad_raise NO_AUTH_SYNC "The authority '$authority_pretty_name' doesn't support auth_sync_process"
    }

    set parameters [auth::driver::get_parameter_values \
                        -authority_id $authority_id \
                        -impl_id $impl_id]

    return [acs_sc::invoke \
                -error \
                -contract "auth_sync_process" \
                -impl_id $impl_id \
                -operation GetElements \
                -call_args [list $parameters]]
}





#####
#
# auth::sync::get_doc::http namespace
#
#####

ad_proc -private auth::sync::get_doc::http::register_impl {} {
    Register this implementation
} {
    set spec {
        contract_name "auth_sync_retrieve"
        owner "acs-authentication"
        name "HTTPGet"
        pretty_name "HTTP GET"
        aliases {
            GetDocument auth::sync::get_doc::http::GetDocument
            GetParameters auth::sync::get_doc::http::GetParameters
        }
    }

    return [acs_sc::impl::new_from_spec -spec $spec]

}

ad_proc -private auth::sync::get_doc::http::unregister_impl {} {
    Unregister this implementation
} {
    acs_sc::impl::delete -contract_name "auth_sync_retrieve" -impl_name "HTTPGet"
}

ad_proc -private auth::sync::get_doc::http::GetParameters {} {
    Parameters for HTTP GetDocument implementation.
} {
    return {
        IncrementalURL {The URL from which to retrieve document for incremental update. Will retrieve this most of the time.}
        SnapshotURL {The URL from which to retrieve document for snapshot update. If specified, will get this once per month.}
    }
}

d_proc -private auth::sync::get_doc::http::GetDocument {
    parameters
} {
    Retrieve the document by HTTP
} {
    array set result {
        doc_status failed_to_conntect
        doc_message {}
        document {}
        snapshot_p f
    }

    array set param $parameters

    if { ($param(SnapshotURL) ne "" && [clock format [clock seconds] -format "%d"] eq "01")
         || $param(IncrementalURL) eq ""
     } {
        # On the first day of the month, we get a snapshot
        set url $param(SnapshotURL)
        set result(snapshot_p) "t"
    } else {
        # All the other days of the month, we get the incremental
        set url $param(IncrementalURL)
    }

    if { $url eq "" } {
        error "You must specify at least one URL to get."
    }

    set dict [util::http::get -url $url]
    set result(document) [dict get $dict page]

    set result(doc_status) "ok"

    return [array get result]
}



#####
#
# auth::sync::get_doc::file namespace
#
#####

ad_proc -private auth::sync::get_doc::file::register_impl {} {
    Register this implementation
} {
    set spec {
        contract_name "auth_sync_retrieve"
        owner "acs-authentication"
        name "LocalFilesystem"
        pretty_name "Local Filesystem"
        aliases {
            GetDocument auth::sync::get_doc::file::GetDocument
            GetParameters auth::sync::get_doc::file::GetParameters
        }
    }

    return [acs_sc::impl::new_from_spec -spec $spec]

}

ad_proc -private auth::sync::get_doc::file::unregister_impl {} {
    Unregister this implementation
} {
    acs_sc::impl::delete -contract_name "auth_sync_retrieve" -impl_name "LocalFilesystem"
}

ad_proc -private auth::sync::get_doc::file::GetParameters {} {
    Parameters for FILE GetDocument implementation.
} {
    return {
        IncrementalPath {The path to the document for incremental update. Will retrieve this most of the time.}
        SnapshotPath {The path to the document for snapshot update. If specified, will get this once per month.}
    }
}

d_proc -private auth::sync::get_doc::file::GetDocument {
    parameters
} {
    Retrieve the document from local filesystem
} {
    array set result {
        doc_status failed_to_conntect
        doc_message {}
        document {}
        snapshot_p f
    }

    array set param $parameters

    if { ($param(SnapshotPath) ne "" && [clock format [clock seconds] -format "%d"] eq "01")
         || $param(IncrementalPath) eq ""
     } {
        # On the first day of the month, we get a snapshot
        set path $param(SnapshotPath)
        set result(snapshot_p) "t"
    } else {
        # All the other days of the month, we get the incremental
        set path $param(IncrementalPath)
    }

    if { $path eq "" } {
        error "You must specify at least one path to get."
    }

    set result(document) [template::util::read_file $path]

    set result(doc_status) "ok"

    return [array get result]
}



#####
#
# auth::sync::process_doc::ims namespace
#
#####

ad_proc -private auth::sync::process_doc::ims::register_impl {} {
    Register this implementation
} {
    set spec {
        contract_name "auth_sync_process"
        owner "acs-authentication"
        name "IMS_Enterprise_v_1p1"
        pretty_name "IMS Enterprise 1.1"
        aliases {
            ProcessDocument auth::sync::process_doc::ims::ProcessDocument
            GetAcknowledgementDocument auth::sync::process_doc::ims::GetAcknowledgementDocument
            GetElements auth::sync::process_doc::ims::GetElements
            GetParameters auth::sync::process_doc::ims::GetParameters
        }
    }

    return [acs_sc::impl::new_from_spec -spec $spec]

}

ad_proc -private auth::sync::process_doc::ims::unregister_impl {} {
    Unregister this implementation
} {
    acs_sc::impl::delete -contract_name "auth_sync_process" -impl_name "IMS_Enterprise_v_1p1"
}

ad_proc -private auth::sync::process_doc::ims::GetParameters {} {
    Parameters for IMS Enterprise 1.1 auth_sync_process implementation.
} {
    return {
        Elements {List of elements covered by IMS batch synchronization, which we should prevent users from editing in OpenACS. Example: 'username email first_names last_name url'.}
    }
}


d_proc -private auth::sync::process_doc::ims::GetElements {
    parameters
} {
    Elements controlled by IMS Enterprise 1.1 auth_sync_process implementation.
} {
    array set param $parameters
    return $param(Elements)
}

d_proc -private auth::sync::process_doc::ims::ProcessDocument {
    job_id
    document
    parameters
} {
    Process IMS Enterprise 1.1 document.
} {
    set tree [xml_parse -persist $document]
    set root_node [xml_doc_get_first_node $tree]

    if { [xml_node_get_name $root_node] ne "enterprise" } {
        $tree delete
        error "Root node was not <enterprise>"
    }

    # Loop over <person> records
    foreach person_node [xml_node_get_children_by_name $root_node "person"] {
        switch [xml_node_get_attribute $person_node "recstatus"] {
            1 {
                set operation "insert"
            }
            2 {
                set operation "update"
            }
            3 {
                set operation "delete"
            }
            default {
                set operation "snapshot"
            }
        }

        # Initialize this record
        array unset user_info

        set username [xml_get_child_node_content_by_path $person_node { { userid } { sourcedid id } }]

        set user_info(email) [xml_get_child_node_content_by_path $person_node { { email } }]
        set user_info(url) [xml_get_child_node_content_by_path $person_node { { url } }]

        # We need a little more logic to deal with first_names/last_name, since they may not be split up in the XML
        set user_info(first_names) [xml_get_child_node_content_by_path $person_node { { name n given } }]
        set user_info(last_name) [xml_get_child_node_content_by_path $person_node { { name n family } }]

        if { $user_info(first_names) eq "" || $user_info(last_name) eq "" } {
            set formatted_name [xml_get_child_node_content_by_path $person_node { { name fn } }]
            if { $formatted_name ne "" || [string first " " $formatted_name] > -1 } {
                # Split, so everything up to the last space goes to first_names, the rest to last_name
                regexp {^(.+) ([^ ]+)$} $formatted_name match user_info(first_names) user_info(last_name)
            }
        }

        auth::sync::job::action \
            -job_id $job_id \
            -operation $operation \
            -username $username \
            -array user_info
    }
    $tree delete
}


d_proc -private auth::sync::process_doc::ims::GetAcknowledgementDocument {
    job_id
    document
    parameters
} {
    Generates an record-wise acknowledgement document in home-brewed
    adaptation of the IMS Enterprise v 1.1 spec.
} {
    set tree [xml_parse -persist $document]

    set root_node [xml_doc_get_first_node $tree]
    if { [xml_node_get_name $root_node] ne "enterprise" } {
        $tree delete
        error "Root node was not <enterprise>"
    }

    set timestamp [xml_get_child_node_content_by_path $root_node { { properties datetime } }]
    $tree delete

    append doc {<?xml version="1.0" encoding="} [ns_config "ns/parameters" OutputCharset] {"?>} \n
    append doc {<enterprise>} \n
    append doc {  <properties>} \n
    append doc {    <type>acknowledgement</type>} \n
    append doc {    <datetime>} $timestamp {</datetime>} \n
    append doc {  </properties>} \n

    array set recstatus {
        insert 1
        update 2
        delete 3
    }

    # Loop over successful actions
    db_foreach select_success_actions {
        select entry_id,
               operation,
               username
        from   auth_batch_job_entries
        where  job_id = :job_id
        and    success_p = 't'
        order  by entry_id
    } {
        if { [info exists recstatus($operation)] } {
            append doc {  <person recstatus="} $recstatus($operation)  {">} \n
            append doc {    <sourcedid><source>OpenACS</source><id>} $username {</id></sourcedid>} \n
            append doc {  </person>} \n
        } else {
            ns_log Error "Illegal operation encountered in job action log: '$operation'. Entry_id is '$entry_id'."
        }
    }

    append doc {</enterprise>} \n

    return $doc
}

# Local variables:
#    mode: tcl
#    tcl-indent-level: 4
#    indent-tabs-mode: nil
# End: