File: System\DirectoryServices\ActiveDirectory\ActiveDirectorySiteLink.cs
Web Access
Project: src\src\runtime\src\libraries\System.DirectoryServices\src\System.DirectoryServices.csproj (System.DirectoryServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;

namespace System.DirectoryServices.ActiveDirectory
{
    public class ActiveDirectorySiteLink : IDisposable
    {
        internal readonly DirectoryContext context;
        private readonly string _name;
        private readonly ActiveDirectoryTransportType _transport = ActiveDirectoryTransportType.Rpc;
        private bool _disposed;

        internal bool existing;
        internal readonly DirectoryEntry cachedEntry;
        private const int systemDefaultCost = 0;
        private readonly TimeSpan _systemDefaultInterval = new TimeSpan(0, 15, 0);
        private const int appDefaultCost = 100;
        private const int appDefaultInterval = 180;
        private readonly ActiveDirectorySiteCollection _sites = new ActiveDirectorySiteCollection();
        private bool _siteRetrieved;

        public ActiveDirectorySiteLink(DirectoryContext context, string siteLinkName) : this(context, siteLinkName, ActiveDirectoryTransportType.Rpc, null)
        {
        }

        public ActiveDirectorySiteLink(DirectoryContext context, string siteLinkName, ActiveDirectoryTransportType transport) : this(context, siteLinkName, transport, null)
        {
        }

        public ActiveDirectorySiteLink(DirectoryContext context, string siteLinkName, ActiveDirectoryTransportType transport, ActiveDirectorySchedule? schedule)
        {
            ValidateArgument(context, siteLinkName, transport);

            //  work with copy of the context
            context = new DirectoryContext(context);

            this.context = context;
            _name = siteLinkName;
            _transport = transport;

            // bind to the rootdse to get the configurationnamingcontext
            DirectoryEntry de;

            try
            {
                de = DirectoryEntryManager.GetDirectoryEntry(context, WellKnownDN.RootDSE);
                string config = (string)PropertyManager.GetPropertyValue(context, de, PropertyManager.ConfigurationNamingContext)!;

                string parentDN;
                if (transport == ActiveDirectoryTransportType.Rpc)
                    parentDN = "CN=IP,CN=Inter-Site Transports,CN=Sites," + config;
                else
                    parentDN = "CN=SMTP,CN=Inter-Site Transports,CN=Sites," + config;

                de = DirectoryEntryManager.GetDirectoryEntry(context, parentDN);
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }
            catch (ActiveDirectoryObjectNotFoundException)
            {
                // this is the case where the context is a config set and we could not find an ADAM instance in that config set
                throw new ActiveDirectoryOperationException(SR.Format(SR.ADAMInstanceNotFoundInConfigSet, context.Name));
            }

            try
            {
                string rdn = "cn=" + _name;
                rdn = Utils.GetEscapedPath(rdn);
                cachedEntry = de.Children.Add(rdn, "siteLink");
                cachedEntry.Properties["cost"].Value = appDefaultCost;
                cachedEntry.Properties["replInterval"].Value = appDefaultInterval;
                if (schedule != null)
                    cachedEntry.Properties[nameof(schedule)].Value = schedule.GetUnmanagedSchedule();
            }
            catch (COMException e)
            {
                if (e.ErrorCode == unchecked((int)0x80072030))
                {
                    // if it is ADAM and transport type is SMTP, throw NotSupportedException.
                    DirectoryEntry tmpDE = DirectoryEntryManager.GetDirectoryEntry(context, WellKnownDN.RootDSE);
                    if (Utils.CheckCapability(tmpDE, Capability.ActiveDirectoryApplicationMode) && transport == ActiveDirectoryTransportType.Smtp)
                    {
                        throw new NotSupportedException(SR.NotSupportTransportSMTP);
                    }
                }

                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }
            finally
            {
                de.Dispose();
            }
        }

        internal ActiveDirectorySiteLink(DirectoryContext context, string siteLinkName, ActiveDirectoryTransportType transport, bool existing, DirectoryEntry entry)
        {
            this.context = context;
            _name = siteLinkName;
            _transport = transport;
            this.existing = existing;
            this.cachedEntry = entry;
        }

        public static ActiveDirectorySiteLink FindByName(DirectoryContext context, string siteLinkName)
        {
            return FindByName(context, siteLinkName, ActiveDirectoryTransportType.Rpc);
        }

        public static ActiveDirectorySiteLink FindByName(DirectoryContext context, string siteLinkName, ActiveDirectoryTransportType transport)
        {
            ValidateArgument(context, siteLinkName, transport);

            //  work with copy of the context
            context = new DirectoryContext(context);

            // bind to the rootdse to get the configurationnamingcontext
            DirectoryEntry de;

            try
            {
                de = DirectoryEntryManager.GetDirectoryEntry(context, WellKnownDN.RootDSE);
                string config = (string)PropertyManager.GetPropertyValue(context, de, PropertyManager.ConfigurationNamingContext)!;
                string containerDN = "CN=Inter-Site Transports,CN=Sites," + config;
                if (transport == ActiveDirectoryTransportType.Rpc)
                    containerDN = "CN=IP," + containerDN;
                else
                    containerDN = "CN=SMTP," + containerDN;
                de = DirectoryEntryManager.GetDirectoryEntry(context, containerDN);
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }
            catch (ActiveDirectoryObjectNotFoundException)
            {
                // this is the case where the context is a config set and we could not find an ADAM instance in that config set
                throw new ActiveDirectoryOperationException(SR.Format(SR.ADAMInstanceNotFoundInConfigSet, context.Name));
            }

            try
            {
                ADSearcher adSearcher = new ADSearcher(de,
                                                      "(&(objectClass=siteLink)(objectCategory=SiteLink)(name=" + Utils.GetEscapedFilterValue(siteLinkName) + "))",
                                                      ActiveDirectorySite.s_distinguishedName,
                                                      SearchScope.OneLevel,
                                                      false, /* don't need paged search */
                                                      false /* don't need to cache result */
                                                      );
                SearchResult? srchResult = adSearcher.FindOne();
                if (srchResult == null)
                {
                    // no such sitelink object
                    Exception e = new ActiveDirectoryObjectNotFoundException(SR.DSNotFound, typeof(ActiveDirectorySiteLink), siteLinkName);
                    throw e;
                }
                else
                {
                    DirectoryEntry connectionEntry = srchResult.GetDirectoryEntry();
                    // it is an existing site object
                    ActiveDirectorySiteLink link = new ActiveDirectorySiteLink(context, siteLinkName, transport, true, connectionEntry);
                    return link;
                }
            }
            catch (COMException e)
            {
                if (e.ErrorCode == unchecked((int)0x80072030))
                {
                    // if it is ADAM and transport type is SMTP, throw NotSupportedException.
                    DirectoryEntry tmpDE = DirectoryEntryManager.GetDirectoryEntry(context, WellKnownDN.RootDSE);
                    if (Utils.CheckCapability(tmpDE, Capability.ActiveDirectoryApplicationMode) && transport == ActiveDirectoryTransportType.Smtp)
                    {
                        throw new NotSupportedException(SR.NotSupportTransportSMTP);
                    }
                    else
                    {
                        // object is not found since we cannot even find the container in which to search
                        throw new ActiveDirectoryObjectNotFoundException(SR.DSNotFound, typeof(ActiveDirectorySiteLink), siteLinkName);
                    }
                }

                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }
            finally
            {
                de.Dispose();
            }
        }

        public string Name
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                return _name;
            }
        }

        public ActiveDirectoryTransportType TransportType
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                return _transport;
            }
        }

        public ActiveDirectorySiteCollection Sites
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                if (existing)
                {
                    // if asked the first time, we need to properly construct the site collection
                    if (!_siteRetrieved)
                    {
                        _sites.initialized = false;
                        _sites.Clear();
                        GetSites();
                        _siteRetrieved = true;
                    }
                }
                _sites.initialized = true;
                _sites.de = cachedEntry;
                _sites.context = context;
                return _sites;
            }
        }

        public int Cost
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                try
                {
                    if (cachedEntry.Properties.Contains("cost"))
                        return (int)cachedEntry.Properties["cost"][0]!;
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }

                // property is not set in the directory, we need to return the system default value
                return systemDefaultCost;
            }
            set
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                if (value < 0)
                    throw new ArgumentException(null, nameof(value));

                try
                {
                    cachedEntry.Properties["cost"].Value = value;
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
        }

        public TimeSpan ReplicationInterval
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                try
                {
                    if (cachedEntry.Properties.Contains("replInterval"))
                    {
                        int tmpValue = (int)cachedEntry.Properties["replInterval"][0]!;
                        return new TimeSpan(0, tmpValue, 0);
                    }
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }

                return _systemDefaultInterval;
            }
            set
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                if (value < TimeSpan.Zero)
                    throw new ArgumentException(SR.NoNegativeTime, nameof(value));

                double tmpVal = value.TotalMinutes;
                if (tmpVal > int.MaxValue)
                    throw new ArgumentException(SR.ReplicationIntervalExceedMax, nameof(value));

                int totalMinutes = (int)tmpVal;
                if (totalMinutes < tmpVal)
                    throw new ArgumentException(SR.ReplicationIntervalInMinutes, nameof(value));

                try
                {
                    cachedEntry.Properties["replInterval"].Value = totalMinutes;
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
        }

        public bool ReciprocalReplicationEnabled
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                int options = 0;
                PropertyValueCollection? propValue = null;
                try
                {
                    propValue = cachedEntry.Properties["options"];
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }

                if (propValue.Count != 0)
                {
                    options = (int)propValue[0]!;
                }

                //NTDSSITELINK_OPT_TWOWAY_SYNC ( 1 << 1 )  force sync in opposite direction at end of sync
                if ((options & 0x2) == 0)
                    return false;
                else
                    return true;
            }
            set
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                int options = 0;
                PropertyValueCollection? propValue = null;

                try
                {
                    propValue = cachedEntry.Properties["options"];

                    if (propValue.Count != 0)
                    {
                        options = (int)propValue[0]!;
                    }

                    //NTDSSITELINK_OPT_TWOWAY_SYNC ( 1 << 1 )  force sync in opposite direction at end of sync
                    if (value)
                    {
                        options |= 0x2;
                    }
                    else
                    {
                        options &= (~(0x2));
                    }
                    cachedEntry.Properties["options"].Value = options;
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
        }

        public bool NotificationEnabled
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                int options = 0;
                PropertyValueCollection? propValue = null;
                try
                {
                    propValue = cachedEntry.Properties["options"];
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }

                if (propValue.Count != 0)
                {
                    options = (int)propValue[0]!;
                }

                // NTDSSITELINK_OPT_USE_NOTIFY ( 1 << 0 )   Use notification on this link
                if ((options & 0x1) == 0)
                    return false;
                else
                    return true;
            }
            set
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                int options = 0;
                PropertyValueCollection? propValue = null;

                try
                {
                    propValue = cachedEntry.Properties["options"];
                    if (propValue.Count != 0)
                    {
                        options = (int)propValue[0]!;
                    }

                    // NTDSSITELINK_OPT_USE_NOTIFY ( 1 << 0 )   Use notification on this link
                    if (value)
                    {
                        options |= 0x1;
                    }
                    else
                    {
                        options &= (~(0x1));
                    }

                    cachedEntry.Properties["options"].Value = options;
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
        }

        public bool DataCompressionEnabled
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                int options = 0;
                PropertyValueCollection? propValue = null;

                try
                {
                    propValue = cachedEntry.Properties["options"];
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }

                if (propValue.Count != 0)
                {
                    options = (int)propValue[0]!;
                }

                //NTDSSITELINK_OPT_DISABLE_COMPRESSION ( 1 << 2 )
                //  0 - Compression of replication data across this site link enabled
                //  1 - Compression of replication data across this site link disabled
                if ((options & 0x4) == 0)
                    return true;
                else
                    return false;
            }
            set
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                int options = 0;
                PropertyValueCollection? propValue = null;

                try
                {
                    propValue = cachedEntry.Properties["options"];
                    if (propValue.Count != 0)
                    {
                        options = (int)propValue[0]!;
                    }

                    //NTDSSITELINK_OPT_DISABLE_COMPRESSION ( 1 << 2 )
                    //  0 - Compression of replication data across this site link enabled
                    //  1 - Compression of replication data across this site link disabled
                    if (!value)
                    {
                        options |= 0x4;
                    }
                    else
                    {
                        options &= (~(0x4));
                    }
                    cachedEntry.Properties["options"].Value = options;
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
        }

        public ActiveDirectorySchedule? InterSiteReplicationSchedule
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                ActiveDirectorySchedule? schedule = null;
                try
                {
                    if (cachedEntry.Properties.Contains("schedule"))
                    {
                        byte[] tmpSchedule = (byte[])cachedEntry.Properties["schedule"][0]!;
                        Debug.Assert(tmpSchedule != null && tmpSchedule.Length == 188);
                        schedule = new ActiveDirectorySchedule();
                        schedule.SetUnmanagedSchedule(tmpSchedule);
                    }
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }

                return schedule;
            }
            set
            {
                if (_disposed)
                    throw new ObjectDisposedException(GetType().Name);

                try
                {
                    if (value == null)
                    {
                        if (cachedEntry.Properties.Contains("schedule"))
                            cachedEntry.Properties["schedule"].Clear();
                    }
                    else
                    {
                        cachedEntry.Properties["schedule"].Value = value.GetUnmanagedSchedule();
                    }
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
        }

        public void Save()
        {
            if (_disposed)
                throw new ObjectDisposedException(GetType().Name);

            try
            {
                cachedEntry.CommitChanges();
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }

            if (existing)
            {
                _siteRetrieved = false;
            }
            else
            {
                existing = true;
            }
        }

        public void Delete()
        {
            if (_disposed)
                throw new ObjectDisposedException(GetType().Name);

            if (!existing)
            {
                throw new InvalidOperationException(SR.CannotDelete);
            }
            else
            {
                try
                {
                    cachedEntry.Parent.Children.Remove(cachedEntry);
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
        }

        public override string ToString()
        {
            if (_disposed)
                throw new ObjectDisposedException(GetType().Name);

            return _name;
        }

        public DirectoryEntry GetDirectoryEntry()
        {
            if (_disposed)
                throw new ObjectDisposedException(GetType().Name);

            if (!existing)
            {
                throw new InvalidOperationException(SR.CannotGetObject);
            }
            else
            {
                return DirectoryEntryManager.GetDirectoryEntryInternal(context, cachedEntry.Path);
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // free other state (managed objects)
                cachedEntry?.Dispose();
            }

            // free your own state (unmanaged objects)

            _disposed = true;
        }

        private static void ValidateArgument(DirectoryContext context, string siteLinkName, ActiveDirectoryTransportType transport)
        {
            // basic validation first
            if (context == null)
                throw new ArgumentNullException(nameof(context));

            // if target is not specified, then we determin the target from the logon credential, so if it is a local user context, it should fail
            if ((context.Name == null) && (!context.isRootDomain()))
            {
                throw new ArgumentException(SR.ContextNotAssociatedWithDomain, nameof(context));
            }

            // more validation for the context, if the target is not null, then it should be either forest name or server name
            if (context.Name != null)
            {
                if (!(context.isRootDomain() || context.isServer() || context.isADAMConfigSet()))
                    throw new ArgumentException(SR.NotADOrADAM, nameof(context));
            }

            if (siteLinkName == null)
                throw new ArgumentNullException(nameof(siteLinkName));

            if (siteLinkName.Length == 0)
                throw new ArgumentException(SR.EmptyStringParameter, nameof(siteLinkName));

            if (transport < ActiveDirectoryTransportType.Rpc || transport > ActiveDirectoryTransportType.Smtp)
                throw new InvalidEnumArgumentException("value", (int)transport, typeof(ActiveDirectoryTransportType));
        }

        private void GetSites()
        {
            NativeComInterfaces.IAdsPathname? pathCracker = null;
            pathCracker = (NativeComInterfaces.IAdsPathname)new NativeComInterfaces.Pathname();
            ArrayList propertyList = new ArrayList();
            // need to turn off the escaping for name
            pathCracker.EscapedMode = NativeComInterfaces.ADS_ESCAPEDMODE_OFF_EX;
            string propertyName = "siteList";

            propertyList.Add(propertyName);
            Hashtable values = Utils.GetValuesWithRangeRetrieval(cachedEntry, "(objectClass=*)", propertyList, SearchScope.Base);
            ArrayList? siteLists = (ArrayList?)values[propertyName.ToLowerInvariant()];

            // somehow no site list
            if (siteLists == null)
                return;

            for (int i = 0; i < siteLists.Count; i++)
            {
                string dn = (string)siteLists[i]!;

                // escaping manipulation
                pathCracker.Set(dn, NativeComInterfaces.ADS_SETTYPE_DN);
                string rdn = pathCracker.Retrieve(NativeComInterfaces.ADS_FORMAT_LEAF);
                Debug.Assert(rdn != null && Utils.Compare(rdn, 0, 3, "CN=", 0, 3) == 0);
                rdn = rdn.Substring(3);
                ActiveDirectorySite site = new ActiveDirectorySite(context, rdn, true);

                // add to the collection
                _sites.Add(site);
            }
        }
    }
}