xquery version "3.1";
(:
    ART-DECOR® STANDARD COPYRIGHT AND LICENSE NOTE
    Copyright © ART-DECOR Expert Group and ART-DECOR Open Tools GmbH
    see https://docs.art-decor.org/copyright and https://docs.art-decor.org/licenses

    This file is part of the ART-DECOR® tools suite.
:)
(:~ User API allows read, create, update of ART-DECOR server user :)
module namespace aduser                 = "http://art-decor.org/ns/art-decor-users";
(: 
Don't import those 2, they import api-user-settings, circularity leads to problems
import module namespace art             = "http://art-decor.org/ns/art" at "../modules/art-decor.xqm";
import module namespace decor           = "http://art-decor.org/ns/decor" at "api-decor.xqm";
:)
import module namespace get             = "http://art-decor.org/ns/art-decor-settings" at "../modules/art-decor-settings.xqm";
import module namespace adsearch        = "http://art-decor.org/ns/decor/search" at "api-decor-search.xqm";

declare namespace request     = "http://exist-db.org/xquery/request";
declare namespace xmldb       = "http://exist-db.org/xquery/xmldb";
declare namespace xs          = "http://www.w3.org/2001/XMLSchema";
declare namespace sub         = "http://art-decor.org/ns/art-decor-user-subscriptions";
declare namespace json        = "http://www.json.org";
declare namespace sm          = "http://exist-db.org/xquery/securitymanager";
declare option exist:serialize "method=xml media-type=text/xml";

