• RE: Platform call to upload document to Document Tray

    Hey all,

    I'd still appreciate any insight folks can give on my OP, but for now I have written a C# routine that is callable from FoxPro, and I can upload a document to a Document Tray successfully.

    I think the issue for non-.NET tools is that the "Id" for document trays is very strange (discovered it from the .NET side). File cabinets have an Id equivalent to their Guids, but a document tray puts a "b_" in front of their Guid, resulting in an Id that does not meet Guid standards. If I try to manually add the "b_" in front and upload a document (from FoxPro), I get an error about the Guid being malformed.

    Things work in .NET because there the FileCabinet type is used for these method calls -- can't do that with regular HTTP POSTs.

    And my other questions remain. For example, how do I clip a document from a document tray to a document already stored in a file cabinet in .NET? I cannot find any examples except the "merge" and "transfer" examples. As I said before, I don't want to do a file cabinet-based merge because I don't want to have to store documents in a file cabinet just to clip them and delete them (plus it creates a whole new document instead of just clipping onto an existing one). "Transfer" examples do not appear to allow clipping, just movement.

    I'd like to be able to emulate the "Clip" functionality from the right-click menu in the web client, just via the Platform SDK. Can anyone help? How are other people automating these clip processes?

    Thanks,
    Joe Kaufman
  • Platform call to upload document to Document Tray

    Hey all,

    I am trying to upload a document to a document tray via the Platform SDK. We have been uploading documents to file cabinets for a couple years now, no problems. That works in both C# and Visual FoxPro.

    According to the examples here:

    https://developer.docuware.com/dotNet_CodeExamples/57da87ed-111c-4f78-96e9-75d3b9462ce9.html

    namely, the UploadSingleFileToBasket() method, a document tray is treated like a file cabinet, and an upload works the same way. You just don't bother indexing the uploaded document because it hasn't been stored yet.

    So, I figured out how to get the GUID for the document tray itself (not the backing file cabinet), and I tried using that guid to upload a file. The call succeeds (no HTTP error) but the resulting response has the following process error in it:

       FileCabinet with id <guid> is not assigned to the user

    The document is not in the document tray, either. The authenticated user has rights to the document tray I am using both explicitly and via Role. Yet it comes back saying it "is not assigned to the user". What does this mean?

    The bottom line is that I want to upload a file to a document tray instead of a file cabinet. Why? Because I want to then clip the document to an existing document already stored in a file cabinet (like the way it can be done manually via the right-click "Clip" option in the web client. I don't want to do a full "merge" operation where two documents in a file cabinet are merged and then deleted. I want to simply clip a new document to an existing document. If there is a better way of doing that, I'm all ears. We have to have this working because we are about to receive thousands of documents from an external scanning vendor, and I need to upload the first volume of a document to a file cabinet and then clip additional volumes to the original. This needs to be automated, obviously.

    Finally, I am wondering how people find the correct guid for a document tray? We name all of our document trays with a unique name, but that is not a requirement in DocuWare. You could have 800 trays, all named "Inbox", if you want. I just created three trays, all with the name "joek", all backed by the same file cabinet, and all with the exact same user permissions. There is literally no way to tell them apart, aside from each having a unique GUID. How would one automate uploads to various document trays (i.e. some sort of automated, centralized document distribution point) when there is no way to distinguish them uniquely? I guess we just have to be careful to name them distinctly?

    Thanks for any help anyone can offer.

    Thanks,
    Joe Kaufman
  • RE: IE vs. Chrome Performance After Upgrade to 7.1

    Teresa,

    You just know I am going to start with the obligatory, "Why on Earth are they using IE?" It is old and broken and insecure... I assume there is some Silverlight requirement in play such that the client cannot standardize on Chrome or Firefox?

    Speaking of Firefox, how does DocuWare perform on Firefox and Chrome? Edge? Edge would be the heir apparent to IE, though I think it was pretty much created new from the ground up. It would be interesting to see how they all perform relative to each other.

    Sorry I cannot be of more help -- we are on 6.11 and all browsers seem to work OK. We have to use IE for things like setting up Import Configs, since that is still in Silverlight in version 6.11.

    Thanks,
    Joe Kaufman
  • RE: .NET API - Execute a Query using Dialog expression

    Judging by the literals used in URL integration queries (and just tested using a dialog query), it looks like dates need to be of the format:

    "YYYY-MM-DD", as in, "2019-08-01" for August 1, 2019.

    You pass the date value in as a string formatted in that way. For DateTime values, you use:

    "YYYY-MM-DD HH:mm:ss", as in, "2019-08-09 17:32:39".

    Be aware that DateTime fields are always stored with UTC time. That goes for system fields as well as user-defined fields.

    Good luck,
    Joe Kaufman
  • RE: User Long Name

    Seth,

    Additionally, the full name can be found in the "settings" field of that same user record, embedded in the XML.

    Thanks,
    Joe Kaufman
  • RE: Moving Documents to another Cabinet with Indexes

    Simon,

    I'm still on 6.11 (on-premise), but are "Export Workflows" still a thing? Those let you move documents (with indexes) from a source to a destination.

    An Export Workflow is of the type of workflows that are built in without requiring the WorkFlow module nor the AutoIndex module.

    Search this PDF for "Document Transfer". It looks like that is the version 7 way of doing things...

    Thanks,
    Joe Kaufman
  • RE: (Using Platform Services) Can a document be downloaded WITH its stamps included?

    Danny,

    Glad you got things figured out! I am appreciative of your post because I realized using the straight HTTP calls from non-NET environments was not bringing down stamps and annotations. I had to utilize the longer URL to get those to come down, so now that works, too (in Visual FoxPro).

    You state, "STAMPS always come over with the document when downloaded from Platform Services in any setting."... That was not the case for me. The first thing I did when I started researching your question was stamp a document and draw some annotations on it, and neither came down when I pulled the file down (just using a straight stream to file). Only when I changed the target type to PDF and set KeepAnnotations to true did they come down. (This behavior was the same using .NET and Visual FoxPro.) I never had a scenario where just one or the other showed up -- it was both or neither in every scenario I tried. Granted, I stopped testing various combinations of properties once I was able to get stamps and annotations to pull down, but still...

    I think this thread should be helpful to future platform users downloading files!

    Thanks,
    Joe Kaufman



     
  • RE: (Using Platform Services) Can a document be downloaded WITH its stamps included?

    Danny,

    I think I figured it out...

    The comments for the DocuWare FileDownloadBase type helped:
     
    public abstract class FileDownloadBase
    {
        //
        // Summary:
        //     Creates a new instance of this class
        public FileDownloadBase();
    
        //
        // Summary:
        //     Specifies the annotation layers to be included in the output file.
        //
        // Remarks:
        //     This list is used only when KeepAnnotations flag is set to true; if this flag
        //     is set to false, then this list is ignored and no layers are included. In order
        //     to keep all layers, just set KeepAnnotations to true and pass an empty list or
        //     do not specify a list at all.
        [Dolphin]
        [XmlElement("Layers")]
        public List<int> Layers { get; set; }
        //
        // Summary:
        //     If this flag is true then the annotations are rendered in the PDF file, otherwise
        //     the annotations are removed.
        //
        // Remarks:
        //     This flag applies only to the PDF target format.
        [DefaultValue(true)]
        [XmlAttribute]
        public bool KeepAnnotations { get; set; }
    }

    So, the Layers property doesn't really matter, but KeepAnnotations only works for a target of PDF. I was using "Auto". When I switched it to be specifically "PDF", annotations and stamps downloaded with the file!

    This is what my download routine looks like now in C#:
     
    public static string SaveDocumentToFile(this Document document, string fileName = "")
    {
        LastError = "";
        try
        {
            if (document.FileDownloadRelationLink == null)
            {
                document = document.GetDocumentFromSelfRelation();
            }
    
            FileDownloadType targetFileType = FileDownloadType.Auto;
            if (document.ContentType.ToUpper() == "application/pdf".ToUpper())
            {
                // Be specific about PDF otherwise KeepAnnotations will not work as desired.
                targetFileType = FileDownloadType.PDF;
            }
    
            var downloadResponse = document.PostToFileDownloadRelationForStreamAsync(
                new FileDownload()
                {
                    TargetFileType = targetFileType,
                    KeepAnnotations = true
                }).Result;
    
            var contentHeaders = downloadResponse.ContentHeaders;
            string defaultFileName = contentHeaders.ContentDisposition.FileName;
            // If there is a space in the file name, the content header filename will place escaped double-quote marks around the
            // file name. This is a problem for other file-name manipulation routines.
            defaultFileName = defaultFileName.Replace('"', ' ').Trim();
            string defaultExtension = VFP.JUSTEXT(defaultFileName);
            if (fileName.Length == 0)
            {
                // No file name passed in, so we should get a default file name for saving the document.
                fileName = VFP.PUTFILE(defaultFileName, defaultExtension, "Save As");
            }
            // If file name is empty at this point, it indicates user cancellation.
            if (fileName.Length == 0) return "";
    
            using (FileStream file = File.Create(fileName))
            {
                using (Stream stream = downloadResponse.Content)
                {
                    stream.CopyTo(file);
                }
            }
            return fileName;
        }
        catch (Exception ex)
        {
            LastError = ex.Message;
            return "";
        }
    }

    Nevermind the "VFP" static function calls -- those are part of my VFP library that uses VFP function names to do things in native C#. The bottom line is that the file that gets pulled down specifically targets PDF if the content type of the document is in fact PDF, and then KeepAnnotations is honored properly. You don't need to set Layers to anything at all. You can also disregard "LastError" -- that is a static member field I use to set the last error if one occurs.

    Hope this helps!

    Thanks,
    Joe Kaufman

     
  • RE: DWIntegration EX_INVALID_PASS_PHRASE-1529591502

    Romain,

    Yes, your concerns are certainly valid! We are all internal, on-premise, so as long as username and password are casually hidden, that is good enough. We don't have anyone intercepting traffic and UrlDecoding to phish for passwords...

    I think I tried the fully encrypted way and never got it to work either, and not just because I was using FoxPro. I believe I tried in C# and couldn't get it working until I used the DocuWare DLL that provides the underpinnings for URL integration. I don't know how to take advantage of that through PHP unless PHP has a .NET bridge you can take advantage of. Using the .NET assembly DocuWare.WebIntegration.dll lets you create a URL via code such as:
     
    public static DWIntegrationUrl GetIntegrationUrl(IntegrationType integrationType = IntegrationType.Viewer)
    {
        LastError = "";
        DWIntegrationUrl returnUrl = null;
        try
        {
            // Prepare DWIntegration information.
            DWIntegrationInfo integrationInfo = new DWIntegrationInfo(ServiceEntryPoint + "/WebClient", DefaultOrganizationID, false);
            integrationInfo.Scheme = DefaultIntegrationProtocol;
            integrationInfo.OrganizationId = DefaultOrganizationID;
            // Now generate a plain-text URL object with the appropriate integration type.
            returnUrl = new DWIntegrationUrl(integrationInfo, integrationType);
            // Set up default user credentials. *** NOTE *** Password goes first, then username!
            returnUrl.Parameters.UserCredentials = new UserCredentials(DefaultIntegrationPassword, DefaultIntegrationUsername);
            // Set some other reasonable defaults for the integration URL object.
            returnUrl.Parameters.DisplayOneDoc = true;
        }
        catch (Exception ex)
        {
            LastError = ex.Message;
        }
        return returnUrl;
    }

    (LastError is a static member field that I use for error handling/communication.)

    Here are a couple deeper dives into what the DocuWare.WebIntegration assembly/namespace exposes:
     
    namespace DocuWare.WebIntegration
    {
        public class DWIntegrationUrl : DWWebUrl
        {
            public DWIntegrationUrl(DWIntegrationInfo integrationInfo, IntegrationType integrationType);
            public DWIntegrationUrl(DWIntegrationInfo integrationInfo, DWIntegrationUrlParameters urlParams);
    
            public DWIntegrationUrlParameters Parameters { get; set; }
            public DWIntegrationInfo Info { get; set; }
    
            public static DWIntegrationUrl Parse(string url);
            public static DWIntegrationUrl Parse(DWIntegrationInfo integrationInfo, string urlString);
            public static DWIntegrationUrl Parse(DWIntegrationInfo integrationInfo, Dictionary<string, string> parameters);
            public static DWIntegrationInfo ParseIntegrationInfo(string urlString);
        }
    
        public abstract class DWWebUrl
        {
            protected DWWebUrlLoginParameters parameters;
    
            protected DWWebUrl(DWWebLoginInfo integrationInfo, IntegrationType integrationType);
            protected DWWebUrl(DWWebLoginInfo integrationInfo, DWWebUrlLoginParameters urlParams);
    
            public string HttpAddress { get; }
            public string BaseUrl { get; }
            public virtual string Url { get; }
            public DWWebLoginInfo Info { get; set; }
            public DWWebUrlLoginParameters Parameters { get; set; }
            protected virtual string Page { get; }
        }
    
        public class DWIntegrationInfo : DWWebLoginInfo
        {
            public DWIntegrationInfo(string httpAddress, int orgID, bool useWindowsLogin);
            public DWIntegrationInfo(string httpAddress, string orgID, bool useWindowsLogin);
            protected DWIntegrationInfo(string httpAddress);
    
            public bool IsEncrypted { get; set; }
            protected override string LoginPage { get; }
    
            public static DWIntegrationInfo Parse(string urlString);
            protected override void AddQueryParams(Dictionary<string, string> par);
        }
    
        public class DWWebLoginInfo
        {
            protected bool isEncrypted;
    
            public DWWebLoginInfo(string httpAddress);
            public DWWebLoginInfo(string webClentUrl, int orgID, bool useWindowsLogin);
            protected DWWebLoginInfo(string webClentUrl, string orgID, bool useWindowsLogin);
    
            public bool IsEncrypted { get; }
            public string Scheme { get; set; }
            public string WebClientUrl { get; set; }
            public string OrganizationId { get; set; }
            public bool UseWindowsLogin { get; set; }
            public UrlVersion Version { get; set; }
            protected virtual string LoginPage { get; }
    
            public static DWWebLoginInfo Parse(string urlString);
            protected static bool ArrayContains(string[] array, string value);
            protected virtual void AddQueryParams(Dictionary<string, string> par);
            protected void ParseBaseUrl(string httpAddress);
            protected void ParseInternal(string urlString);
        }
    }

    I am not sure I would have gotten it to work any other way, as my URLs always looked a bit off from what the URL Creator was generating (as is the situation with you).

    Thanks,
    Joe Kaufman
  • RE: DWIntegration EX_INVALID_PASS_PHRASE-1529591502

    Romain,

    I can't be sure, but I think you might be encoding things too much, if that makes sense? Granted, I operate over an open connection, but this is how I do it (FoxPro code, hopefully it makes sense). This is what generating a URL to view a document might look like:
     
    FUNCTION DWViewDocument
        LPARAMETERS pcFilecabinetGUID, puDocumentID, plDontRunURL, plUseWindowsAuth
        IF (VARTYPE(pcFilecabinetGUID) <> "C") OR EMPTY(ALLTRIM(pcFilecabinetGUID))
            * We cannot proceed without a file cabinet GUID.
            RETURN ""
        ENDIF
        * Document ID can be passed in as a numeric value or a string.
        IF (VARTYPE(puDocumentID) == "C")
            puDocumentID = VAL(puDocumentID)
        ENDIF
        IF (VARTYPE(puDocumentID) <> "N")
            * We cannot proceed without a numeric document ID.
            RETURN ""
        ENDIF        
        SET PROCEDURE TO GlobalProc ADDITIVE
        LOCAL lcURL
        * Establish a base URL with a viewer as the integration type.
        lcURL = DWGetBaseIntegrationURL(DW_INTEGRATION_TYPE_VIEWER, plUseWindowsAuth)
        * Add on file cabinet GUID.
        lcURL = lcURL + "&fc=" + pcFilecabinetGUID
        * Add on the document ID.
        lcURL = lcURL + "&did=" + ALLTRIM(STR(INT(puDocumentID)))
        IF (NOT plDontRunURL)
            * Execute the URL.
            ExecuteCommand(lcURL)
        ENDIF
        * The URL used can be returned.
        RETURN lcURL
    ENDFUNC

    Here is what DWGetBaseIntegrationURL() looks like:
     
    FUNCTION DWGetBaseIntegrationURL
        LPARAMETERS pcIntegrationType, plUseWindowsAuth, plDontDisplayOneDoc
        IF (VARTYPE(pcIntegrationType) == "C")
            pcIntegrationType = UPPER(ALLTRIM(pcIntegrationType))
        ELSE
            * An empty integration type will make the URL be login-only.
            pcIntegrationType = ""
        ENDIF
        LOCAL lcBaseURL, lcParams
        * Start with our integration entry point.
        IF plUseWindowsAuth
            * Use NTML entry point.
            lcBaseURL = DW_INTEGRATION_ENTRY_POINT_NTLM
        ELSE
            lcBaseURL = DW_INTEGRATION_ENTRY_POINT
        ENDIF
        lcParams = ""
        * Now generate parameters, starting with login credentials (if we are not using a Windows login).
        IF (NOT plUseWindowsAuth)
            * We are NOT going use Windows authentication, so use login credentials generated here.
            lcParams = lcParams + "&lc=" + DWGetURLIntegrationCreds()
        ENDIF
        * We can set any other defaults here, too.
        IF (pcIntegrationType == DW_INTEGRATION_TYPE_RESULTLIST_VIEWER) OR (pcIntegrationType == DW_INTEGRATION_TYPE_HISTORY)
            IF (NOT plDontDisplayOneDoc)
                lcParams = lcParams + "&displayOneDoc=true"
            ENDIF
        ENDIF
        * Finally, add on the integration type parameter, if we have one.    
        IF (NOT EMPTY(pcIntegrationType))
            lcParams = lcParams + "&p=" + pcIntegrationType
        ENDIF
        IF (NOT EMPTY(lcParams))
            IF (LEFT(lcParams, 1) = "&")
                * Do not need the ampersand on the first parameter.
                lcParams = SUBSTR(lcParams, 2)
            ENDIF
            lcBaseURL = lcBaseURL + "?" + lcParams
        ENDIF
        RETURN lcBaseURL
    ENDFUNC

    And finally, here is what DWGetURLIntegrationCreds() looks like:
     
    FUNCTION DWGetURLIntegrationCreds
        SET PROCEDURE TO GlobalProc ADDITIVE
        LOCAL lcPlainCreds, lcEncodedCreds
        * Start with a plain-text credential string. We use a literal "\n" to denote a new-line character
        * between username and password, as that is how DocuWare expect the credentials to be encoded.
        lcPlainCreds = "User=" + DW_USERNAME_URL_INTEGRATION + "\nPwd=" + DW_PASSWORD_URL_INTEGRATION
        * URL encode the plain text into a login credential string.
        lcEncodedCreds = UrlTokenEncode(lcPlainCreds)
        RETURN lcEncodedCreds
    ENDFUNC

    The UrlTokenEncode() function does this (looks the same as yours):
     
    FUNCTION UrlTokenEncode
        * This function takes in a string and encodes it the same way System.Web.HttpServerUtility.UrlTokenEncode() 
        * would to in .NET:
        * 1. Use STRCONV to convert the input to Base64.
        * 2. Replace "+" with "-" and "/" with "_". Example: Foo+bar/=== becomes Foo-bar_===.
        * 3. Replace any number of "=" at the end of the string with an integer denoting how many they were. Example: Foo-bar_=== becomes Foo-bar_3.
        LPARAMETERS lcURL
        LOCAL lcEncoded, lnNumEqualSigns
        * 1. Convert string to Base64.
        lcEncoded = STRCONV(lcURL, 13)
        * 2. Replace certain characters with others.
        lcEncoded = CHRTRAN(lcEncoded, "+/", "-_")
        * 3. See how many "=" are at the end of the encoded string and convert to digit.
        lnNumEqualSigns = LEN(lcEncoded) - LEN(RTRIM(lcEncoded, 1, "="))
        lcEncoded = LEFT(lcEncoded, LEN(lcEncoded) - lnNumEqualSigns)
        lcEncoded = lcEncoded + ALLTRIM(STR(lnNumEqualSigns))
        RETURN lcEncoded
    ENDFUNC

    After all this, the URL is sent as-is -- no further encryption. User credentials and the query parameter are Url encoded, but that is it.

    Maybe that won't work if you are using a secure connection, but on my system what I generate matches the URL Integrator, and the URLs work to do things like view documents. Your user credentials look correct, so it must be something with encrypting/encoding.

    Good luck,
    Joe Kaufman