Codeunit 9510 Document Service Management, source in 29

Source29

src/Layers/W1/BaseApp/eServices/EDocument/DocumentServiceManagement.Codeunit.al1472 lines, Copyright (c) Microsoft Corporation. MIT

// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.EServices.EDocument;

using System;
using System.Azure.Identity;
using System.Azure.KeyVault;
using System.Environment;
using System.Integration;
using System.IO;
using System.Privacy;
using System.Security.Authentication;
using System.Security.Encryption;
using System.Telemetry;
using System.Utilities;

codeunit 9510 "Document Service Management"
{
    // Provides functions for the storage of documents to online services such as O365 (Office 365).
    Permissions = tabledata "Tenant Media" = rimd;
    InherentEntitlements = X;
    InherentPermissions = X;

    trigger OnRun()
    begin
    end;

    var
        NoConfigErr: Label 'No online document configuration was found.';
        MultipleConfigsErr: Label 'More than one online document configuration was found.';
        SourceFileNotFoundErr: Label 'Cannot open the specified document from the following location: %1 due to the following error: %2.', Comment = '%1=Full path to the file on disk;%2=the detailed error describing why the document could not be accessed.';
        RequiredSourceNameErr: Label 'You must specify a source path for the document.';
        DocumentService: DotNet IDocumentService;
        DocumentServiceFactory: DotNet DocumentServiceFactory;
        ServiceType: Text;
        LastServiceType: Text;
        RequiredTargetNameErr: Label 'You must specify a name for the document.';
        RequiredTargetURIErr: Label 'You must specify the URI that you want to open.';
        ValidateConnectionErr: Label 'Cannot connect because the user name and password have not been specified, or because the connection was canceled.';
        AccessTokenErrMsg: Label 'Failed to acquire an access token.';
        AccessTokenEmptyErr: Label 'There was a problem authenticating. Please try to sign out and sign in again.';
        MissingClientIdOrSecretErr: Label 'The client ID or client secret have not been initialized.';
        SharePointIsoStorageSecretNotConfiguredErr: Label 'Client secret for SharePoint has not been configured.';
        SharePointIsoStorageSecretNotConfiguredLbl: Label 'Client secret for SharePoint has not been configured.', Locked = true;
        AuthTokenOrCodeNotReceivedErr: Label 'No access token or authorization error code received. The authorization failure error is: %1.', Comment = '%1=Authentiaction Failure Error', Locked = true;
        AccessTokenAcquiredFromCacheErr: Label 'The attempt to acquire the access token form cache has failed.', Locked = false;
        OAuthAuthorityUrlLbl: Label 'https://login.microsoftonline.com/common/oauth2', Locked = true;
        SharePointTelemetryCategoryTxt: Label 'AL Sharepoint Integration', Locked = true;
        SharePointClientIdAKVSecretNameLbl: Label 'sharepoint-clientid', Locked = true;
        SharePointClientSecretAKVSecretNameLbl: Label 'sharepoint-clientsecret', Locked = true;
        MissingClientIdTelemetryTxt: Label 'The client ID has not been initialized.', Locked = true;
        MissingClientSecretTelemetryTxt: Label 'The client secret has not been initialized.', Locked = true;
        InitializedClientIdTelemetryTxt: Label 'The client ID has been initialized.', Locked = true;
        InitializedClientSecretTelemetryTxt: Label 'The client secret has been initialized.', Locked = true;
        AcquiredTokenOnBehalfFlowTxt: Label 'Acquired a token using the on behalf flow.', Locked = true;
        CantFindMySiteErr: Label 'Could not determine the location of your OneDrive for Business, contact your partner to set this up.';
        CantFindMySiteTryLoginErr: Label 'Could not connect to your OneDrive for Business.\Try to sign in to OneDrive from your browser at https://portal.office.com/onedrive.\\Contact your partner if you''re unsure whether you have access to OneDrive.';
        UnknownLocationErr: Label 'An unexpected error occurred while trying to configure the Document Service. Try again later.';
        DocumentSharingNoNameErr: Label 'The document to be shared has not specified a name.';
        DocumentSharingNoExtErr: Label 'The document to be shared has not specified a file extension.';
        SharePointFileExistsInstructionsTxt: Label 'A file named "%1" already exists in your %2 folder in OneDrive for Business.\\ Would you like to use the existing file, add this new file as the latest version of the existing file, or rename it and keep them both?', Comment = '%1 = a file name, for example "CustomerCard.xlsx"; %2 = the product name, for example "Business Central"';
        SharePointFileExistsOptionsTxt: Label 'Use existing,Replace,Keep both', Comment = 'A comma separated list with options.';
        DocumentServiceCategoryLbl: Label 'AL DocumentService', Locked = true;
        DocumentSharingStartLbl: Label 'Handling document sharing event.', Locked = true;
        DocumentServiceDefaultingLbl: Label 'Configuring defaults for document service', Locked = true;
        DefaultLocationErrCodeErr: Label 'Graph request to determine root storage location resulted in status code: %1', Locked = true;
        CouldNotFindLocationInResponseErr: Label 'Graph request could not find the webUrl property for the default location.', Locked = true;
        LocationResponseInvalidErr: Label 'Graph request did not return a JSON document.', Locked = true;
        LocationFoundTxt: Label 'A default location was found of length %1', Locked = true;
        TokenRequestTxt: Label 'A token was requested. GetTokenFromCache: %1', Locked = true;
        CheckingDriveProgressTxt: Label 'Checking that you have a valid drive.';
        CheckingBcDocumentFolderProgressTxt: Label 'Checking that the default folder exists.';
        SharepointUnexpectedStatusCodeErr: Label 'OneDrive returned an unexpected error code: %1.', Comment = '%1 = An error code from OneDrive, for example 503';
        SharepointInvalidJsonErr: Label 'OneDrive returned an invalid response. Details: %1.', Comment = '%1 = The response details from OneDrive (e.g. "Your Drive is not available")';
        MissingOneDriveLicenseSaasErr: Label 'You don''t have a license for OneDrive.';
        MissingOneDriveLicenseOnPremErr: Label 'You don''t have a license for OneDrive, or your Microsoft Entra application for %1 on-premises doesn''t have the necessary permissions.', Comment = '%1 = the product name for Business Central';
        GraphApiUrlOnPremTxt: Label 'https://graph.microsoft.com', Locked = true;
        StartingLinkGenerationTelemetryMsg: Label 'Starting OneDrive link generation.', Locked = true;
        UsingDefaultDocumentServiceTelemetryMsg: Label 'Using default Document Service setup.', Locked = true;
        UsingCustomDocumentServiceTelemetryMsg: Label 'Using a Document Service retrieved from the database.', Locked = true;
        EmptyTokenTelemetryMsg: Label 'Empty access token from Azure AD Mgt.', Locked = true;
        SharepointStatusCodeTelemetryMsg: Label 'Sharepoint returned an error code: %1.', Locked = true;
        SharepointEmptyFileTelemetryMsg: Label 'Sharepoint returned an empty file.', Locked = true;
        SharepointFileTelemetryMsg: Label 'Sharepoint file size: %1', Locked = true;
        NoCompanyFolderTelemetryMsg: Label 'No company specific folder found, falling back to base Document Service folder.', Locked = true;
        NoCompanyOrBcFolderTelemetryMsg: Label 'No Document Service folder found, falling back to OneDrive root.', Locked = true;
        ConfigurationForTestConnectionTelemetryMsg: Label 'Configuration for test connection retrieved, with authentication: %1.', Locked = true;
        TrySaveStreamFromRecTelemetryMsg: Label 'TrySaveStreamFromRec started with conflict behavior: %1.', Locked = true;
        SettingResourceLocationTelemetryTxt: Label 'Setting resource location (old location length: %1, new location length: %2).', Locked = true;
        LocationTooLongTelemetryMsg: Label 'Maximum location length is %1, but the new location has length %2.', Locked = true;
        OneDriveFeatureNameTelemetryTxt: Label 'OneDrive', Locked = true;
        TokenRequestEventTelemetryTxt: Label 'Token Request', Locked = true;
        HttpsDomainTxt: Label 'https://%1', Locked = true;
        ValueDoesNotExistErr: Label 'Value does not exist: %1', Comment = '%1 = The response details from OneDrive (e.g. "Your Drive is not available")';
        DownloadUrlDoesNotExistErr: Label 'Download Url does not exist: %1', Comment = '%1 = The response details from OneDrive (e.g. "Your Drive is not available")';
        SharepointUnexpectedErr: Label 'OneDrive returned an unexpected value. Try again later.';
        SharepointItemIdMsg: Label 'OneDrive item: %1', Comment = '%1 = Item id of file', Locked = true;
        SharepointUnableToGetDownloadUrlMsg: Label 'No download url returned by sharepoint.', Locked = true;

    [Scope('OnPrem')]
    procedure TestConnection()
    var
        DocumentServiceRec: Record "Document Service";
        DocumentServiceHelper: DotNet NavDocumentServiceHelper;
    begin
        // Tests connectivity to the Document Service using the current configuration in Dynamics NAV.
        // An error occurrs if unable to successfully connect.
        if not IsConfiguredLegacy() then
            Error(NoConfigErr);

        DocumentServiceHelper.Reset();
        SetDocumentService();

        if DocumentServiceRec.FindFirst() then begin
            Session.LogMessage('0000FR2', StrSubstNo(ConfigurationForTestConnectionTelemetryMsg, DocumentServiceRec."Authentication Type"), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', DocumentServiceCategoryLbl);

            SetProperties(false, DocumentServiceRec);

            if DocumentServiceRec."Authentication Type" = DocumentServiceRec."Authentication Type"::Legacy then
                if IsNull(DocumentService.Credentials) then
                    Error(ValidateConnectionErr);
        end else
            Error(NoConfigErr);

        DocumentService.ValidateConnection();
        CheckError();
    end;

    [Scope('OnPrem')]
    procedure SaveFile(SourcePath: Text; TargetName: Text; ConflictBehavior: Enum "Doc. Sharing Conflict Behavior"): Text
    var
        SourceFile: File;
        SourceInStream: InStream;
    begin
        // Saves a file to the Document Service using the configured location specified in Dynamics NAV.
        // SourcePath: The path to a physical file on the Dynamics NAV server.
        // TargetName: The name which will be given to the file saved to the Document Service.
        // Overwrite: TRUE if the target file should be overwritten.
        // - An error is shown if Overwrite is FALSE and a file with that name already exists.
        // Returns: A URI to the file on the Document Service.

        if SourcePath = '' then
            Error(RequiredSourceNameErr);

        if TargetName = '' then
            Error(RequiredTargetNameErr);

        if not IsConfigured() then
            Error(NoConfigErr);

        if not SourceFile.Open(SourcePath) then
            Error(SourceFileNotFoundErr, SourcePath, GetLastErrorText);

        SourceFile.CreateInStream(SourceInStream);

        exit(SaveStream(SourceInStream, TargetName, ConflictBehavior));
    end;

    procedure RunDocumentServiceSetup(Notification: Notification)
    begin
        Page.Run(Page::"Document Service Setup");
    end;

    procedure GetOneDriveScenario(var DocumentServiceScenario: Record "Document Service Scenario"): Boolean
    var
        Company: Record "Company";
        NullGuid: Guid;
    begin
        Company.Get(CompanyName());

        if not DocumentServiceScenario.Get(DocumentServiceScenario."Service Integration"::OneDrive, Company.Id) then
            if not DocumentServiceScenario.Get(DocumentServiceScenario."Service Integration"::OneDrive, NullGuid) then
                exit(false);

        exit(true);
    end;

    procedure IsConfiguredLegacy(): Boolean
    var
        DocumentServiceRec: Record "Document Service";
        DocumentServiceScenario: Record "Document Service Scenario";
    begin
        if not DocumentServiceScenario.IsEmpty() then
            exit(false);

        if DocumentServiceRec.Count > 1 then
            Error(MultipleConfigsErr);

        if not DocumentServiceRec.FindFirst() then
            exit(false);

        if (DocumentServiceRec.Location = '') or (DocumentServiceRec.Folder = '') then
            exit(false);

        exit(true);
    end;

    local procedure IsOneDriveEnabled(): Boolean
    var
        DocumentServiceRec: Record "Document Service";
        DocumentServiceScenario: Record "Document Service Scenario";
        EnvironmentInformation: Codeunit "Environment Information";
    begin
        GetOneDriveScenario(DocumentServiceScenario);

        if not DocumentServiceScenario."Use for Application" then
            exit(false);

        if EnvironmentInformation.IsOnPrem() then begin
            if DocumentServiceScenario."Document Service" = '' then
                exit(false);

            exit(DocumentServiceRec.Get(DocumentServiceScenario."Document Service"));
        end;

        exit(true);
    end;

    local procedure IsOneDriveEnabledForSystem(): Boolean
    var
        DocumentServiceScenario: Record "Document Service Scenario";
    begin
        GetOneDriveScenario(DocumentServiceScenario);
        exit(DocumentServiceScenario."Use for Platform");
    end;

    local procedure IsOneDriveEnabledOrUsingLegacySetup(): Boolean
    var
        DocumentServiceScenario: Record "Document Service Scenario";
        EnvironmentInformation: Codeunit "Environment Information";
    begin
        if not DocumentServiceScenario.IsEmpty() then
            exit(IsOneDriveEnabled());

        exit(IsConfiguredLegacy() or EnvironmentInformation.IsSaaSInfrastructure());
    end;

    procedure IsConfigured(): Boolean
    var
        DocumentServiceRec: Record "Document Service";
        DocumentServiceScenario: Record "Document Service Scenario";
    begin
        // Returns TRUE if Dynamics NAV has been configured with a Document Service.
        OnBeforeIsConfigured(DocumentServiceRec);

        if not DocumentServiceScenario.IsEmpty() then
            exit(IsOneDriveEnabled());

        exit(IsConfiguredLegacy());
    end;

    [Scope('OnPrem')]
    procedure IsServiceUri(TargetURI: Text): Boolean
    var
        DocumentServiceRec: Record "Document Service";
        IsValid: Boolean;
    begin
        // Returns TRUE if the TargetURI points to a location on the currently-configured Document Service.

        if TargetURI = '' then
            exit(false);

        if DocumentServiceRec.FindLast() then
            if DocumentServiceRec.Location <> '' then begin
                SetDocumentService();
                SetProperties(true, DocumentServiceRec);
                IsValid := DocumentService.IsValidUri(TargetURI);
                CheckError();
                exit(IsValid);
            end;

        exit(false);
    end;

    [Scope('OnPrem')]
    procedure SetServiceType(RequestedServiceType: Text)
    var
        DocumentServiceHelper: DotNet NavDocumentServiceHelper;
    begin
        // Sets the type name of the Document Service.
        // The type must match the DocumentServiceMetadata attribute value on the IDocumentServiceHandler interface
        // exposed by at least one assembly in the Server installation folder.
        // By default, Dynamics NAV uses the SharePoint Online Document Service with type named 'SHAREPOINTONLINE'.
        ServiceType := RequestedServiceType;
        DocumentServiceHelper.SetDocumentServiceType(RequestedServiceType);
    end;

    procedure GetServiceType(): Text
    begin
        // Gets the name of the current Document Service.

        exit(ServiceType);
    end;

    [Scope('OnPrem')]
    procedure OpenDocument(TargetURI: Text)
    begin
        // Navigates to the specified URI on the Document Service from the client device.

        if TargetURI = '' then
            Error(RequiredTargetURIErr);

        if not IsConfigured() then
            Error(NoConfigErr);

        SetDocumentService();
        HyperLink(DocumentService.GenerateViewableDocumentAddress(TargetURI));
        CheckError();
    end;

    local procedure InitializeDefaultService(var DocumentServiceRec: Record "Document Service"; Name: Code[30])
    var
        OAuth2: Codeunit OAuth2;
        RedirectUrl: Text;
    begin
        DocumentServiceRec.Init();
        DocumentServiceRec."Service ID" := Name;
        DocumentServiceRec."Authentication Type" := DocumentServiceRec."Authentication Type"::OAuth2;
        OAuth2.GetDefaultRedirectUrl(RedirectUrl);
        DocumentServiceRec."Redirect URL" := CopyStr(RedirectUrl, 1, MaxStrLen(DocumentServiceRec."Redirect URL"));
        DocumentServiceRec.Location := GetLocation();
        DocumentServiceRec.Folder := GetDefaultFolderName();
    end;

    procedure GetDefaultFolderName(): Text[250]
    begin
        exit(CopyStr(GetSafeDocumentServiceFolderName(ProductName.Short()), 1, 250));
    end;

    local procedure GetSafeCompanyName(): Text
    var
        Company: Record "Company";
        SafeCompanyName: Text;
    begin
        if (not Company.Get(CompanyName())) or (Company."Display Name" = '') then
            exit(GetSafeDocumentServiceFolderName(CompanyName()));

        SafeCompanyName := GetSafeDocumentServiceFolderName(Company."Display Name");

        if SafeCompanyName = '' then
            exit(GetSafeDocumentServiceFolderName(CompanyName()));

        exit(SafeCompanyName);
    end;

    local procedure GetSafeDocumentServiceFolderName(FolderName: Text): Text
    var
        FileManagement: Codeunit "File Management";
    begin
        FolderName := FileManagement.GetSafeFileName(FolderName);
        FolderName := DelChr(FolderName, '<', ' '); // Remove leading whitespace
        FolderName := DelChr(FolderName, '>', '. '); // Remove trailing periods and whitespace
        FolderName := DelChr(FolderName, '=', '%'); // Remove all percentage symbols

        exit(FolderName);
    end;

    local procedure GetLocation(): Text[250]
    var
        DocumentServiceScenario: Record "Document Service Scenario";
        DocumentServiceRec: Record "Document Service";
    begin
        if not GetOneDriveScenario(DocumentServiceScenario) then
            exit(GetDefaultLocation());

        if DocumentServiceScenario."Document Service" <> '' then
            if DocumentServiceRec.Get(DocumentServiceScenario."Document Service") then
                if DocumentServiceRec.Location <> '' then
                    exit(DocumentServiceRec.Location);

        exit(GetDefaultLocation());
    end;

    [TryFunction]
    procedure TryGetDefaultLocation(var Location: Text[250])
    begin
        Location := GetDefaultLocation();
    end;

    local procedure GetDefaultLocation(): Text[250]
    var
        UrlHelper: Codeunit "Url Helper";
        AzureAdMgt: Codeunit "Azure AD Mgt.";
        UriBuilder: Codeunit "Uri Builder";
        HttpWebRequestMgt: Codeunit "Http Web Request Mgt.";
        ResponseHeaders: DotNet NameValueCollection;
        StatusCode: DotNet HttpStatusCode;
        DriveJsonObject: JsonObject;
        Location: Text;
        ResponseContent: Text;
        ResponseErrorMessage: Text;
        ResponseErrorDetails: Text;
        Endpoint: Text;
        Token: SecretText;
    begin
        Endpoint := GetGraphDriveRootUrl();
        Token := AzureAdMgt.GetOnBehalfAccessTokenAsSecretText(UrlHelper.GetGraphUrl());

        if Token.IsEmpty() then begin
            Session.LogMessage('0000FSJ', EmptyTokenTelemetryMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
            Error(AccessTokenEmptyErr);
        end;

        HttpWebRequestMgt.Initialize(Endpoint);
        HttpWebRequestMgt.DisableUI();
        HttpWebRequestMgt.SetReturnType('application/json');
        HttpWebRequestMgt.AddHeader('Authorization', SecretStrSubstNo('Bearer %1', Token));

        if not HttpWebRequestMgt.SendRequestAndReadTextResponse(ResponseContent, ResponseErrorMessage, ResponseErrorDetails, StatusCode, ResponseHeaders) then begin
            if StatusCode in [401, 404] then // Seems to mostly occur when the backend is still provisioning
                Error(CantFindMySiteTryLoginErr);

            CheckLicenseError(StatusCode, ResponseErrorDetails);

            Session.LogMessage('0000FJY', StrSubstNo(DefaultLocationErrCodeErr, StatusCode.ToString()), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', DocumentServiceCategoryLbl);
            Error(UnknownLocationErr);
        end;

        if not DriveJsonObject.ReadFrom(ResponseContent) then begin
            Session.LogMessage('0000FLF', LocationResponseInvalidErr, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', DocumentServiceCategoryLbl);
            Error(UnknownLocationErr);
        end;

        if not ExtractWebUrlFromJson(DriveJsonObject, Location) then begin
            Session.LogMessage('0000FLG', CouldNotFindLocationInResponseErr, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', DocumentServiceCategoryLbl);
            Error(CantFindMySiteTryLoginErr);
        end;

        UriBuilder.Init(Location);
        Location := 'https://' + UriBuilder.GetHost() + '/';

        Session.LogMessage('0000FLH', StrSubstNo(LocationFoundTxt, StrLen(Location)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', DocumentServiceCategoryLbl);
        exit(CopyStr(Location, 1, 250));
    end;

    local procedure CheckLicenseError(HttpStatusCode: Integer; HttpErrorDetails: Text)
    begin
        if HttpStatusCode in [401, 403, 404] then
            LicenseError();

        if (HttpStatusCode = 400) and (StrPos(HttpErrorDetails, 'Tenant does not have a SPO license.') > 0) then
            LicenseError();
    end;

    local procedure SetResourceLocation(var DocumentServiceRec: Record "Document Service"): Boolean
    var
        NewLocation: Text;
    begin
        SetDocumentService();
        SetProperties(true, DocumentServiceRec);

        NewLocation := DocumentService.GetLocation();

        Session.LogMessage('0000GB1', StrSubstNo(SettingResourceLocationTelemetryTxt, StrLen(DocumentServiceRec.Location), StrLen(NewLocation)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', DocumentServiceCategoryLbl);

        if NewLocation <> '' then begin
            if StrLen(NewLocation) > MaxStrLen(DocumentServiceRec.Location) then
                Session.LogMessage('0000GB2', StrSubstNo(LocationTooLongTelemetryMsg, MaxStrLen(DocumentServiceRec.Location), StrLen(NewLocation)), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', DocumentServiceCategoryLbl);

            DocumentServiceRec.Location := CopyStr(NewLocation, 1, MaxStrLen(DocumentServiceRec.Location));
            exit(true);
        end;

        exit(false); // Couldn't resolve a location, credentials may be invalid for the target site
    end;

    [NonDebuggable]
    local procedure SetProperties(GetTokenFromCache: Boolean; var DocumentServiceRec: Record "Document Service")
    var
        DocumentServiceHelper: DotNet NavDocumentServiceHelper;
        AccessToken: SecretText;
    begin
        OnBeforeSetProperties(DocumentServiceRec);
        // The Document Service will throw an exception if the property is not known to the service type provider.
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName(Description), DocumentServiceRec.Description);
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName(Location), DocumentServiceRec.Location);
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName("Document Repository"), DocumentServiceRec."Document Repository");
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName(Folder), DocumentServiceRec.Folder);
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName("Authentication Type"), DocumentServiceRec."Authentication Type");
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName("User Name"), DocumentServiceRec."User Name");

        if (DocumentServiceRec."Authentication Type" = DocumentServiceRec."Authentication Type"::Legacy) then begin
            DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName(Password), DocumentServiceRec.Password);
            DocumentService.Credentials := DocumentServiceHelper.ProvideCredentials();
        end else begin
            GetAccessToken(DocumentServiceRec.Location, AccessToken, GetTokenFromCache);
            DocumentService.Properties.SetProperty('Token', AccessToken.Unwrap());
        end;

        if not (DocumentServiceHelper.LastErrorMessage = '') then
            Error(DocumentServiceHelper.LastErrorMessage);
    end;

    procedure GetMyBusinessCentralFilesLink(): Text
    var
        PrivacyNotice: Codeunit "Privacy Notice";
        PrivacyNoticeRegistrations: Codeunit "Privacy Notice Registrations";
        DocumentServiceFolder: Text;
        DriveRootFolderJson: JsonObject;
        DriveBcFolderJson: JsonObject;
        DriveFolderUrl: Text;
        WebUrl: Text;
        ProgressDialog: Dialog;
    begin
        Session.LogMessage('0000FMJ', StartingLinkGenerationTelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);

        if not PrivacyNotice.ConfirmPrivacyNoticeApproval(PrivacyNoticeRegistrations.GetOneDrivePrivacyNoticeId()) then
            exit;

        ProgressDialog.Open('#1##############################');

        ProgressDialog.Update(1, CheckingDriveProgressTxt);
        GetDriveFolderInfo(GetGraphDriveRootUrl(), DriveRootFolderJson);

        ProgressDialog.Update(1, CheckingBcDocumentFolderProgressTxt);
        // Try to open the company specific folder. If it does not exist, open the document service folder. Otherwise, show a dialog and fall back.
        DocumentServiceFolder := GetDocumentServiceFolder() + '/' + GetSafeCompanyName();
        DriveFolderUrl := MakeChildrenPathUrl(GetGraphDriveRootUrl(), DocumentServiceFolder);
        if GetDriveFolderInfo(DriveFolderUrl, DriveBcFolderJson) then
            if ExtractWebUrlFromJson(DriveBcFolderJson, WebUrl) then
                exit(WebUrl);

        Session.LogMessage('0000FN5', NoCompanyFolderTelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
        DocumentServiceFolder := GetDocumentServiceFolder();
        DriveFolderUrl := MakeChildrenPathUrl(GetGraphDriveRootUrl(), DocumentServiceFolder);
        if GetDriveFolderInfo(DriveFolderUrl, DriveBcFolderJson) then
            if ExtractWebUrlFromJson(DriveBcFolderJson, WebUrl) then
                exit(WebUrl);

        Session.LogMessage('0000FN6', NoCompanyOrBcFolderTelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
        if ExtractWebUrlFromJson(DriveRootFolderJson, WebUrl) then
            exit(WebUrl);
    end;

    [TryFunction]
    local procedure ExtractWebUrlFromJson(DriveBcFolderJson: JsonObject; var WebUrl: Text)
    var
        DriveWebUrlJson: JsonToken;
    begin
        if DriveBcFolderJson.Get('webUrl', DriveWebUrlJson) then
            if DriveWebUrlJson.IsValue then begin
                WebUrl := DriveWebUrlJson.AsValue().AsText();
                exit;
            end;

        Error('');
    end;

    [TryFunction]
    local procedure GetDriveFolderInfo(FolderUrl: Text; var FolderJson: JsonObject)
    var
        HttpWebRequestMgt: Codeunit "Http Web Request Mgt.";
        ResponseBody: Text;
        ErrorMessage: Text;
        ErrorDetails: Text;
        HttpStatusCode: DotNet HttpStatusCode;
        ResponseHeaders: DotNet NameValueCollection;
    begin
        InitializeGraphWebRequest(FolderUrl, HttpWebRequestMgt, 'application/json', 'GET');

        if not HttpWebRequestMgt.SendRequestAndReadTextResponse(ResponseBody, ErrorMessage, ErrorDetails, HttpStatusCode, ResponseHeaders) then begin
            Session.LogMessage('0000FML', StrSubstNo(SharepointStatusCodeTelemetryMsg, HttpStatusCode), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);

            CheckLicenseError(HttpStatusCode, ErrorDetails);
            Error(SharepointUnexpectedStatusCodeErr, HttpStatusCode);
        end;

        if not FolderJson.ReadFrom(ResponseBody) then
            Error(SharepointInvalidJsonErr, ResponseBody);
    end;

    [TryFunction]
    local procedure GetFileContent(var DocumentSharing: Record "Document Sharing")
    var
        HttpWebRequestMgt: Codeunit "Http Web Request Mgt.";
        TempBlob: Codeunit "Temp Blob";
        ErrorMessage: Text;
        ErrorDetails: Text;
        FileUrl: Text;
        HttpStatusCode: DotNet HttpStatusCode;
        ResponseHeaders: DotNet NameValueCollection;
        InStream: InStream;
        OutStream: OutStream;
    begin
        ResolveItemId(DocumentSharing);
        GetFileDownloadUrl(DocumentSharing, FileUrl);
        InitializeOneDriveWebRequest(FileUrl, HttpWebRequestMgt, '');

        if not HttpWebRequestMgt.SendRequestAndReadResponse(TempBlob, ErrorMessage, ErrorDetails, HttpStatusCode, ResponseHeaders) then begin
            Session.LogMessage('0000IN8', StrSubstNo(SharepointStatusCodeTelemetryMsg, HttpStatusCode), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);

            CheckLicenseError(HttpStatusCode, ErrorDetails);
            Error(SharepointUnexpectedStatusCodeErr, HttpStatusCode);
        end;

        if TempBlob.Length() = 0 then
            Session.LogMessage('0000IN9', SharepointEmptyFileTelemetryMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt)
        else begin
            Session.LogMessage('0000JB4', StrSubstNo(SharepointFileTelemetryMsg, TempBlob.Length()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
            DocumentSharing.Data.CreateOutStream(OutStream);
            TempBlob.CreateInStream(InStream);
            CopyStream(OutStream, InStream);
        end;
    end;

    [TryFunction]
    local procedure GetFileDownloadUrl(var DocumentSharing: Record "Document Sharing"; var FileUrl: Text)
    var
        HttpWebRequestMgt: Codeunit "Http Web Request Mgt.";
        ErrorMessage: Text;
        ErrorDetails: Text;
        MetadataUrl: Text;
        ResponseBody: Text;
        HttpStatusCode: DotNet HttpStatusCode;
        ResponseHeaders: DotNet NameValueCollection;
        JsonObject: JsonObject;
        JsonToken: JsonToken;
    begin
        MetadataUrl := GetGraphFileByIdUrl(DocumentSharing."Item Id");
        // Add Prefer header to get the new downloadurl for the onedrive content while hitting the GetFileContent function
        InitializeGraphWebRequest(MetadataUrl, HttpWebRequestMgt, 'application/json', 'GET');
        HttpWebRequestMgt.AddHeader('Prefer', 'pacToken=N');

        if not HttpWebRequestMgt.SendRequestAndReadTextResponse(ResponseBody, ErrorMessage, ErrorDetails, HttpStatusCode, ResponseHeaders) then begin
            Session.LogMessage('0000IN8', StrSubstNo(SharepointStatusCodeTelemetryMsg, HttpStatusCode), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);

            CheckLicenseError(HttpStatusCode, ErrorDetails);
            Error(SharepointUnexpectedStatusCodeErr, HttpStatusCode);
        end;

        if not JsonObject.ReadFrom(ResponseBody) then
            Error(SharepointInvalidJsonErr, ResponseBody);

        if not JsonObject.Get('@microsoft.graph.downloadUrl', JsonToken) then
            Error(DownloadUrlDoesNotExistErr, ResponseBody);

        FileUrl := JsonToken.AsValue().AsText();
        if FileUrl = '' then
            Session.LogMessage('0000JWY', SharepointUnableToGetDownloadUrlMsg, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
    end;

    [TryFunction]
    local procedure DeleteDriveItem(DocumentSharing: Record "Document Sharing")
    var
        HttpWebRequestMgt: Codeunit "Http Web Request Mgt.";
        ResponseBody: Text;
        ErrorMessage: Text;
        ErrorDetails: Text;
        FileUrl: Text;
        HttpStatusCode: DotNet HttpStatusCode;
        ResponseHeaders: DotNet NameValueCollection;
    begin
        ResolveItemId(DocumentSharing);
        FileUrl := GetGraphFileByIdUrl(DocumentSharing."Item Id");
        InitializeGraphWebRequest(FileUrl, HttpWebRequestMgt, 'application/json', 'DELETE');

        if not HttpWebRequestMgt.SendRequestAndReadTextResponse(ResponseBody, ErrorMessage, ErrorDetails, HttpStatusCode, ResponseHeaders) then begin
            Session.LogMessage('0000J18', StrSubstNo(SharepointStatusCodeTelemetryMsg, HttpStatusCode), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);

            CheckLicenseError(HttpStatusCode, ErrorDetails);
        end;
    end;

    local procedure ResolveItemId(var DocumentSharing: Record "Document Sharing")
    var
        HttpWebRequestMgt: Codeunit "Http Web Request Mgt.";
        JsonObject: JsonObject;
        JsonToken: JsonToken;
        JsonValue: JsonValue;
        ResponseBody: Text;
        ErrorMessage: Text;
        ErrorDetails: Text;
        FileUrl: Text;
        HttpStatusCode: DotNet HttpStatusCode;
        ResponseHeaders: DotNet NameValueCollection;
    begin
        // Item Id has already been retrieved.
        if DocumentSharing."Item Id" <> '' then
            exit;

        FileUrl := GetGraphItemIdUrl(DocumentSharing);
        InitializeGraphWebRequest(FileUrl, HttpWebRequestMgt, 'application/json', 'GET');

        if not HttpWebRequestMgt.SendRequestAndReadTextResponse(ResponseBody, ErrorMessage, ErrorDetails, HttpStatusCode, ResponseHeaders) then begin
            Session.LogMessage('0000J19', StrSubstNo(SharepointStatusCodeTelemetryMsg, HttpStatusCode), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);

            CheckLicenseError(HttpStatusCode, ErrorDetails);
            Error(SharepointUnexpectedStatusCodeErr, HttpStatusCode);
        end;

        if not JsonObject.ReadFrom(ResponseBody) then
            Error(SharepointInvalidJsonErr, ResponseBody);

        if not JsonObject.Get('id', JsonToken) then
            Error(ValueDoesNotExistErr, ResponseBody);

        JsonValue := JsonToken.AsValue();
        DocumentSharing."Item Id" := CopyStr(JsonValue.AsText(), 1, MaxStrLen(DocumentSharing."Item Id"));

        if DocumentSharing."Item Id" = '' then
            Error(SharepointUnexpectedErr);

        Session.LogMessage('0000JB5', StrSubstNo(SharepointItemIdMsg, DocumentSharing."Item Id"), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
    end;

    local procedure InitializeGraphWebRequest(FileUrl: Text; var HttpWebRequestMgt: Codeunit "Http Web Request Mgt."; ReturnType: Text; RequestMethod: Text)
    var
        AzureADMgt: Codeunit "Azure AD Mgt.";
        Token: SecretText;
    begin
        Token := AzureADMgt.GetAccessTokenAsSecretText(GetGraphDomain(), AzureADMgt.GetO365ResourceName(), false);
        InitializeWebRequest(FileUrl, RequestMethod, ReturnType, HttpWebRequestMgt, Token);
    end;

    local procedure InitializeOneDriveWebRequest(FileUrl: Text; var HttpWebRequestMgt: Codeunit "Http Web Request Mgt."; ReturnType: Text)
    var
        AzureADMgt: Codeunit "Azure AD Mgt.";
        Token: SecretText;
    begin
        Token := AzureADMgt.GetOnBehalfAccessTokenAsSecretText(GetResourceUrl(FileUrl));
        InitializeWebRequest(FileUrl, 'GET', ReturnType, HttpWebRequestMgt, Token);
    end;

    local procedure InitializeWebRequest(
            Url: Text;
            Method: Text;
            ReturnType: Text;
            var HttpWebRequestMgt: Codeunit "Http Web Request Mgt.";
            Token: SecretText)
    begin
        if Token.IsEmpty() then begin
            Session.LogMessage('0000FMK', EmptyTokenTelemetryMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
            LicenseError();
        end;
        HttpWebRequestMgt.Initialize(Url);
        HttpWebRequestMgt.DisableUI();
        HttpWebRequestMgt.SetMethod(Method);
        HttpWebRequestMgt.SetReturnType(ReturnType);
        HttpWebRequestMgt.AddHeader('Authorization', SecretStrSubstNo('Bearer %1', Token));
    end;

    local procedure LicenseError()
    var
        EnvironmentInformation: Codeunit "Environment Information";
    begin
        if EnvironmentInformation.IsSaaSInfrastructure() then
            Error(MissingOneDriveLicenseSaasErr)
        else
            Error(MissingOneDriveLicenseOnPremErr, ProductName.Short());
    end;

    local procedure GetDocumentServiceFolder(): Text
    var
        DocumentServiceRec: Record "Document Service";
    begin
        if IsConfiguredLegacy() then begin
            DocumentServiceRec.FindFirst();
            Session.LogMessage('0000FMM', UsingCustomDocumentServiceTelemetryMsg, Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
            exit(DocumentServiceRec.Folder);
        end;

        Session.LogMessage('0000FMN', UsingDefaultDocumentServiceTelemetryMsg, Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
        exit(GetDefaultFolderName());
    end;

    local procedure GetGraphDriveUrl(): Text
    var
        Domain: Text;
    begin
        Domain := GetGraphDomain();

        Domain := DelChr(Domain, '>', '/');
        Domain += '/v1.0/me/drive';

        exit(Domain);
    end;

    local procedure GetGraphDriveRootUrl(): Text
    begin
        exit(GetGraphDriveUrl() + '/root');
    end;

    local procedure GetGraphDriveItemUrl(): Text
    begin
        exit(GetGraphDriveUrl() + '/items');
    end;

    local procedure GetGraphItemIdUrl(DocumentSharing: Record "Document Sharing"): Text
    var
        FileUrl: Text;
    begin
        // The DocumentUri contains /Document at the start and needs to be removed for getting the file-path
        FileUrl := ':/' + DocumentSharing.DocumentUri.Remove(1, StrLen(DocumentSharing.DocumentRootUri + '/Document'));
        exit(GetGraphDriveRootUrl() + FileUrl);
    end;

    local procedure GetGraphFileByIdUrl(ItemId: Text): Text
    begin
        exit(GetGraphDriveItemUrl() + '/' + ItemId);
    end;

    local procedure MakeChildrenPathUrl(RootDriveUrl: Text; FolderString: Text): Text
    var
        Uri: Codeunit Uri;
        Folders: List of [Text];
        Folder: Text;
    begin
        // From: https://go.microsoft.com/fwlink/?linkid=2206172
        // A driveItem can be addressed by either a unique identifier or where that item exists in the drive's hierarchy (i.e. user path).
        // Within an API request, a colon can be used to shift between API path space and user path space.
        // Ensure user data within the URL follows the addressing and path encoding requirements.
        // Examples:
        // /drive/root:/path/to/file                          -> Access a driveItem by path under the root.

        Folders := FolderString.Split('/');
        RootDriveUrl := DelChr(RootDriveUrl, '>', '/') + ':';

        foreach Folder in Folders do
            if Folder <> '' then
                RootDriveUrl := RootDriveUrl + '/' + Uri.EscapeDataString(Folder);

        exit(RootDriveUrl);
    end;

    local procedure GetGraphDomain(): Text
    var
        UrlHelper: Codeunit "Url Helper";
        Domain: Text;
    begin
        Domain := UrlHelper.GetGraphUrl();
        if Domain = '' then
            Domain := GraphApiUrlOnPremTxt; // This fallback is needed for OnPremise

        exit(Domain)
    end;

    local procedure SetDocumentService()
    var
        RequestedServiceType: Text;
    begin
        // Sets the Document Service for the current Service Type, reusing an existing service if possible.

        RequestedServiceType := GetServiceType();

        if RequestedServiceType = '' then
            RequestedServiceType := 'SHAREPOINTONLINE';

        if LastServiceType <> RequestedServiceType then begin
            DocumentService := DocumentServiceFactory.CreateService(RequestedServiceType);
            LastServiceType := RequestedServiceType;
        end;
    end;

    local procedure CheckError()
    begin
        // Checks whether the Document Service received an error and displays that error to the user.

        if not IsNull(DocumentService.LastError) and (DocumentService.LastError.Message <> '') then
            Error(DocumentService.LastError.Message);
    end;

    [TryFunction]
    local procedure TrySaveStreamFromRec(InStream: InStream; TargetName: Text; ConflictBehavior: Enum "Doc. Sharing Conflict Behavior"; var DocumentServiceRec: Record "Document Service"; var DocumentUri: Text; var UploadedFileName: Text)
    var
        LocalDocumentService: Record "Document Service";
        DotNetConflictBehavior: DotNet ConflictBehavior;
        DotNetUploadedDocument: DotNet UploadedDocument;
    begin
        Session.LogMessage('0000FTC', StrSubstNo(TrySaveStreamFromRecTelemetryMsg, Format(ConflictBehavior)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
        Clear(DocumentUri);

        // Saves a stream to the Document Service using the configured location specified in Dynamics NAV.
        SetDocumentService();

        if DocumentServiceRec."Service ID" <> '' then
            SetProperties(true, DocumentServiceRec)
        else begin
            if not LocalDocumentService.FindFirst() then
                Error(NoConfigErr);

            if LocalDocumentService."Authentication Type" = LocalDocumentService."Authentication Type"::OAuth2 then
                SetResourceLocation(LocalDocumentService);

            SetProperties(true, LocalDocumentService);
        end;

        DotNetConflictBehavior := ConflictBehavior.AsInteger();
        DotNetUploadedDocument := DocumentService.Save(InStream, TargetName, DotNetConflictBehavior);
        CheckError();

        UploadedFileName := TargetName;
        if not IsNull(DotNetUploadedDocument) then begin
            if not IsNull(DotNetUploadedDocument.Uri) then
                DocumentUri := DotNetUploadedDocument.Uri.AbsoluteUri;
            if not IsNull(DotNetUploadedDocument.FileName) then
                UploadedFileName := DotNetUploadedDocument.FileName;
        end;
    end;

    local procedure SaveStream(InStream: InStream; TargetName: Text; ConflictBehavior: Enum "Doc. Sharing Conflict Behavior") DocumentUri: Text
    var
        DocumentServiceRec: Record "Document Service";
        TempDocumentServiceRec: Record "Document Service" temporary;
        UploadedFileName: Text;
        DocumentSharingSource: Enum "Document Sharing Source";
    begin
        if IsOneDriveEnabled() then begin
            InitTempDocumentServiceRecord(TempDocumentServiceRec, DocumentSharingSource::App);
            TrySaveStreamFromRec(InStream, TargetName, ConflictBehavior, TempDocumentServiceRec, DocumentUri, UploadedFileName);
        end else
            TrySaveStreamFromRec(InStream, TargetName, ConflictBehavior, DocumentServiceRec, DocumentUri, UploadedFileName);
    end;

    local procedure GetAccessToken(Location: Text; var AccessToken: SecretText; GetTokenFromCache: Boolean)
    var
        DocumentServiceScenario: Record "Document Service Scenario";
        AzureAdMgt: Codeunit "Azure AD Mgt.";
        EnvironmentInformation: Codeunit "Environment Information";
        FeatureTelemetry: Codeunit "Feature Telemetry";
        OAuth2: Codeunit OAuth2;
        PromptInteraction: Enum "Prompt Interaction";
        Scopes: List of [Text];
        ClientId: Text;
        ClientSecret: SecretText;
        Resource: Text;
        RedirectURL: Text;
        AuthError: Text;
    begin
        Session.LogMessage('0000FSI', StrSubstNo(TokenRequestTxt, Format(GetTokenFromCache)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
        FeatureTelemetry.LogUsage('0000HUM', OneDriveFeatureNameTelemetryTxt, TokenRequestEventTelemetryTxt); // To Account for Platform usage scenarios

        Clear(AccessToken);
        Resource := GetResourceUrl(Location);
        if EnvironmentInformation.IsSaaSInfrastructure() then
            AccessToken := AzureAdMgt.GetOnBehalfAccessTokenAsSecretText(Resource)
        else
            if not DocumentServiceScenario.IsEmpty() then
                AccessToken := AzureAdMgt.GetAccessTokenAsSecretText(Resource, Resource, true);

        if not AccessToken.IsEmpty() then begin
            Session.LogMessage('0000GPT', AcquiredTokenOnBehalfFlowTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
            exit;
        end;

        if (not DocumentServiceScenario.IsEmpty()) then
            Error(AccessTokenErrMsg); // oauth fields are no longer used

        GetScopes(Location, Scopes);
        ClientId := GetClientId();
        ClientSecret := GetClientSecret();
        RedirectURL := GetRedirectURL();

        if GetTokenFromCache then
            OAuth2.AcquireAuthorizationCodeTokenFromCache(ClientId, ClientSecret, RedirectURL, OAuthAuthorityUrlLbl, Scopes, AccessToken);
        if not AccessToken.IsEmpty() then
            exit;

        Session.LogMessage('0000DB7', AccessTokenAcquiredFromCacheErr, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
        OAuth2.AcquireTokenByAuthorizationCode(
                    ClientId,
                    ClientSecret,
                    OAuthAuthorityUrlLbl,
                    RedirectURL,
                    Scopes,
                    PromptInteraction::"Select Account",
                    AccessToken,
                    AuthError
                );

        if AccessToken.IsEmpty() then begin
            Session.LogMessage('0000DB8', StrSubstNo(AuthTokenOrCodeNotReceivedErr, AuthError), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
            Error(AccessTokenErrMsg);
        end;
    end;

    local procedure GetClientId(): Text
    var
        DocumentServiceRec: Record "Document Service";
        AzureKeyVault: Codeunit "Azure Key Vault";
        EnvironmentInformation: Codeunit "Environment Information";
        [NonDebuggable]
        ClientId: Text;
    begin
        if EnvironmentInformation.IsSaaSInfrastructure() then
            if not AzureKeyVault.GetAzureKeyVaultSecret(SharePointClientIdAKVSecretNameLbl, ClientId) then
                Session.LogMessage('0000DB9', MissingClientIdTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt)
            else begin
                Session.LogMessage('0000DBA', InitializedClientIdTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
                exit(ClientId);
            end;

        if DocumentServiceRec.FindFirst() then begin
            ClientId := DocumentServiceRec."Client Id";
            OnGetSharePointClientId(ClientId);
            if ClientId <> '' then begin
                Session.LogMessage('0000DBB', InitializedClientIdTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
                exit(ClientId);
            end;
        end;

        Error(MissingClientIdOrSecretErr);
    end;

    local procedure GetClientSecret(): SecretText
    var
        DocumentServiceRec: Record "Document Service";
        AzureKeyVault: Codeunit "Azure Key Vault";
        EnvironmentInformation: Codeunit "Environment Information";
        ClientSecret: SecretText;
        [NonDebuggable]
        ClientSecretFromEvent: Text;
    begin
        if EnvironmentInformation.IsSaaSInfrastructure() then
            if not AzureKeyVault.GetAzureKeyVaultSecret(SharePointClientSecretAKVSecretNameLbl, ClientSecret) then
                Session.LogMessage('0000DBC', MissingClientSecretTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt)
            else begin
                Session.LogMessage('0000DBD', InitializedClientSecretTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
                exit(ClientSecret);
            end;

        if not DocumentServiceRec.IsEmpty() then begin
            ClientSecret := GetClientSecretFromIsolatedStorage();
            if not ClientSecret.IsEmpty() then begin
                Session.LogMessage('0000DBE', InitializedClientSecretTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
                exit(ClientSecret);
            end;
        end;

        OnGetSharePointClientSecret(ClientSecretFromEvent);
        ClientSecret := ClientSecretFromEvent;
        if not ClientSecret.IsEmpty() then begin
            Session.LogMessage('0000DBF', InitializedClientSecretTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
            exit(ClientSecret);
        end;

        Error(MissingClientIdOrSecretErr);
    end;

    [Scope('OnPrem')]
    local procedure GetRedirectURL(): Text
    var
        DocumentServiceRec: Record "Document Service";
        EnvironmentInformation: Codeunit "Environment Information";
        RedirectURL: Text;
    begin
        if EnvironmentInformation.IsSaaSInfrastructure() then
            exit(RedirectURL);

        if DocumentServiceRec.FindFirst() then
            RedirectURL := DocumentServiceRec."Redirect URL";

        if RedirectURL = '' then
            OnGetSharePointRedirectURL(RedirectURL);

        exit(RedirectURL);
    end;

    local procedure GetScopes(Location: Text; var Scopes: List of [Text])
    begin
        Location := GetResourceUrl(Location);
        Scopes.Add(Location + '/AllSites.FullControl');
        Scopes.Add(Location + '/User.ReadWrite.All');
    end;

    local procedure GetResourceUrl(Location: Text): Text
    var
        Uri: Codeunit Uri;
    begin
        Uri.Init(Location);
        exit(StrSubstNo(HttpsDomainTxt, Uri.GetHost()));
    end;

    [Scope('OnPrem')]
    internal procedure SetClientSecret(ClientSecret: SecretText)
    var
        DocumentServiceRec: Record "Document Service";
        IsolatedStorageManagement: Codeunit "Isolated Storage Management";
    begin
        if not DocumentServiceRec.FindFirst() then
            Error(NoConfigErr);

        if ClientSecret.IsEmpty() then
            if not IsNullGuid(DocumentServiceRec."Client Secret Key") then begin
                IsolatedStorageManagement.Delete(DocumentServiceRec."Client Secret Key", DATASCOPE::Company);
                exit;
            end;

        if IsNullGuid(DocumentServiceRec."Client Secret Key") then begin
            DocumentServiceRec."Client Secret Key" := CreateGuid();
            DocumentServiceRec.Modify();
        end;

        IsolatedStorageManagement.Set(DocumentServiceRec."Client Secret Key", ClientSecret, DATASCOPE::Company);
    end;

    [Scope('OnPrem')]
    local procedure GetClientSecretFromIsolatedStorage(): SecretText
    var
        DocumentServiceRec: Record "Document Service";
        IsolatedStorageManagement: Codeunit "Isolated Storage Management";
        ClientSecret: SecretText;
    begin
        if not DocumentServiceRec.FindFirst() then
            Error(NoConfigErr);

        if IsNullGuid(DocumentServiceRec."Client Secret Key") or
           not IsolatedStorage.Contains(DocumentServiceRec."Client Secret Key", DATASCOPE::Company)
        then begin
            Session.LogMessage('0000DBG', SharePointIsoStorageSecretNotConfiguredLbl, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', SharePointTelemetryCategoryTxt);
            Error(SharePointIsoStorageSecretNotConfiguredErr);
        end;

        IsolatedStorageManagement.Get(DocumentServiceRec."Client Secret Key", DATASCOPE::Company, ClientSecret);
        exit(ClientSecret);
    end;

    [Scope('OnPrem')]
    [TryFunction]
    internal procedure TryGetClientSecretFromIsolatedStorage(var ClientSecret: SecretText)
    begin
        ClientSecret := GetClientSecretFromIsolatedStorage();
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"System Action Triggers", 'OnOpenInExcel', '', false, false)]
    [NonDebuggable]
    local procedure OnTryAcquireAccessTokenOnOpenInExcel(Location: Text)
    var
        DocumentServiceScenario: Record "Document Service Scenario";
        DocumentServiceRec: Record "Document Service";
        ResultJson: JsonObject;
        Token: SecretText;
        Result: Text;
    begin
        if DocumentServiceScenario.IsEmpty() then begin
            GetAccessToken(Location, Token, true);
            Session.SetDocumentServiceToken(Token.Unwrap());
        end;

        if Location = '' then
            Location := GetDefaultLocation();

        GetAccessToken(Location, Token, true);

        ResultJson.Add(DocumentServiceRec.FieldName(Location), Location);
        ResultJson.Add(DocumentServiceRec.FieldName(Folder), GetDefaultFolderName() + '/' + GetSafeCompanyName());
        ResultJson.Add('Token', Token.Unwrap());

        ResultJson.WriteTo(Result);
        Session.SetDocumentServiceToken(Result);
    end;

    procedure OpenInOneDrive(FileName: Text; FileExtension: Text; InStream: InStream)
    var
        DocumentSharing: Codeunit "Document Sharing";
    begin
        DocumentSharing.Share(FileName, FileExtension, InStream, Enum::"Document Sharing Intent"::Open);
    end;

    procedure EditInOneDrive(FileName: Text; FileExtension: Text; var TempBlob: Codeunit "Temp Blob"): Boolean
    begin
        exit(EditInOneDrive(FileName, FileExtension, Enum::"Doc. Sharing Conflict Behavior"::Ask, TempBlob));
    end;

    procedure EditInOneDrive(FileName: Text; FileExtension: Text; DocSharingConflictBehavior: Enum "Doc. Sharing Conflict Behavior"; var TempBlob: Codeunit "Temp Blob"): Boolean
    var
        TempDocumentSharing: Record "Document Sharing" temporary;
        DocumentSharing: Codeunit "Document Sharing";
        CryptographyManagement: Codeunit "Cryptography Management";
        HashAlgorithmType: Option MD5,SHA1,SHA256,SHA384,SHA512;
        Hash: Text;
        InStream: InStream;
        OutStream: OutStream;
    begin
        TempDocumentSharing.Name := CopyStr(FileName, 1, MaxStrLen(TempDocumentSharing.Name));
        TempDocumentSharing.Extension := CopyStr(FileExtension, 1, MaxStrLen(TempDocumentSharing.Extension));
        TempDocumentSharing."Document Sharing Intent" := Enum::"Document Sharing Intent"::Edit;
        TempDocumentSharing."Conflict Behavior" := DocSharingConflictBehavior;

        TempBlob.CreateInStream(InStream);
        TempDocumentSharing.Data.CreateOutStream(OutStream);
        CopyStream(OutStream, Instream);
        TempBlob.CreateInStream(InStream);

        Hash := CryptographyManagement.GenerateHash(InStream, HashAlgorithmType::SHA1);

        TempDocumentSharing.Insert();
        DocumentSharing.Share(TempDocumentSharing);

        TempBlob.CreateOutStream(OutStream);
        TempDocumentSharing.Data.CreateInStream(InStream);
        CopyStream(OutStream, InStream);
        TempBlob.CreateInStream(InStream);

        exit(Hash <> CryptographyManagement.GenerateHash(InStream, HashAlgorithmType::SHA1));
    end;

    procedure OpenInOneDriveFromMedia(FileName: Text; FileExtension: Text; MediaId: Guid)
    var
        DocumentSharingIntent: Enum "Document Sharing Intent";
    begin
        InvokeDocumentSharingFlowFromMedia(FileName, FileExtension, MediaId, DocumentSharingintent::Open);
    end;

    procedure EditInOneDriveFromMedia(FileName: Text; FileExtension: Text; MediaId: Guid): Boolean
    var
        DocumentSharingIntent: Enum "Document Sharing Intent";
    begin
        exit(InvokeDocumentSharingFlowFromMedia(FileName, FileExtension, MediaId, DocumentSharingintent::Edit));
    end;

    procedure ShareWithOneDrive(FileName: Text; FileExtension: Text; InStream: InStream)
    var
        DocumentSharing: Codeunit "Document Sharing";
    begin
        DocumentSharing.Share(FileName, FileExtension, InStream, Enum::"Document Sharing Intent"::Share);
    end;

    procedure ShareWithOneDriveFromMedia(FileName: Text; FileExtension: Text; MediaId: Guid)
    var
        DocumentSharingIntent: Enum "Document Sharing Intent";
    begin
        InvokeDocumentSharingFlowFromMedia(FileName, FileExtension, MediaId, DocumentSharingintent::Share);
    end;

    local procedure InvokeDocumentSharingFlowFromMedia(FileName: Text; FileExtension: Text; MediaId: Guid; DocumentSharingIntent: Enum "Document Sharing Intent"): Boolean
    var
        TempDocumentSharing: Record "Document Sharing" temporary;
        TenantMedia: Record "Tenant Media";
        CryptographyManagement: Codeunit "Cryptography Management";
        HashAlgorithmType: Option MD5,SHA1,SHA256,SHA384,SHA512;
        InStream: InStream;
        OutStream: OutStream;
        Hash: Text;
    begin
        SetFileNameAndExtension(TempDocumentSharing, FileName, FileExtension);

        TenantMedia.Get(MediaId);
        TenantMedia.CalcFields(Content);
        TempDocumentSharing.Data := TenantMedia.Content;
        TempDocumentSharing."Document Sharing Intent" := DocumentSharingIntent;
        TempDocumentSharing.Insert();
        TempDocumentSharing.Data.CreateInStream(InStream);
        Hash := CryptographyManagement.GenerateHash(InStream, HashAlgorithmType::SHA1);
        Codeunit.Run(Codeunit::"Document Sharing", TempDocumentSharing);

        if (DocumentSharingIntent = Enum::"Document Sharing Intent"::Edit) and
            (Hash <> CryptographyManagement.GenerateHash(InStream, HashAlgorithmType::SHA1)) then begin
            TempDocumentSharing.Data.CreateInStream(InStream);
            TenantMedia.Content.CreateOutStream(OutStream);

            CopyStream(OutStream, InStream);
            TenantMedia.Modify();
            exit(true);
        end;
    end;

    [NonDebuggable]
    [Scope('OnPrem')]
    procedure TestLocationResolves(Location: Text[250]; AccessToken: SecretText): Boolean
    var
        DocumentServiceRec: Record "Document Service";
    begin
        SetDocumentService();

        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName(Location), Location);
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName(Folder), GetDefaultFolderName());
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName("Authentication Type"), DocumentServiceRec."Authentication Type"::OAuth2);
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName("User Name"), '');
        DocumentService.Properties.SetProperty(DocumentServiceRec.FieldName("Document Repository"), '');
        DocumentService.Properties.SetProperty('Token', AccessToken.Unwrap());

        Location := DocumentService.GetLocation();
        exit(Location <> '');
    end;

    local procedure InitTempDocumentServiceRecord(var TempDocumentServiceRec: Record "Document Service" temporary; Source: Enum "Document Sharing Source")
    var
        DocumentServiceRec: Record "Document Service";
        DocumentServiceScenario: Record "Document Service Scenario";
        EnvironmentInformation: Codeunit "Environment Information";
        ScenarioEnabled: Boolean;
    begin
        if Source = Source::App then
            ScenarioEnabled := IsOneDriveEnabled()
        else
            ScenarioEnabled := IsOneDriveEnabledForSystem();

        if (ScenarioEnabled or (EnvironmentInformation.IsSaaSInfrastructure() and DocumentServiceScenario.IsEmpty())) then begin // has new config and app is enabled, or no new config + SaaS
            Session.LogMessage('0000FK0', DocumentServiceDefaultingLbl, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', DocumentServiceCategoryLbl);
            InitializeDefaultService(TempDocumentServiceRec, 'SHARE');
        end else begin
            if not DocumentServiceScenario.IsEmpty() or not DocumentServiceRec.FindFirst() then
                Error(CantFindMySiteErr); // either the new setup has disabled the flow, or there is no setup

            TempDocumentServiceRec.TransferFields(DocumentServiceRec);
        end;

        Clear(TempDocumentServiceRec."User Name"); // Clearing the user name will ensure the NST uploads to the personal store (e.g. OneDrive)
        TempDocumentServiceRec.Folder := CopyStr(TempDocumentServiceRec.Folder + '/' + GetSafeCompanyName(), 1, MaxStrLen(TempDocumentServiceRec.Folder));

        if not SetResourceLocation(TempDocumentServiceRec) then
            Error(CantFindMySiteTryLoginErr);
    end;

    local procedure SetFileNameAndExtension(var TempDocumentSharing: Record "Document Sharing" temporary; FileName: Text; FileExtension: Text)
    begin
        TempDocumentSharing.Name := CopyStr(FileName, 1, MaxStrLen(TempDocumentSharing.Name) - StrLen(FileExtension)) + FileExtension;
        TempDocumentSharing.Extension := CopyStr(FileExtension, 1, MaxStrLen(TempDocumentSharing.Extension));
    end;

    internal procedure GetTelemetryCategory(): Text
    begin
        exit(DocumentServiceCategoryLbl);
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Document Sharing", 'OnCanUploadDocument', '', false, false)]
    local procedure OnCanUploadDocument(var CanUpload: Boolean)
    var
        PrivacyNotice: Codeunit "Privacy Notice";
        PrivacyNoticeRegistrations: Codeunit "Privacy Notice Registrations";
    begin
        if CanUpload then
            exit;

        if PrivacyNotice.IsApprovalStateDisagreed(PrivacyNoticeRegistrations.GetOneDrivePrivacyNoticeId()) then
            exit;

        CanUpload := IsOneDriveEnabledOrUsingLegacySetup();
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Document Sharing", 'OnCanUploadSystemDocument', '', false, false)]
    local procedure OnCanUploadSystemDocument(var CanUpload: Boolean)
    var
        PrivacyNotice: Codeunit "Privacy Notice";
        PrivacyNoticeRegistrations: Codeunit "Privacy Notice Registrations";
    begin
        if CanUpload then
            exit;

        if PrivacyNotice.IsApprovalStateDisagreed(PrivacyNoticeRegistrations.GetOneDrivePrivacyNoticeId()) then
            exit;

        CanUpload := IsOneDriveEnabledForSystem();
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Document Sharing", 'OnUploadDocument', '', false, false)]
    local procedure OnUploadDocument(var DocumentSharing: Record "Document Sharing" temporary; var Handled: Boolean)
    var
        TempDocumentServiceRec: Record "Document Service" temporary;
        PrivacyNotice: Codeunit "Privacy Notice";
        FileManagement: Codeunit "File Management";
        PrivacyNoticeRegistrations: Codeunit "Privacy Notice Registrations";
        DocumentUri: Text;
        UploadedFileName: Text;
        InStr: InStream;
        OutStr: OutStream;
    begin
        if Handled then
            exit;

        Session.LogMessage('0000FJZ', DocumentSharingStartLbl, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', DocumentServiceCategoryLbl);

        if DocumentSharing.Source = DocumentSharing.Source::App then
            if (not IsOneDriveEnabledOrUsingLegacySetup()) then
                exit;

        if DocumentSharing.Source = DocumentSharing.Source::System then
            if (not IsOneDriveEnabledForSystem()) then
                exit;

        if not PrivacyNotice.ConfirmPrivacyNoticeApproval(PrivacyNoticeRegistrations.GetOneDrivePrivacyNoticeId()) then
            exit;

        DocumentSharing.Name := CopyStr(FileManagement.GetSafeFileName(DocumentSharing.Name), 1, MaxStrLen(DocumentSharing.Name));
        if DocumentSharing.Name = '' then
            Error(DocumentSharingNoNameErr);

        if DocumentSharing.Extension = '' then
            Error(DocumentSharingNoExtErr);

        InitTempDocumentServiceRecord(TempDocumentServiceRec, DocumentSharing.Source);

        DocumentSharing.CalcFields(DocumentSharing.Data);
        DocumentSharing.Data.CreateInStream(InStr);

        if not GuiAllowed() then
            TrySaveStreamFromRec(InStr, DocumentSharing.Name, Enum::"Doc. Sharing Conflict Behavior"::Fail, TempDocumentServiceRec, DocumentUri, UploadedFileName)
        else
            if not TrySaveStreamFromRec(InStr, DocumentSharing.Name, Enum::"Doc. Sharing Conflict Behavior"::Fail, TempDocumentServiceRec, DocumentUri, UploadedFileName)
                or (DocumentUri = '')
            then
                // Use given behavior if not behavior "Ask"
                if DocumentSharing."Conflict Behavior" <> Enum::"Doc. Sharing Conflict Behavior"::Ask then
                    TrySaveStreamFromRec(InStr, DocumentSharing.Name, DocumentSharing."Conflict Behavior", TempDocumentServiceRec, DocumentUri, UploadedFileName)
                else
                    case StrMenu(SharePointFileExistsOptionsTxt, 0, StrSubstNo(SharePointFileExistsInstructionsTxt, DocumentSharing.Name, ProductName.Short())) of
                        0: // Cancel
                            begin
                                Handled := false;
                                exit;
                            end;
                        1: // Reuse
                            TrySaveStreamFromRec(InStr, DocumentSharing.Name, Enum::"Doc. Sharing Conflict Behavior"::Reuse, TempDocumentServiceRec, DocumentUri, UploadedFileName);
                        2: // Replace
                            TrySaveStreamFromRec(InStr, DocumentSharing.Name, Enum::"Doc. Sharing Conflict Behavior"::Replace, TempDocumentServiceRec, DocumentUri, UploadedFileName);
                        3: // Keep both
                            TrySaveStreamFromRec(InStr, DocumentSharing.Name, Enum::"Doc. Sharing Conflict Behavior"::Rename, TempDocumentServiceRec, DocumentUri, UploadedFileName);
                    end;

        DocumentSharing.Name := CopyStr(UploadedFileName, 1, MaxStrLen(DocumentSharing.Name));
        DocumentSharing.DocumentUri := CopyStr(DocumentUri, 1, MaxStrLen(DocumentSharing.DocumentUri));

        DocumentSharing.Token.CreateOutStream(OutStr);
        OutStr.WriteText(DocumentService.Properties.GetProperty('Token'));

        DocumentSharing.DocumentPreviewUri := CopyStr(
            DocumentService.GenerateViewableDocumentAddress(DocumentSharing.DocumentUri),
            1,
            MaxStrLen(DocumentSharing.DocumentPreviewUri));

        if DocumentService.Properties.IsPropertySet('DocumentRootLocation') then
            DocumentSharing.DocumentRootUri := DocumentService.Properties.GetProperty('DocumentRootLocation');

        DocumentSharing.Modify();

        Handled := DocumentSharing.DocumentUri <> '';
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Document Sharing", 'OnGetFileContents', '', false, false)]
    local procedure OnGetFileContents(var DocumentSharing: Record "Document Sharing" temporary; var Handled: Boolean)
    begin
        if Handled then
            exit;
        Handled := GetFileContent(DocumentSharing);
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Document Sharing", 'OnDeleteDocument', '', false, false)]
    local procedure OnDeleteDocument(var DocumentSharing: Record "Document Sharing" temporary; var Handled: Boolean)
    begin
        if Handled then
            exit;
        Handled := DeleteDriveItem(DocumentSharing);
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeIsConfigured(var DocumentServiceRec: Record "Document Service")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeSetProperties(var DocumentServiceRec: Record "Document Service")
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnGetSharePointClientId(var ClientId: Text)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnGetSharePointClientSecret(var ClientSecret: Text)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnGetSharePointRedirectURL(var RedirectURL: Text)
    begin
    end;
}