(:~ Groups that are allowed to read and/or change properties of users that are not the currently logged in user :)
declare variable $aduser:editGroups             := ('decor-admin','dba','terminology-admin');
(:~ The path to the user info file. Copied here so we can remove it from art-decor-settings.xqm :)
declare variable $aduser:strUserInfo            := $get:strUserInfo;
(:~ The collection contents of the user info. Copied here so we can remove it from art-decor-settings.xqm :)
declare variable $aduser:colUserInfo             := collection($aduser:strUserInfo);
(:~
:   To subscribe to certain issues, the type needs to be from this list. Supported types may be found in DECOR.xsd under simpleType DecorObjectType. 
:   Special: #ALL (any issue), #NOOB (issues without objects), #ISAUTHOR (issues the user authored), #ISASSIGNED (issues the user is currently assigned to)
:)
declare variable $aduser:subALL                 := '#ALL';
declare variable $aduser:subNOOB                := '#NOOB';
declare variable $aduser:subISAUTHOR            := '#ISAUTHOR';
declare variable $aduser:subISASSIGNED          := '#ISASSIGNED';
declare variable $aduser:arrSubscriptionTypes   := ($aduser:subALL,$aduser:subNOOB,$aduser:subISAUTHOR,$aduser:subISASSIGNED,doc($get:strDecorTypes)//DecorObjectType/enumeration/@value/string());
(:~
:   The default type of subscription for issues.
:)
declare variable $aduser:arrSubscriptionDefault := ($aduser:subISAUTHOR,$aduser:subISASSIGNED);
(:~
:   Resource that holds all subscriptions
:)
declare variable $aduser:strSubscriptionFile    := 'user-subscriptions.xml';
(:~ uri that the user full name is under in eXist-db :)
declare variable $aduser:uriPropFullName        := xs:anyURI('http://axschema.org/namePerson');
(:~ uri that the user description is under in eXist-db :)
declare variable $aduser:uriPropDescription     := xs:anyURI('http://exist-db.org/security/description');

(: Avoid import from api-decor.xqm; circular imports lead to problems :)
declare variable $aduser:SECTIONS-ALL as xs:string+          := ('project',
                                                                'datasets',
                                                                'ids',
                                                                'issues',
                                                                'rules',
                                                                'scenarios',
                                                                'terminology');

(: Avoid import from art-decor.xqm; circular imports lead to problems :)
declare %private     function aduser:getDecorById($projectId as xs:string) as element(decor)* {
    $get:colDecorData/decor[project[@id = $projectId]]
};

(:~
:   Return full userInfo for the currently logged in user
:   
:   @return The configured user info e.g. 
:       <user name="john">
:           <defaultLanguage/>
:           <displayName/>
:           <description/>
:           <email/>
:           <organization/>
:           <logins/>
:           <lastissuenotify/>
:       </user>, or null
:   @since 2013-11-07
:)
declare function aduser:getUserInfo() as xs:string? {
    aduser:getUserInfo(get:strCurrentUserName())
};

(:~
:   Return full userInfo for the given username. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to get the info for
:   @return The configured user info e.g. 
:       <user name="john">
:           <defaultLanguage/>
:           <displayName/>
:           <description/>
:           <email/>
:           <organization/>
:           <logins/>
:           <lastissuenotify/>
:       </user>, null or error()
:   @since 2013-11-07
:)
declare function aduser:getUserInfo($username as xs:string) as element(user)? {
    if (get:strCurrentUserName() = $username) then (
        collection($aduser:strUserInfo)//user[@name = $username]
    )[1]
    else
    if (sm:get-user-groups(get:strCurrentUserName()) = $aduser:editGroups) then (
        collection($aduser:strUserInfo)//user[@name = $username]
    )[1]
    else (
        error(QName('http://art-decor.org/ns/error', 'InsufficientPermissions'), concat('User ',get:strCurrentUserName(),' cannot request info for user "',$username,'". User must be a member of any of these groups: ',string-join($aduser:editGroups,' '),')'))
    )
};

(:~
:   Return sequence of user names that have an entry in user-info.xml
:   
:   @return The list of user names, or null
:   @since 2013-11-07
:)
declare function aduser:getUserList() as xs:string* {
    aduser:searchUserByName(())/@name
};
declare function aduser:getUserList($searchString as xs:string?) as xs:string* {
    aduser:searchUserByName($searchString)/@name
};

(:~
:   Search user by (user) name
:
:   @param $searchString - required. The username to set the info for
:   @return list of elements containing username and displayName <user username="...">DisplayName</user>
:   @since 2016-11-20
:)
declare function aduser:searchUserByName($searchString as xs:string?) as element()* {
    if (empty($searchString)) then (
        $aduser:colUserInfo//user
    )
    else (
        let $luceneQuery    := adsearch:getSimpleLuceneQuery($searchString)
        let $luceneOptions  := adsearch:getSimpleLuceneOptions()
        return
            $aduser:colUserInfo//user[ft:query(@name | displayName, $luceneQuery)]
    )
};

(:~
:   Return active status for the current user (may be guest).
:   
:   @return The boolean. true = active, or false = inactive
:   @since 2018-09-03
:   @see aduser:isUserActive($username as xs:string)
:)
declare function aduser:isUserActive() as xs:boolean {
    aduser:isUserActive(get:strCurrentUserName())
};
(:~
:   Return active status for the current user (may be guest).
:   
:   @param $username - required. The username to set the info for
:   @return The boolean. true = active, or false = inactive
:   @since 2018-09-03
:)
declare function aduser:isUserActive($username as xs:string) as xs:boolean {
    let $active     :=
        try { 
            if (sm:user-exists($username)) then sm:is-account-enabled($username) else false()
        }
        catch * {()}
        
    return
        if (empty($active)) then 
            try { 
                let $userinfo   := aduser:getUserInfo($username)
                return
                    if ($userinfo) then not($userinfo/@active = 'false') else false()
            }
            catch * {true()} 
        else ($active)
};

(:~
:   Return language for the current user (may be guest) or the server language as fallback.
:   
:   @return The configured language e.g. 'en-US'
:   @since 2013-11-07
:   @see aduser:getUserLanguage($username as xs:string, $defaultwhenempty as xs:boolean)
:)
declare function aduser:getUserLanguage() as xs:string {
    aduser:getUserLanguage(get:strCurrentUserName(), true())
};

(:~
:   Return language for the given username or the server language as fallback.
:   
:   @param $username The username to get the info for
:   @return The configured user language e.g. 'en-US', the server language (if no user setting exists) or error()
:   @since 2013-11-07
:   @see aduser:getUserLanguage($username as xs:string, $defaultwhenempty as xs:boolean)
:)
declare function aduser:getUserLanguage($username as xs:string) as xs:string {
    aduser:getUserLanguage($username, true())
};

(:~
:   Return language for the given username. If the user does not have a preference and parameter $defaultwhenempty is true, 
:   the browser language setting is returned, if that fails to get a language, the server language is returned as final 
:   fallback. If the user does not have a preference and parameter $defaultwhenempty is false, the result is empty()
:   If this username is not equal to the currently logged in user, the currently logged in user needs to be part of a group 
:   with permissions. If he is not, an error is returned.
:   
:   @param $username The username to get the info for
:   @return The configured user language e.g. 'en-US', the server language (if no user setting exists) or error()
:   @since 2013-11-07
:)
declare function aduser:getUserLanguage($username as xs:string, $defaultwhenempty as xs:boolean) as xs:string? {
    let $language := try { aduser:getUserInfo($username)/defaultLanguage[string-length()>0] } catch * {()}
    return
    if ($language[string-length() gt 0] or not($defaultwhenempty)) then 
        $language
    else (
        let $lang := aduser:getSupportedBrowserLanguage()
        return
        if ($lang) then
            $lang
        else (
            $get:strArtLanguage
        )
    )
};

(:~
:   Return language based on browser language. The returned value comes from the list of languages found in 
:   form-resources.xml to make sure it is supported in the interface
:   See also: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
:   Accept-Language: nl,en-us;q=0.7,en;q=0.3
:   Accept-Language: nl-nl
:   Note: Safari sends only 1 language, FireFox a list, mileage may vary per browser (version)
:   
:   @return The first accepted browser language that is also supported by ART e.g. 'en-US', or ()
:   @since 2013-11-07
:)
declare %private function aduser:getSupportedBrowserLanguage() as xs:string? {
    let $supported-languages := doc(concat($get:strArtResources,'/form-resources.xml'))//@xml:lang
    let $accept-language     := if (request:exists()) then request:get-header('accept-language') else ()
    
    let $accepted-and-supported-languages :=
        for $lang-range in tokenize($accept-language,',')
        let $lang               := tokenize($lang-range,';')[1]
        let $supported-language := $supported-languages[lower-case(.)=lower-case($lang)]
        return
            if (empty($supported-language)) then
                (: e.g. $lang=nl :)
                if (string-length($lang)=2) then
                    $supported-languages[starts-with(lower-case(.),lower-case($lang))]
                else ()
            else (
                $supported-language
            )
    return
        $accepted-and-supported-languages[1]
};

(:~
:   Return groups for the currently logged in user
:   
:   @return The configured groups e.g. 'decor decor-admin', or null
:   @since 2015-10-14
:)
declare function aduser:getUserGroups() as xs:string? {
    aduser:getUserGroups(get:strCurrentUserName())
};

(:~
:   Return groups for the given username. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to get the info for
:   @return The configured groups e.g. 'decor decor-admin', or null
:   @since 2015-10-14
:)
declare function aduser:getUserGroups($username as xs:string) as xs:string? {
    aduser:getUserInfo($username)/groups
};

(:~
:   Return organization for the currently logged in user
:   
:   @return The configured organization e.g. 'St. Joseph Hospital', or null
:   @since 2013-11-07
:)
declare function aduser:getUserOrganization() as xs:string? {
    aduser:getUserOrganization(get:strCurrentUserName())
};

(:~
:   Return organization for the given username. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to get the info for
:   @return The configured organization e.g. 'St. Joseph Hospital', null or error()
:   @since 2013-11-07
:)
declare function aduser:getUserOrganization($username as xs:string) as xs:string? {
    aduser:getUserInfo($username)/organization
};

(:~
:   Return display name for the currently logged in user, usually his full name
:   
:   @return The configured display name e.g. 'John Doe', or null
:   @since 2013-11-07
:)
declare function aduser:getUserDisplayName() as xs:string? {
    aduser:getUserDisplayName(get:strCurrentUserName())
};

(:~
:   Return display name for the given username, usually his full name. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to get the info for
:   @return The configured display name e.g. 'John Doe', null or error()
:   @since 2013-11-07
:)
declare function aduser:getUserDisplayName($username as xs:string) as xs:string? {
    try {
        let $displayName    := sm:get-account-metadata($username, $aduser:uriPropFullName)
        
        return
            if (empty($displayName)) then error() else ($displayName)
    }    
    catch * {
        (aduser:getUserInfo($username)/displayName[string-length() gt 0], $username)[1]
    }
};

(:~
:   Return email for the currently logged in user
:   
:   @return The configured email e.g. 'mailto:johndoe@stjosephhosptial.org', or null
:   @since 2013-11-07
:)
declare function aduser:getUserEmail() as xs:string? {
    aduser:getUserEmail(get:strCurrentUserName())
};

(:~
:   Return email for the given username. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to get the info for
:   @return The configured email e.g. 'mailto:johndoe@stjosephhosptial.org', null or error()
:   @since 2013-11-07
:)
declare function aduser:getUserEmail($username as xs:string) as xs:string? {
    aduser:getUserInfo($username)/email
};

(: Purposely disabled: we probably do not want description to be a user-setting thing but rather part of the exist-db user-accounts 
        sm:set-account-metadata($username,'http://exist-db.org/security/description',$description)
:)
(:~
:   Return description for the currently logged in user
:   
:   @return The configured description e.g. 'Added at request of XXX' or null
:   @since 2013-11-07
:)
(:declare function aduser:getUserDescription() as xs:string? {
    aduser:getUserDescription(get:strCurrentUserName())
};
:)

(:~
:   Return description for the given username. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to get the info for
:   @return The configured description e.g. 'Added at request of XXX', null or error()
:   @since 2013-11-07
:)
(:declare function aduser:getUserDescription($username as xs:string) as xs:string? {
    aduser:getUserInfo($username)/description
};
:)

(:~
:   Return the date the account was added
:   
:   @return The configured user info e.g. 2013-01-01T12:34:23
:   @since 2013-11-07
:)
declare function aduser:getUserCreationDate() as xs:dateTime? {
    aduser:getUserCreationDate(get:strCurrentUserName())
};

(:~
:   Return the date the account was added. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to get the info for
:   @return The configured user info e.g. 2013-01-01T12:34:23, null or error()
:   @since 2013-11-07
:)
declare function aduser:getUserCreationDate($username as xs:string) as xs:dateTime? {
    let $strCreationDate := aduser:getUserInfo($username)/@effectiveDate
    return
    if (exists($strCreationDate)) then
        xs:dateTime($strCreationDate)
    else ()
};

(:~
:   Return last time the currently logged in user was logged in
:   
:   @return The configured last login time e.g. '2013-11-11T13:24:00' or null
:   @since 2013-11-07
:)
declare function aduser:getUserLastLoginTime() as xs:dateTime? {
    aduser:getUserLastLoginTime(get:strCurrentUserName())
};

(:~
:   Return last time the currently logged in user was logged in. If this username is not equal to the 
:   currently logged in user, the currently logged in user needs to be part of a group with permissions. If he 
:   is not, an error is returned
:   
:   @param $username The username to get the info for
:   @return The configured dateTime as xs:dateTime e.g. '2013-11-11T13:24:00', null or error()
:   @since 2013-11-07
:)
declare function aduser:getUserLastLoginTime($username as xs:string) as xs:dateTime? {
    max(aduser:getUserInfo($username)/logins/login/xs:dateTime(@at))
};

(:~
:   Return last time the currently logged in user was notified for issues
:   
:   @return The configured last notify time e.g. '2013-11-11T13:24:00' or null
:   @since 2013-11-07
:)
declare function aduser:getUserLastIssueNotify() as xs:dateTime? {
    aduser:getUserLastIssueNotify(get:strCurrentUserName())
};

(:~
:   Return last time the currently logged in user was notified for issues. If this username is not equal to the 
:   currently logged in user, the currently logged in user needs to be part of a group with permissions. If he 
:   is not, an error is returned
:   
:   @param $username The username to get the info for
:   @return The configured dateTime as xs:dateTime e.g. '2013-11-11T13:24:00', null or error()
:   @since 2013-11-07
:)
declare function aduser:getUserLastIssueNotify($username as xs:string) as xs:dateTime? {
    if (aduser:getUserInfo($username)/lastissuenotify/@at castable as xs:dateTime) then (
        xs:dateTime(aduser:getUserInfo($username)/lastissuenotify/@at)
    ) else ()
};

(:~
:   Return time the currently logged in user was notified for his new account. If this username is not equal to the 
:   currently logged in user, the currently logged in user needs to be part of a group with permissions. If he 
:   is not, an error is returned.
    If there is an empty element lastaccountnotify 1981-01-01T00:00:00 is returned. 
    If there is no element lastaccountnotify null is returned. This allows
:   to not inform users that already have an account (since long time ago)
:   
:   @param $username The username to get the info for
:   @return The configured dateTime as xs:dateTime e.g. '2013-11-11T13:24:00', '1981-01-01T00:00:00', or null or error()
:   @since 2021-07-11
:)
declare function aduser:getUserLastAccountNotify($username as xs:string) as xs:dateTime? {
    let $lan := aduser:getUserInfo($username)/lastaccountnotify
    return
        if (count($lan) = 1) then 
            if ($lan/@at castable as xs:dateTime) 
            then xs:dateTime($lan/@at)
            else xs:dateTime('1981-01-01T00:00:00')
        else ()
};

(:~
:   See aduser:getProjectPreference($username, $projectId)
:
:   @since 2016-11-20
:)
declare function aduser:getProjectPreference($projectId as xs:string?) as element(project)* {
    aduser:getProjectPreference(get:strCurrentUserName(), $projectId)
};

(:~
:   Returns entry for a project for the given username. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $projectId The project/@id to retieve info for. If empty returns all
:   @return project element that was created or updated or error()
:   @since 2016-11-20
:)
declare function aduser:getProjectPreference($username as xs:string, $projectId as xs:string?) as element(project)* {
    if (empty($projectId)) then (
        aduser:getUserInfo($username)/preferences/project
    ) else (
        aduser:getUserInfo($username)/preferences/project[@id = $projectId]
    )
};

(:~
:   Return the DECOR project subscription settings for the requested project. 
:   
:   @param $prefix - required. The project prefix to get the info for
:   @return one or more issue types for the project, or $aduser:arrSubscriptionDefault
:   @since 2014-06-23
:)
declare function aduser:getUserDecorSubscriptionSettings($prefix as xs:string) as xs:string+ {
    aduser:getUserDecorSubscriptionSettings(get:strCurrentUserName(),$prefix)
};

(:~
:   Return the DECOR project settings for a specific user and project.
:   
:   @param $username - required. The username to get the info for
:   @param $prefix - required. project prefix to get the info for
:   @return one or more issue types for the project, or $aduser:arrSubscriptionDefault
:   @since 2014-06-23
:)
declare function aduser:getUserDecorSubscriptionSettings($username as xs:string, $prefix as xs:string) as xs:string+ {
    (:  NOTE: this purposely bypasses the security check implemented in aduser:getUserInfo() because in 
        saving issues the subscriptions need to be updated for all users based on this info, regardless of 
        who was logged in at the time :)
    let $return := 
        if ($get:colDecorData/decor/project[@prefix=$prefix]/author[@username=$username][@notifier='on']) then
            $aduser:subALL
        else if ($username='guest') then () 
        else (
            $aduser:colUserInfo//project[@prefix=$prefix][ancestor::user/@name=$username][1]/@subscribeIssues
        )
        
    return
        if ($return) then (tokenize($return,' ')) else ($aduser:arrSubscriptionDefault)
};

(:~
:   Return boolean value to indicate if the currently logged in user has a subscription for requested issueId. 
:   
:   @param $issueId - required. The issue id to get the info for
:   @return if subscription exists 'true()', else 'false()'
:   @since 2014-06-23
:)
declare function aduser:userHasIssueSubscription($issueId as xs:string) as xs:boolean {
    aduser:userHasIssueSubscription(get:strCurrentUserName(),$issueId)
};

(:~
:   Return boolean value to indicate if this username has a subscription for requested issueId. 
:   
:   @param $username - required. The username to get the info for
:   @param $issueId - optional. The issue id to get the info for
:   @return if subscription exists 'true()', else 'false()'
:   @since 2014-06-23
:)
declare function aduser:userHasIssueSubscription($username as xs:string, $issueId as xs:string) as xs:boolean {
    let $step1 := collection($get:strArtData)//sub:issue[@id = $issueId]
    let $step2 := $step1[@user = $username]
    return $step2/@notify = 'true'
};

(:~
:   Return potential empty string value to indicate if this username has a subscription for requested issueId. This
:   allows you to distinguish between 'has subscription', 'explicitly does not have a subscription', 'subscription not set'
:   
:   @param $username - required. The username to get the info for
:   @param $issueId - optional. The issue id to get the info for
:   @return if subscription exists 'true()', if explicitly no subscription 'false()', else empty
:   @since 2014-06-23
:)
declare %private function aduser:userCheckIssueSubscription($username as xs:string, $issueId as xs:string?) as xs:boolean? {
    let $step1 := collection($get:strArtData)//sub:issue[@id = $issueId]
    let $step2 := $step1[@user = $username]/@notify
    return
        if ($step2 castable as xs:boolean) then xs:boolean($step2) else ()
};

(:~
:   Return boolean value to indicate if an issue with certain characteristics is within the settings for automatic subscription
:   for the current logged in user.
:   See aduser:userHasIssueAutoSubscription($username, $prefix, $issueId, $objectTypes, $originalAuthorUserName, $currentAssignedAuthorName)
:   for more info
:   
:   @param $prefix - required. The project prefix to get the info for
:   @param $issueId - optional. The issue id to get the info for
:   @param $objectTypes - optional. The issue object types to get the info for (issue/object/@type)
:   @param $originalAuthorUserName - optional. The username of the original issue author (project/author[@id=issue/tracking[first]/author/@id]/@username)
:   @param $currentAssignedAuthorName - optional. The username of the currently assigned person (project/author[@id=issue/assignment[last]/author/@id]/@username)
:   @return if subscription exists 'true()', if explicitly no subscription 'false()', else empty
:   @since 2014-06-23
:)
declare function aduser:userHasIssueAutoSubscription($prefix as xs:string, $issueId as xs:string?, $objectTypes as xs:string*, $originalAuthorUserName as xs:string?, $currentAssignedAuthorName as xs:string?) as xs:boolean {
    aduser:userHasIssueAutoSubscription(get:strCurrentUserName(), $prefix, $issueId, $objectTypes, $originalAuthorUserName, $currentAssignedAuthorName, ())
};

(:~
:   Return boolean value to indicate if an issue with certain characteristics is within the settings for automatic subscription
:   for the current logged in user. Logic:
:   - If the user has explicitly set or unset a subscription, return that value as the auto setting is then irrelevant
:   - If the user has an auto subscription to #ALL issues, return true()
:   - If the user is the current issue author and has an auto subscription to #ISAUTHOR issues, return true()
:   - If the user is the currently assigned to the issue and has an auto subscription to #ISASSIGNED issues, return true()
:   - If the issue has no objects and the user has an auto subscription to #NOOB issues, return true()
:   - If the issue has at least 1 object that the user has an auto subscription for , return true()
:   - Else false()
:   
:   @param $username - required. The username to get the info for
:   @param $prefix - required. The project prefix to get the info for
:   @param $issueId - optional. The issue id to get the info for
:   @param $objectTypes - optional. The issue object types to get the info for (issue/object/@type)
:   @param $originalAuthorUserName - optional. The username of the original issue author (project/author[@id=issue/tracking[first]/author/@id]/@username)
:   @param $currentAssignedAuthorName - optional. The username of the currently assigned person (project/author[@id=issue/assignment[last]/author/@id]/@username)
:   @return if subscription exists 'true()', if explicitly no subscription 'false()', else empty
:   @since 2014-06-23
:)
declare function aduser:userHasIssueAutoSubscription($username as xs:string, $prefix as xs:string, $issueId as xs:string?, $objectTypes as xs:string*, $originalAuthorUserName as xs:string?, $currentAssignedAuthorName as xs:string?) as xs:boolean {
    aduser:userHasIssueAutoSubscription(get:strCurrentUserName(), $prefix, $issueId, $objectTypes, $originalAuthorUserName, $currentAssignedAuthorName, ())
};
declare function aduser:userHasIssueAutoSubscription($username as xs:string, $prefix as xs:string, $issueId as xs:string?, $objectTypes as xs:string*, $originalAuthorUserName as xs:string?, $currentAssignedAuthorName as xs:string?, $userSubscriptionSettings as xs:string*) as xs:boolean {
    let $userSubscriptionSettings   := if (empty($userSubscriptionSettings)) then aduser:getUserDecorSubscriptionSettings($username,$prefix) else $userSubscriptionSettings
    let $userCurrentSubscription    := aduser:userCheckIssueSubscription($username,$issueId)
    return
        if (empty($userCurrentSubscription)=false()) then
            (:subscription explicitly set or unset for this particular issue:)
            $userCurrentSubscription
        else if ($userSubscriptionSettings=$aduser:subALL) then
            (:user subscribes to any issue type:)
            true()
        else if ($username=$originalAuthorUserName and $userSubscriptionSettings=$aduser:subISAUTHOR) then
            (:user subscribes to any issue where he is author:)
            true()
        else if ($username=$currentAssignedAuthorName and $userSubscriptionSettings=$aduser:subISASSIGNED) then
            (:user subscribes to any issue where he is the assigned person:)
            true()
        else if (empty($objectTypes) and $userSubscriptionSettings=$aduser:subNOOB) then
            (:user subscribes to any issue where there's no object:)
            true()
        else if (empty($objectTypes)=false() and $objectTypes=$userSubscriptionSettings) then
            (:user subscribes to any issue where there's at least one object of a certain type:)
            true()
        else
            false()
};

(: ----------------  Write functions ---------------- :)

(:~
:   See aduser:createUserInfo($username)
:
:   @since 2013-11-07
:)
declare function aduser:createUserInfo() as element() {
    aduser:createUserInfo(get:strCurrentUserName())
};

(:~
:   Create basic userInfo if there is no user info yet for the given username. If this username is not equal to the currently 
:   logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   If the given username does not have a matching account in eXist, an error is returned.
:   
:   @param $username The username to set the info for
:   @return The existing user info, or the basic info we just added e.g. 
:       <user name="john">
:           <defaultLanguage/>
:           <displayName/>
:           <description/>
:           <email/>
:           <organization/>
:           <logins/>
:           <lastissuenotify/>
:       </user>, null or error()
:   @since 2013-11-07
:)
declare function aduser:createUserInfo($username as xs:string) as element(user) {
    let $currentUser    := get:strCurrentUserName()
    let $username       := if ($username = '') then 'guest' else $username
    
    let $check          := 
        if ($currentUser = ('','guest')) then
            if ($username = 'guest') then () else (
                error(QName('http://art-decor.org/ns/error', 'InsufficientPermissions'), concat('Guest user cannot add user settings for a different user ',get:strCurrentUserName(),'. Cannot add user settings.'))
            )
        else ()
    let $check          :=
        if ($username = 'guest') then () else if (sm:user-exists($username)) then () else (
            error(QName('http://art-decor.org/ns/error', 'UserDoesNotExist'), concat('User ',$username,' does not have an ART account yet. Cannot add user settings.'))
        )
    
    let $userInfo       := aduser:getUserInfo($username)
    
    let $userInfo       := 
        if ($userInfo) then ($userInfo) else (
            let $newUserInfo    :=
                <user name="{$username}" active="{aduser:isUserActive($username)}" effectiveDate="{format-dateTime(current-dateTime(), '[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]')}">
                    <defaultLanguage>{$get:strArtLanguage}</defaultLanguage>
                    <displayName>{$username}</displayName>
                    <email/>
                    <groups/>
                    <organization/>
                    <logins/>
                    <lastissuenotify/>
                    <decor-settings/>
                </user>
            let $userFile       := xmldb:store($aduser:strUserInfo, $username || '.xml', $newUserInfo)
            let $fixPermissions := 
                if ($username = 'guest.xml') then 
                    sm:chmod(xs:anyURI($userFile), 'rw-rw-rw-') 
                else (
                    sm:chmod(xs:anyURI($userFile), 'rw-rw----'),
                    sm:chown(xs:anyURI($userFile), $username || ':dba')
                )

            return doc($userFile)/user
        )
        
    return
        if ($userInfo) then $userInfo else (
            error(QName('http://art-decor.org/ns/error', 'UserDoesNotExist'), concat('User ',$username,' does not have a user-info.xml account. Cannot get user settings.'))
        )
};

(:~
:   See aduser:createUserInfo($username, $language, $displayName, $email, $organization)
:
:   @since 2013-11-07
:)
declare function aduser:setUserInfo($language as xs:string?, $displayName as xs:string?, $email as xs:string?, $organization as xs:string?) as element() {
    aduser:setUserInfo(get:strCurrentUserName(), $language, $displayName, $email, $organization)
};

(:~
:   Set basic userInfo overwriting any existing info for the given properties. If this username is not equal to the currently 
:   logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $language The language to set, format 'll-CC' (language-country)
:   @param $displayName The display name to set
:   @param $email The email address to set format user@host.realm
:   @param $organization The organization name to set
:   @return The existing user info after applying the updates e.g. 
:       <user name="john" effectiveDate="2013-01-01T00:00:00">
:           <defaultLanguage>en-US</defaultLanguage>
:           <groups>decor decor-admin</groups>
:           <displayName>John Doe</displayName>
:           <description/>
:           <email/>
:           <organization>St. Johns Hospital</organization>
:       </user>, null or error()
:   @since 2013-11-07
:)
declare function aduser:setUserInfo($username as xs:string, $language as xs:string?, $displayName as xs:string?, $email as xs:string?, $organization as xs:string?) as element() {
    let $update   := aduser:setUserLanguage($username, $language)
    let $update   := aduser:setUserDisplayName($username, $displayName)
    let $update   := aduser:setUserEmail($username, $email)
    let $update   := aduser:setUserOrganization($username, $organization)
    let $update   := aduser:setUserGroups($username)
    let $update   := aduser:setUserActive($username)
    
    return
        aduser:getUserInfo($username)
};

(:~
:   Purposefully a local function and currently not used. Left in for convenience. @effectiveDate is a new 
:   attribute that needs to be populated to be useful. Must be read-only, so only if the attribute is not set 
:   yet, it is written
:
:   @param $username The username to set the info for
:   @param $language The date/time stamp to set if no value exists yet
:   @since 2013-11-07
:)
declare %private function aduser:setUserCreationDate($username as xs:string, $datetime as xs:dateTime) {
    let $currentUserInfo := aduser:createUserInfo($username)
    return
        if (exists($currentUserInfo/@effectiveDate)) then (
            (:update value $currentUserInfo/@effectiveDate with $datetime:)
        )
        else (
            update insert attribute effectiveDate {$datetime} into $currentUserInfo
        )
};

(:~
:   Sets the active status for the given username based on system value. If this username is not equal to the currently 
:   logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @return nothing or error()
:   @since 2018-09-03
:)
declare function aduser:setUserActive($username as xs:string) {
    let $currentUserInfo := aduser:createUserInfo($username)
    return
        if (exists($currentUserInfo/@active)) then
            update value $currentUserInfo/@active with aduser:isUserActive($username)
        else (
            update insert attribute active {aduser:isUserActive($username)} into $currentUserInfo
        )
};

(:~
:   @param $language The language to set, format 'll-CC' (language-country) or empty
:   @see aduser:setUserLanguage($username, $language)
:   @since 2013-11-07
:   @since 2014-04-07 Made $language optional so you can 'unset' the language. Applicable to guest user mostly, an empty language will trigger browser language based behavior
:)
declare function aduser:setUserLanguage($language as xs:string?) {
    aduser:setUserLanguage(get:strCurrentUserName(),$language)
};

(:~
:   Sets the default language for the given username. If this username is not equal to the currently 
:   logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $language The language to set, format 'll-CC' (language-country) or empty
:   @return nothing or error()
:   @error Parameter language SHALL be empty or have the case sensitive format ll-CC, e.g. en-US or de-DE
:   @since 2013-11-07
:   @since 2014-04-07 Made $language optional so you can 'unset' the language. Applicable to guest user mostly, an empty language will trigger browser language based behavior
:)
declare function aduser:setUserLanguage($username as xs:string, $language as xs:string?) {
    let $language        := 
        if (empty($language)) 
        then ''
        else if (matches($language,'^[a-z]{2}-[A-Z]{2}$')) 
        then $language 
        else (
            error(QName('http://art-decor.org/ns/error', 'InvalidParameterFormat'), 'Parameter language SHALL be empty or have the case sensitive format ll-CC, e.g. en-US or de-DE')
        )
    let $currentUserInfo := aduser:createUserInfo($username)
    return
        if (exists($currentUserInfo/defaultLanguage)) then
            update value $currentUserInfo/defaultLanguage with $language
        else (
            update insert <defaultLanguage>{$language}</defaultLanguage> into $currentUserInfo
        )
};

(:~
:   See aduser:setUserGroups($username, $groups)
:
:   @return nothing or error()
:   @since 2015-10-14
:)
declare function aduser:setUserGroups() {
    aduser:setUserOrganization(get:strCurrentUserName())
};

(:~
:   Sets the groups string for the given username. If this username is not equal to the currently 
:   logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @return nothing or error()
:   @since 2015-10-14
:)
declare function aduser:setUserGroups($username as xs:string) {
    let $groups          := <groups>{for $group in sm:get-user-groups($username) return <group>{$group}</group>}</groups>
    let $currentUserInfo := aduser:createUserInfo($username)
    return
        if (exists($currentUserInfo/groups)) then
            update replace $currentUserInfo/groups with $groups
        else (
            update insert $groups into $currentUserInfo
        )
};

(:~
:   See aduser:setUserOrganization($username, $organization)
:
:   @param $organization The organization name to set
:   @return nothing or error()
:   @since 2013-11-07
:)
declare function aduser:setUserOrganization($organization as xs:string?) {
    aduser:setUserOrganization(get:strCurrentUserName(),$organization)
};

(:~
:   Sets the organization name for the given username. If this username is not equal to the currently 
:   logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $organization The organization name to set
:   @return nothing or error()
:   @since 2013-11-07
:)
declare function aduser:setUserOrganization($username as xs:string, $organization as xs:string?) {
    let $organization    := if (exists($organization)) then $organization else ('')
    let $currentUserInfo := aduser:createUserInfo($username)
    return
        if (exists($currentUserInfo/organization)) then
            update value $currentUserInfo/organization with $organization
        else (
            update insert <organization>{$organization}</organization> into $currentUserInfo
        )
};

(:~
:   See aduser:setUserDisplayName($username, $displayName)
:
:   @since 2013-11-07
:)
declare function aduser:setUserDisplayName($displayName as xs:string?) {
    aduser:setUserDisplayName(get:strCurrentUserName(),$displayName)
};

(:~
:   Sets the display name for the given username. If this username is not equal to the currently 
:   logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $displayName The display name to set
:   @return nothing or error()
:   @since 2013-11-07
:)
declare function aduser:setUserDisplayName($username as xs:string, $displayName as xs:string?) {
    let $displayName        := if (empty($displayName)) then $username else $displayName
    let $currentUserInfo    := aduser:createUserInfo($username)
    let $meta               := try { sm:set-account-metadata($username, $aduser:uriPropFullName, $displayName) } catch * {()}
    return
        if (exists($currentUserInfo/displayName)) then
            update value $currentUserInfo/displayName with $displayName
        else (
            update insert <displayName>{$displayName}</displayName> into $currentUserInfo
        )
};

(:~
:   See aduser:setUserEmail($username, $email)
:
:   @since 2013-11-07
:)
declare function aduser:setUserEmail($email as xs:string?) {
    aduser:setUserEmail(get:strCurrentUserName(),$email)
};

(:~
:   Sets the email address name for the given username. If this username is not equal to the currently 
:   logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $email The email address to set format user@host.realm
:   @return nothing or error()
:   @since 2013-11-07
:)
declare function aduser:setUserEmail($username as xs:string, $email as xs:string?) {
    let $email           := if (exists($email)) then $email else ('')
    let $currentUserInfo := aduser:createUserInfo($username)
    return
        if (exists($currentUserInfo/email)) then
            update value $currentUserInfo/email with $email
        else (
            update insert <email>{$email}</email> into $currentUserInfo
        )
};

(: Purposely disabled: we probably do not want description to be a user-setting thing but rather part of the exist-db user-accounts 
        sm:set-account-metadata($username,'http://exist-db.org/security/description',$description)
:)
(:~
:   See aduser:setUserDescription($username, $description)
:
:   @since 2013-11-07
:)
(:declare function aduser:setUserDescription($description as xs:string?) {
    aduser:setUserDescription(get:strCurrentUserName(),$description)
};
:)

(:~
:   Sets the description for the given username. If this username is not equal to the currently 
:   logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $description The description to set
:   @return nothing or error()
:   @since 2013-11-07
:)
(:declare function aduser:setUserDescription($username as xs:string, $description as xs:string?) {
    let $description     := if (exists($description)) then $description else ('')
    let $currentUserInfo := aduser:createUserInfo($username)
    return
        if (exists($currentUserInfo/description)) then
            update value $currentUserInfo/description with $description
        else (
            update insert <description>{$description}</description> into $currentUserInfo
        )
};
:)

(:~
:   See aduser:setUserLastLoginTime($username, $datetime)
:
:   @since 2013-11-07
:)
declare function aduser:setUserLastLoginTime($datetime as xs:dateTime?) {
    aduser:setUserLastLoginTime(get:strCurrentUserName(),$datetime)
};

(:~
:   Sets the last login time for the given username, keeping no more than the 5 latest login elements. If this username is not equal 
:   to the currently logged in user, the currently logged in user needs to be part of a group with permissions. If he is not, an 
:   error is returned
:   
:   @param $username The username to set the info for
:   @param $datetime The dateTime to set, format yyyy-MM-ddTHH:mm:ss(.sss+/-ZZ:zz)
:   @return nothing or error()
:   @since 2013-11-07
:)
declare function aduser:setUserLastLoginTime($username as xs:string, $datetime as xs:dateTime?) {
    let $newlastlogin    := <login at="{substring($datetime,1,19)}"/>
    let $currentUserInfo := aduser:createUserInfo($username)
    return
        if (exists($currentUserInfo/logins)) then
            let $logins := 
                for $login in $currentUserInfo/logins/login[@at]
                order by xs:dateTime($login/@at) descending
                return $login
            let $logins :=
                for $login in ($newlastlogin|subsequence($logins,1,4))
                order by xs:dateTime($login/@at) ascending
                return $login
            return
                (: add last login record + last 4 records to avoid too long lists:)
                update replace $currentUserInfo/logins with <logins>{$logins}</logins>
        else (
            (: non-existent logins element, add into user element :)
            update insert <logins>{$newlastlogin}</logins> into $currentUserInfo
        )
};

(:~
:   See aduser:setUserLastIssueNotify($username, $datetime)
:
:   @since 2013-11-07
:)
declare function aduser:setUserLastIssueNotify($datetime as xs:dateTime?) {
    aduser:setUserLastIssueNotify(get:strCurrentUserName(),$datetime)
};

(:~
:   Sets the last issue notify time for the given username. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $datetime The dateTime to set, format yyyy-MM-ddTHH:mm:ss(.sss+/-ZZ:zz)
:   @return nothing or error()
:   @since 2013-11-07
:)
declare function aduser:setUserLastIssueNotify($username as xs:string, $datetime as xs:dateTime?) {
    let $newlastissuenotify := <lastissuenotify at="{substring($datetime,1,19)}"/>
    let $currentUserInfo    := aduser:createUserInfo($username)
    return
        if (exists($currentUserInfo/lastissuenotify)) then
            update replace $currentUserInfo/lastissuenotify with $newlastissuenotify
        else (
            update insert $newlastissuenotify into $currentUserInfo
        )
};

(:~
:   See aduser:updateProjectPreference($username, $projectId)
:
:   @since 2016-11-20
:)
declare function aduser:updateProjectPreference($projectId as xs:string?) as element(project)? {
    aduser:updateProjectPreference(get:strCurrentUserName(), $projectId)
};

(:~
:   Adds entry for a project for the given username. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $projectId The project/@id to create the entry for
:   @return project element that was created or updated or error()
:   @since 2016-11-20
:)
declare function aduser:updateProjectPreference($username as xs:string, $projectId as xs:string?) as element(project)? {
    if (empty($projectId)) then () else (
        let $now                    := format-dateTime(current-dateTime(), '[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]')
        let $decor                  := aduser:getDecorById($projectId)
        let $projectPrefix          := $decor/project/@prefix
        let $newprojectentry        := <project id="{$projectId}" prefix="{$projectPrefix}" count="1" at="{$now}"/>
        
        let $currentUserInfo        := aduser:createUserInfo($username)
        let $existingprojectentry   := $currentUserInfo/preferences/project[@id = $projectId]
        
        let $update                 := 
            if ($currentUserInfo[preferences]) then
                if ($existingprojectentry) then (
                    let $u  := update replace $existingprojectentry/@prefix with $projectPrefix
                    let $u  := update replace $existingprojectentry/@count with ($existingprojectentry/xs:integer(@count) + 1)
                    let $u  := update replace $existingprojectentry/@at with $now
                    
                    return ()
                )
                else (
                    update insert $newprojectentry into $currentUserInfo/preferences
                )
            else (
                update insert <preferences>{$newprojectentry}</preferences> into $currentUserInfo
            )
        
        return aduser:getProjectPreference($username, $projectId)
    )
};

(:~
:   See aduser:updateProjectPreferenceStatuses($username, $projectId, $section, $hide-statuses)
:
:   @since 2018-01-11
:)
declare function aduser:updateProjectPreferenceStatuses($projectId as xs:string?, $section as xs:string, $hide-statuses as xs:string*) as element(project)? {
    aduser:updateProjectPreferenceStatuses(get:strCurrentUserName(), $projectId, $section, $hide-statuses)
};

(:~
:   Adds a hide-statuses entry for a section in a project for the given username. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned
:   
:   @param $username The username to set the info for
:   @param $projectId The project/@id to create the entry for
:   @param $section The section to create the entry for. See SECTIONS-ALL for supported values, e.g. datasets, rules, terminology
:   @return project element that was created or updated or error()
:   @since 2018-01-11
:)
declare function aduser:updateProjectPreferenceStatuses($username as xs:string, $projectId as xs:string?, $section as xs:string, $hide-statuses as xs:string*) as element(project)? {
    if (empty($projectId)) then () else (
        let $now                    := format-dateTime(current-dateTime(), '[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]')
        let $decor                  := aduser:getDecorById($projectId)
        let $projectPrefix          := $decor/project/@prefix
        let $newprojectentry        := <project id="{$projectId}" prefix="{$projectPrefix}" count="1" at="{$now}"/>
        
        let $currentUserInfo        := aduser:createUserInfo($username)
        let $existingprojectentry   := $currentUserInfo/preferences/project[@id = $projectId]
        
        let $update                 := 
            if ($currentUserInfo[preferences]) then
                if ($existingprojectentry) then () else (
                    update insert $newprojectentry into $currentUserInfo/preferences
                )
            else (
                update insert <preferences>{$newprojectentry}</preferences> into $currentUserInfo
            )
        
        let $existingprojectentry   := $currentUserInfo/preferences/project[@id = $projectId]
        let $newsectionentry        := 
            if (empty($hide-statuses)) then ()
            else
            if ($section = $aduser:SECTIONS-ALL) then
                <section name="{$section}" hide-statuses="{$hide-statuses}"/>
            else ()
        let $update                 :=
            if ($newsectionentry) then (
                if ($existingprojectentry/section[@name = $section][@hide-statuses]) then
                    update value $existingprojectentry/section[@name = $section]/@hide-statuses with $hide-statuses
                else
                if ($existingprojectentry/section[@name = $section]) then
                    update insert $newsectionentry/@hide-statuses into $existingprojectentry/section[@name = $section]
                else
                    update insert $newsectionentry into $existingprojectentry
            )
            else ()
        
        return $currentUserInfo/preferences/project[@id = $projectId]
    )
};

(:~
:   Patches data for all users incorrectly added by aduser:updateProjectPreference leading to nested user/preferences. Before:
:   <preferences>
:       <project id="2.16.840.1.113883.3.1937.99.62.3" prefix="demo1-" count="44" at="2017-01-16T05:13:54"/>
:       <preferences>
:           <project id="2.16.840.1.113883.2.4.3.11.60.90" prefix="peri20-" count="1" at="2017-01-19T14:03:05"/>
:       </preferences>
:       <preferences>
:           <project id="2.16.840.1.113883.2.4.3.11.60.90" prefix="peri20-" count="1" at="2017-02-02T20:20:00"/>
:       </preferences>
:   </preferences>
:   after:
:       <preferences>
:           <project id="2.16.840.1.113883.3.1937.99.62.3" prefix="demo1-" count="44" at="2017-01-16T05:13:54"/>
:       </preferences>
:   
:   @return nothing
:   @since 2017-03-25
:)
declare function aduser:patchAllProjectPreferences() {
    let $delete := update delete $aduser:colUserInfo//user/preferences/preferences
    return ()
};

(:~ Patches groups section for all users so it is no longer a space separated list but a list of child element call group. Before:
:   <groups>group1 group2 group 3</groups>
:
:   after:
:   <groups>
:       <group>group1</group>
:       <group>group2</group>
:       <group>group 3</group>
:   </groups>
:   
:   @return nothing
:   @since 2017-06-12
:)
declare function aduser:patchAllUserGroups() {
    for $userInfo in $aduser:colUserInfo//user
    let $userName       := $userInfo/@name
    return
        if (sm:user-exists($userName)) then (
            aduser:setUserGroups($userName)
        )
        else (
            xmldb:remove(util:collection-name($userInfo), util:document-name($userInfo))
        )
};

(:~ Update art-data user-info.xml in case people have been using the eXist-db dashboard for user management. Additional entries in user-info.xml 
:   are removed. Missing users are added to user-info.xml. Matching entries in user-info.xml are updated for active status and groups
:
:   @return nothing
:   @since 2019-08-17
:)
declare function aduser:patchAllUserInfo() {
    (: rewrite any art-data/user-info.xml users as art-data/user-info/{user/@name}.xml and then remove user-info.xml :)
    let $docName        := 'user-info.xml'
    let $rewrite        :=
        if (doc-available($get:strArtData || '/' || $docName)) then (
            let $r      :=
                for $user in doc($get:strArtData || '/' || $docName)/*/user
                return
                    xmldb:store($aduser:strUserInfo, $user/@name || '.xml', $user)
            let $u      := xmldb:remove($get:strArtData, $docName)
            
            return ()
        ) 
        else (
            for $r in xmldb:get-child-resources($get:strArtData || '/user-info')
            return
                if ($r = 'guest.xml') then sm:chmod(xs:anyURI($get:strArtData || '/user-info/' || $r), 'rw-rw-rw-') else sm:chmod(xs:anyURI($get:strArtData || '/user-info/' || $r), 'rw-rw----')
        )
    
    (: remove users in user-info that have no corresponding eXist-db user :)
    let $removeUsers    :=
        for $user in aduser:getUserList(())
        let $userInfo       := aduser:getUserInfo($user)
        return
            if (sm:user-exists($user)) then () else if (empty($userInfo)) then () else (
                xmldb:remove(util:collection-name($userInfo), util:document-name($userInfo))
            )
    
    (: add users in user-info that have a corresponding eXist-db user, and/or set active status :)
    let $addUsers       :=
        for $username in sm:list-users()[not(. = 'SYSTEM')]
        let $displayName    := (sm:get-account-metadata($username, $aduser:uriPropFullName), aduser:getUserInfo($username)/displayName[string-length() gt 0])[not(. = $username)][1]
        return (
            aduser:setUserActive($username), aduser:setUserGroups($username), aduser:setUserDisplayName($username, ($displayName, $username)[1])
        )
    let $fixPermissions := sm:chmod(xs:anyURI($get:strArtData || '/user-info'), 'rwsrwsr-x')
    let $fixPermissions := sm:chown(xs:anyURI($get:strArtData || '/user-info'), 'admin:dba')
    let $fixPermissions :=
        for $r in xmldb:get-child-resources($get:strArtData || '/user-info')
        let $docname    := $get:strArtData || '/user-info/' || $r
        let $username   := if (doc-available($docname)) then doc($docname)/*/@name else ()
        return
            if ($r = 'guest.xml') then 
                sm:chmod(xs:anyURI($docname), 'rw-rw-rw-') 
            else (
                sm:chmod(xs:anyURI($docname), 'rw-rw----'),
                if (empty($username)) then () else sm:chown(xs:anyURI($docname), $username || ':dba')
            )
    
    return ()
};

(:~
:   See aduser:setUserDecorSubscriptionSettings($username, $prefix, $issueTypes)
:   
:   @param $prefix The project prefix to set subscriptions for
:   @param $issueTypes  Array of space separated issue object types. Supported types may be found in DECOR.xsd under simpleType DecorObjectType. 
:                       Special: #ALL (any issue), #NOOB (issues without objects), #ISAUTHOR (issues the user authored), #ISASSIGNED (issues the user is currently assigned to)
:   @since 2014-06-23
:)
declare function aduser:setUserDecorSubscriptionSettings($prefix as xs:string, $issueTypes as xs:string+) {
    aduser:setUserDecorSubscriptionSettings(get:strCurrentUserName(), $prefix, $issueTypes)
};

(:~
:   Sets the settings for the given project. If this username is not equal to the currently logged in user, the 
:   currently logged in user needs to be part of a group with permissions. If he is not, an error is returned.
:   
:   @param $username - required. The username to set the info for
:   @param $prefix - required. The project prefix to set subscriptions for
:   @param $issueTypes - required.  Array of space separated issue object types. Supported types may be found in DECOR.xsd under simpleType DecorObjectType. 
:                       Special: #ALL (any issue), #NOOB (issues without objects), #ISAUTHOR (issues the user authored), #ISASSIGNED (issues the user is currently assigned to)
:   @return nothing or error()
:   @since 2014-06-23
:)
declare function aduser:setUserDecorSubscriptionSettings($username as xs:string, $prefix as xs:string, $issueTypes as xs:string+) {
    let $inputCheck             :=
        if ($issueTypes[not(.=$aduser:arrSubscriptionTypes)]) then
            error(QName('http://art-decor.org/ns/error', 'InvalidIssueSubscriptionType'), concat('Issue type "',string-join($issueTypes[not(.=$aduser:arrSubscriptionTypes)],' '),'" is not supported. Supported types are "',string-join($aduser:arrSubscriptionTypes,' '),'"'))
        else ()
    let $newProjectSettings     := <project prefix="{$prefix}" subscribeIssues="{string-join($issueTypes,' ')}"/>
    let $currentUserInfo        := aduser:createUserInfo($username)
    let $currentDecorSettings   := $currentUserInfo/decor-settings
    let $currentProjectSettings := $currentDecorSettings/project[@prefix=$prefix]
    
    return
        if ($currentProjectSettings) then
            update replace $currentProjectSettings with $newProjectSettings
        else if ($currentDecorSettings) then
            update insert $newProjectSettings into $currentUserInfo
        else (
            update insert <decor-settings>{$newProjectSettings}</decor-settings> into $currentUserInfo
        )
};

(:~
:   Deletes all active subscriptions for the currently logged in user and the given project or all projects if empty. 
:   Keeps deactivated subscriptions (@notify='false') if $fullreset=false() and deletes those too if $fullreset=true()
:   
:   @param $prefix - optional. The project prefix to set subscriptions for
:   @param $fullreset - required. If false will keep subscriptions that were deactivated. If false will delete any subscription
:   @return nothing
:   @since 2014-06-23
:)
declare function aduser:deleteUserIssueSubscriptions($prefix as xs:string?, $fullreset as xs:boolean) {
    aduser:deleteUserIssueSubscriptions(get:strCurrentUserName(),$prefix)
};

(:~
:   Deletes all active subscriptions for the given user and the given project or all projects if empty. 
:   Keeps deactivated subscriptions (@notify='false') if $fullreset=false() and deletes those too if $fullreset=true()
:
:   Example: if you want to delete every subscription for user john in all projects 
:       aduser:deleteUserIssueSubscriptions('john', (), true())
:   Example: if you want to delete active subscriptions for user john in project demo1- 
:       aduser:deleteUserIssueSubscriptions('john', 'demo1-', false())
:   
:   @param $username - required. The username to set the info for
:   @param $prefix - optional. The project prefix to set subscriptions for
:   @param $fullreset - required. If false will keep subscriptions that were deactivated. If false will delete any subscription
:   @return nothing
:   @since 2014-06-23
:)
declare function aduser:deleteUserIssueSubscriptions($username as xs:string, $prefix as xs:string?, $fullreset as xs:boolean) {
    let $delete :=
        if (empty($prefix)) then
            if ($fullreset) then
                update delete collection($get:strArtData)/sub:decor-subscriptions/sub:issue[@user=$username]
            else (
                update delete collection($get:strArtData)/sub:decor-subscriptions/sub:issue[@user=$username][@notify='true']
            )
        else (
            if ($fullreset) then
                update delete collection($get:strArtData)/sub:decor-subscriptions/sub:issue[@user=$username][@prefix=$prefix]
            else (
                update delete collection($get:strArtData)/sub:decor-subscriptions/sub:issue[@user=$username][@prefix=$prefix][@notify='true']
            )
        )
    return ()
};

(:~
:   Sets a subscription for the given issueId and the currently logged in user.
:   
:   @param $issueId - optional. The issue id to set the info for
:   @return nothing
:   @since 2014-06-23
:)
declare function aduser:setUserIssueSubscription($issueId as xs:string) {
    aduser:setUserIssueSubscription(get:strCurrentUserName(),$issueId)
};

(:~
:   Sets a subscription for the given issueId and the given user.
:   
:   @param $username - required. The username to set the info for
:   @param $issueId - optional. The issue id to set the info for
:   @return nothing
:   @since 2014-06-23
:)
declare function aduser:setUserIssueSubscription($username as xs:string, $issueId as xs:string) {
    aduser:updateUserIssueSubscription($username,$issueId,true())
};

(:~
:   Explicitly deactivates a subscription for the given issueId and currently logged in user.
:   
:   @param $username - required. The username to set the info for
:   @param $issueId - optional. The issue id to set the info for
:   @return nothing
:   @since 2014-06-23
:)
declare function aduser:unsetUserIssueSubscription($issueId as xs:string) {
    aduser:unsetUserIssueSubscription(get:strCurrentUserName(),$issueId)
};

(:~
:   Explicitly deactivates a subscription for the given issueId and the given user.
:   
:   @param $username - required. The username to set the info for
:   @param $issueId - optional. The issue id to set the info for
:   @return nothing
:   @since 2014-06-23
:)
declare function aduser:unsetUserIssueSubscription($username as xs:string, $issueId as xs:string) {
    aduser:updateUserIssueSubscription($username,$issueId,false())
};

(:~
:   Handles the actual logic for aduser:setUserIssueSubscription and aduser:unsetUserIssueSubscription 
:   based on parameter $activate
:   
:   @param $username - required. The username to set the info for
:   @param $issueId - required. The issue id to set the info for
:   @param $activate - required. true() will activate, false() will explicitly deactivate
:   @return nothing
:   @since 2014-06-23
:)
declare %private function aduser:updateUserIssueSubscription($username as xs:string, $issueId as xs:string, $activate as xs:boolean) {
    let $prefix                     := $get:colDecorData//issue[@id=$issueId]/ancestor::decor/project/@prefix
    let $checkParam                 :=
        if (empty($prefix) and $activate) then
            error(QName('http://art-decor.org/ns/error', 'UnknownIssue'), concat('Unable to add subscription for issue with id ''',$issueId,''' as it does not exist.'))
        else ()
    
    let $newSubscription            := 
        <decor-subscriptions xmlns="http://art-decor.org/ns/art-decor-user-subscriptions">
            <issue id="{$issueId}" user="{$username}" prefix="{$prefix}" notify="{$activate}"/>
        </decor-subscriptions>
    let $currentDecorSubscriptions  := collection($get:strArtData)/sub:decor-subscriptions
    let $currentSubscription        := $currentDecorSubscriptions/sub:issue[@id=$issueId][@user=$username]
    
    let $update                     :=
        if ($currentSubscription[@notify]) then
            update value $currentSubscription/@notify with $activate
        else if ($currentDecorSubscriptions) then
            update insert $newSubscription/sub:issue into $currentDecorSubscriptions
        else (
            let $subscrFile := xmldb:store($get:strArtData,$aduser:strSubscriptionFile, $newSubscription)
            return (
                sm:chgrp(xs:anyURI(concat('xmldb:exist://',$subscrFile)),'decor'),
                sm:chmod(xs:anyURI(concat('xmldb:exist://',$subscrFile)),sm:octal-to-mode('0775')),
                sm:clear-acl(xs:anyURI(concat('xmldb:exist://',$subscrFile)))
            )
        )
    return ()
};