3

Good morning, I am struggling to find information on a problem which seems to not have much information available on the internet - that is the "frame notification bar" in internet explorer ( the little yellow window that asks you if you want to "save" or "open" a file downloaded).

I will cut to the chase, the issue I am having is that my code works when an internet explorer visibility is set to true, but doesn't work when visibility is set to false. I have stepped through the code in both situations to see what changes, and noticed the handle for the frame notification bar changes value but other than that all are the same. The relevant code is:

Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long

Sub StartIE()
    Dim appIE As Object
    Dim URLString As String
    Dim HTMLdoc, btn As Object
    Set appIE = CreateObject("internetexplorer.application") ' create an instance of internet explorer


    With appIE
        .Navigate "https://analytics.twitter.com/user" 'this url wont work for you. you will need to have your own twitter account on twitter analytics, and copy the link to the "tweets" page
        .Visible = True ' and show the IE
    End With
    Do While appIE.Busy Or (appIE.READYSTATE <> 4) ' wait until IE has finished loading

        DoEvents
    Loop

    URLString = appIE.LocationURL

    Set HTMLdoc = appIE.document
    Set btn = HTMLdoc.getElementsByClassName("btn btn-default ladda-button")(0) 'finds the export data button
    btn.Click
    Do While appIE.Busy Or (appIE.READYSTATE <> 4) ' wait until IE has finished loading
            DoEvents
    Loop

    Application.Wait (Now + TimeValue("0:00:07"))



    Dim hwnd As LongPtr, h As LongPtr


    Dim o As IUIAutomation ' The following steps are used to download a csv file from a webpage
    Dim e As IUIAutomationElement
    Set o = New CUIAutomation
    h = appIE.hwnd
    h = FindWindowEx(h, 0, "Frame Notification Bar", vbNullString) ' we must find the first frame notification handle
    If h = 0 Then Exit Sub
    Set e = o.ElementFromHandle(ByVal h) 
    Dim iCnd As IUIAutomationCondition
    Set iCnd = o.CreatePropertyCondition(UIA_NamePropertyId, "Save") 
    Dim Button As IUIAutomationElement
    Set Button = e.FindFirst(TreeScope_Subtree, iCnd)
    Dim InvokePattern As IUIAutomationInvokePattern
    Set InvokePattern = Button.GetCurrentPattern(UIA_InvokePatternId)
    InvokePattern.Invoke


    h = appIE.hwnd
    h = FindWindowEx(h, 0, "Frame Notification Bar", vbNullString)
    If h = 0 Then Exit Sub

    Set e = o.ElementFromHandle(ByVal h)
    Dim iCnd2 As IUIAutomationCondition
    Set iCnd2 = o.CreatePropertyCondition(UIA_NamePropertyId, "Open") ' similar to the above snippet, except for the second stage of the frame notification window

    Dim Button2 As IUIAutomationElement
    Set Button2 = e.FindFirst(TreeScope_Subtree, iCnd2)
    Dim InvokePattern2 As IUIAutomationInvokePattern
    Set InvokePattern2 = Button2.GetCurrentPattern(UIA_InvokePatternId)
    InvokePattern2.Invoke

End Sub

Out of this code, the snippet where I believe the issue is occurring is in:

    Dim o As IUIAutomation ' The following steps are used to download a csv file from a webpage
    Dim e As IUIAutomationElement
    Set o = New CUIAutomation
    h = appIE.hwnd
    h = FindWindowEx(h, 0, "Frame Notification Bar", vbNullString) ' we must find the first frame notification handle
    If h = 0 Then Exit Sub
    Set e = o.ElementFromHandle(ByVal h) 
    Dim iCnd As IUIAutomationCondition
    Set iCnd = o.CreatePropertyCondition(UIA_NamePropertyId, "Save") 
    Dim Button As IUIAutomationElement
    Set Button = e.FindFirst(TreeScope_Subtree, iCnd)
    Dim InvokePattern As IUIAutomationInvokePattern
    Set InvokePattern = Button.GetCurrentPattern(UIA_InvokePatternId)
    InvokePattern.Invoke

Can anyone provide me with an idea of why this might be happening, and how I might fix it? I know I'm asking alot already but I would really love an explanation alongside any fix as I am trying to improve my understanding, and find it will be beneficial to others in a similar situation :)

Thankyou in advance.

Christian T
  • 128
  • 1
  • 12
  • 1
    It would be great if you can make a mcve out of this question - simply remove all the third parties libraries and make sure to leave the code to less than 20 lines, in which this behaviour is visible - https://stackoverflow.com/help/mcve – Vityata Feb 20 '18 at 09:28
  • I don't know what references you got activated but I cannot reproduce it :O – Foxfire And Burns And Burns Feb 20 '18 at 09:47
  • Sorry guys, I've tried to remove as much code as I can but because I don't know where the problem lies it's difficult. @FoxfireAndBurnsAndBurns NOTE this code won't work for everyone, as it interacts with an individual twitter account. I have commented in the code where you would need to link to your twitter analytics page rather than the link that is currently there – Christian T Feb 20 '18 at 09:59
  • @ChristianT Is not the Twitter part, my problem are the IUIAutomation and stuff. I'm checking SO to find what references I need to activate this code, because my vba does not get it. I won't be able to help you :( but I will learn something new from your code. Thanks for sharing. – Foxfire And Burns And Burns Feb 20 '18 at 10:02
  • @FoxfireAndBurnsAndBurns I have references to UIAutomationClient and Microsoft HTML Object Library :) – Christian T Feb 20 '18 at 10:08
  • Have you tried to retrieve the data using XHR, without IE? – omegastripes Feb 20 '18 at 19:39
  • @omegastripes I have not, and don't know about xhr. Would it be possible even if the button I am clicking to download does not have an url, or at least a static one? – Christian T Feb 21 '18 at 08:58
  • I guess that is possible by 3 requests: 1) get authorization data, 2) get file URL, 3) download the file. – omegastripes Feb 21 '18 at 09:56
  • @omegastripes even if file URL changes? – Christian T Feb 21 '18 at 11:58
  • Second request is to retrieve volatile URL. – omegastripes Feb 21 '18 at 12:06
  • @omegastripes any chance you could provide a link to some examples, I don't know where to start :S – Christian T Feb 21 '18 at 12:50
  • [About XHR](https://codingislove.com/http-requests-excel-vba/) but the problem is that you do not have an URL where to download the file... – Foxfire And Burns And Burns Feb 21 '18 at 15:26
  • @FoxfireAndBurnsAndBurns I thought this would be the case. Thanks anyway – Christian T Feb 22 '18 at 15:35
  • 1
    I tried your code after activating the references. It's curious how when you make AppIE.Visible=false, it does not work. It fails in the sentence `Set InvokePattern = Button.GetCurrentPattern(UIA_InvokePatternId)`. Your question looks pretty interesting to me and I upvoted for it, hoping somebody can resolve this issue. I wish i could help because this one is a really good question, but I'm afraid i cannot. Let's see if somebody can. Maybe starting a bounty – Foxfire And Burns And Burns Feb 22 '18 at 15:40
  • Thankyou @FoxfireAndBurnsAndBurns I appreciate you trying the code and hope that someone may come up with an answer eventually. For the time being, I am keeping internet explorer minimized as a work around. – Christian T Feb 28 '18 at 13:06

1 Answers1

2

Take a look at the below example:

Option Explicit

Sub SaveTweetsToCsv()

    Dim sAuthToken As String
    Dim sUserName As String
    Dim sStartTime As String
    Dim sEndTime As String
    Dim aHeaders
    Dim sUrl As String
    Dim sParams As String
    Dim sResp As String

    ' Set init data
    sUserName = "myusername" ' Your username
    sStartTime = "1517184000000" ' UNIX time with milliseconds
    sEndTime = "1519603199999"
    ' Check saved auth token
    sAuthToken = GetEnvVar("user", "tw_auth_token")
    ' Retrieve auth token if missing
    If sAuthToken = "" Then sAuthToken = GetAuthToken()
    ' Prepare request parameters
    sUrl = "https://analytics.twitter.com/user/" & sUserName & "/tweets/"
    sParams = "start_time=" & sStartTime & "&end_time=" & sEndTime & "&lang=en"
    ' Set request auth token header
    aHeaders = Array(Array("cookie", "auth_token=" & sAuthToken))
    ' Make request and check availability
    Do
        ' Retrieve status
        WinHTTPRequest "POST", sUrl & "export.json?" & sParams, _
            "", _
            aHeaders, _
            "", _
            "", _
            sResp, _
            ""
        ' Check if auth token is invalid
        If InStr(sResp, "403 Forbidden") > 0 Then sAuthToken = GetAuthToken()
        ' Check report availability
        If InStr(sResp, """status"":""Available""") > 0 Then Exit Do
        DoEvents
        Application.Wait (Now + TimeValue("0:00:01"))
    Loop
    ' Retrieve CSV content
    WinHTTPRequest "GET", sUrl & "bundle?" & sParams, _
        "", _
        aHeaders, _
        "", _
        "", _
        sResp, _
        ""
    ' Save CSV
    WriteTextFile sResp, CreateObject("WScript.Shell").SpecialFolders("Desktop") & "\result.csv", -1
    MsgBox "Completed"

End Sub

Function GetAuthToken() As String

    Dim sLogin As String
    Dim sPassword As String
    Dim sHdrs As String
    Dim sResp As String
    Dim aSetHeaders
    Dim aTmp
    Dim sToken As String
    Dim aPayload
    Dim sPayload As String
    Dim aOptions
    Dim i As Long

    If MsgBox("Login", vbOKCancel) = vbCancel Then End
    sLogin = "mylogin" ' Your login
    sPassword = "mypassword" ' Your password
    ' Retrieve login form
    WinHTTPRequest "GET", "https://twitter.com/", _
        "", _
        "", _
        "", _
        sHdrs, _
        sResp, _
        ""
    ' Extract cookies from headers
    ParseResponse "^Set-(Cookie): (\S*?=\S*?);[\s\S]*?$", sHdrs, aSetHeaders
    ' Extract authenticity_token from login form
    aTmp = Split(sResp, """ name=""authenticity_token""", 2)
    If UBound(aTmp) = 0 Then MsgBox "Failed to get authenticity token": End
    sToken = Mid(aTmp(0), InStrRev(aTmp(0), """") + 1)
    ' Prepare payload for login request
    aPayload = Array( _
        Array("session[username_or_email]", sLogin), _
        Array("session[password]", sPassword), _
        Array("remember_me", "1"), _
        Array("return_to_ssl", "true"), _
        Array("scribe_log", ""), _
        Array("redirect_after_login", "/"), _
        Array("authenticity_token", sToken), _
        Array("ui_metrics", "") _
    )
    For i = 0 To UBound(aPayload)
        aPayload(i) = EncodeUriComponent((aPayload(i)(0))) & "=" & EncodeUriComponent((aPayload(i)(1)))
    Next
    sPayload = Join(aPayload, "&")
    ' Add web form headers
    PushItem aSetHeaders, Array("Content-Type", "application/x-www-form-urlencoded")
    PushItem aSetHeaders, Array("Content-Length", Len(sPayload))
    ' WinHTTP option disabling redirections
    aOptions = Array(Array(6, False)) ' redirectoins disabled
    ' Login request
    WinHTTPRequest "POST", "https://twitter.com/sessions", _
        aOptions, _
        aSetHeaders, _
        sPayload, _
        sHdrs, _
        sResp, _
        ""
    ' Extract auth_token from received headers
    aTmp = Split(sHdrs, "auth_token=", 2)
    If UBound(aTmp) = 0 Then MsgBox "Failed to get auth token": End
    GetAuthToken = Split(aTmp(1), ";", 2)(0)
    ' Save auth token to user env var for further usage
    SetEnvVar "user", "tw_auth_token", GetAuthToken
    MsgBox "Auth token retrieved successfully"

End Function

Sub SetEnvVar(sEnv As String, sName As String, sValue As String)

    CreateObject("WSCript.Shell").Environment(sEnv).Item(sName) = sValue

End Sub

Function GetEnvVar(sEnv As String, sName As String) As String

    GetEnvVar = CreateObject("WSCript.Shell").Environment(sEnv).Item(sName)

End Function

Sub WinHTTPRequest(sMethod, sUrl, aSetOptions, aSetHeaders, vFormData, sRespHeaders, sRespText, aRespBody)

    Dim aItem

    With CreateObject("WinHttp.WinHttpRequest.5.1")
        .Open sMethod, sUrl, False
        If IsArray(aSetOptions) Then
            For Each aItem In aSetOptions
                .Option(aItem(0)) = aItem(1)
            Next
        End If
        If IsArray(aSetHeaders) Then
            For Each aItem In aSetHeaders
                .SetRequestHeader aItem(0), aItem(1)
            Next
        End If
        .send (vFormData)
        sRespHeaders = .GetAllResponseHeaders
        sRespText = .ResponseText
        aRespBody = .ResponseBody
    End With
End Sub

Sub ParseResponse(sPattern, sResponse, aData, Optional bAppend As Boolean = True, Optional bGlobal As Boolean = True, Optional bMultiLine As Boolean = True, Optional bIgnoreCase As Boolean = True)

    Dim oMatch
    Dim aTmp()
    Dim sSubMatch

    If Not (IsArray(aData) And bAppend) Then aData = Array()
    With CreateObject("VBScript.RegExp")
        .Global = bGlobal
        .MultiLine = bMultiLine
        .IgnoreCase = bIgnoreCase
        .Pattern = sPattern
        For Each oMatch In .Execute(sResponse)
            If oMatch.SubMatches.Count = 1 Then
                PushItem aData, oMatch.SubMatches(0)
            Else
                aTmp = Array()
                For Each sSubMatch In oMatch.SubMatches
                    PushItem aTmp, sSubMatch
                Next
                PushItem aData, aTmp
            End If
        Next
    End With

End Sub

Sub PushItem(aData, vItem, Optional bAppend As Boolean = True)

    If Not (IsArray(aData) And bAppend) Then aData = Array()
    ReDim Preserve aData(UBound(aData) + 1)
    aData(UBound(aData)) = vItem

End Sub

Function EncodeUriComponent(sText As String) As String

    Static objHtmlfile As Object

    If objHtmlfile Is Nothing Then
        Set objHtmlfile = CreateObject("htmlfile")
        objHtmlfile.parentWindow.execScript "function encode(s) {return encodeURIComponent(s)}", "jscript"
    End If
    EncodeUriComponent = objHtmlfile.parentWindow.encode(sText)

End Function

Sub WriteTextFile(sContent As String, sPath As String, lFormat As Long)

    With CreateObject("Scripting.FileSystemObject").OpenTextFile(sPath, 2, True, lFormat)
        .Write sContent
        .Close
    End With

End Sub

Note. Excessive auth token requests are detected by the web site as automated, that may lead to the account to be blocked, in that case you will need to enter the captcha and confirm your phone number to receive a code by SMS. That is why auth token is saved to environment variable once it is retrieved, for further usage.

omegastripes
  • 12,351
  • 4
  • 45
  • 96
  • Thankyou for your time in providing this answer, there is a lot to understand here. Having copied this code and changing the login details, this example doesn't work for me and returns the error: the server name or address could not be resolved. – Christian T Mar 05 '18 at 14:13
  • This code is a masterpiece. It worked perfect!, I wish I could understand how you managed to do this. There is a lot to learn here. I just activated reference **Microsoft WinHTTP Services** and this worked awesome. Just 1 question. You said *Excessive auth token requests are detected by the web site as automated, that may lead to the account to be blocked. hat is why auth token is saved to environment variable once it is retrieved, for further usage.* So the code remembers the auth token, to avoid being blocked, right? So using this in a daily routine should not be a problem. You rock!!! – Foxfire And Burns And Burns Mar 05 '18 at 15:15
  • @ChristianT Check your account in browser first. There are several `WinHTTPRequest` calls, could you please elaborate which one generates the error, executing the code step by step in debug mode? Also locals window content, right before calling `WinHTTPRequest` which is failing, would be helpful. The best data for debug in that case is captured requests with Fiddler. Do you have internet access via proxy? – omegastripes Mar 05 '18 at 17:31
  • @FoxfireAndBurnsAndBurns Fiddler shows all necessary request details. The only thing that is necessary to capture traffic from `WinHttp.WinHttpRequest.5.1` is a patch described [here](https://www.telerik.com/blogs/using-fiddler-with-winhttp). – omegastripes Mar 05 '18 at 17:35
  • Hi @omegastripes I don't have internet access via proxy. The first WinHTTPRequest in the GetAuthToken function is where the error is generated. Debugging highlights the line within the WinHTTPRequest sub which reads: .send (vFormData) – Christian T Mar 14 '18 at 13:04
  • @ChristianT Is the error still "the server name or address could not be resolved"? That's odd, since the URL for the first request should be just `https://twitter.com/`, could you please confirm that in locals window? Try to configure proxy settings for WinHTTP using [proxycfg.exe](https://msdn.microsoft.com/en-us/library/windows/desktop/ms761351(v=vs.85).aspx), probably you need to disable proxy. – omegastripes Mar 14 '18 at 14:34
  • @omegastripes I can confirm the error is the same, and the url is as you posted in locals window. One thing which is probably significant, I am doing this from my work computer, where there are a significant number of security measures in place, and I lack permissions for many things. I will not be able to use proxycfg unfortunately – Christian T Mar 14 '18 at 15:10
  • @ChristianT Is your PC in a corporate network? Internet access via proxy is typical for that case. You can check if your actual location is different from your IP location, using any _my IP_ service. – omegastripes Mar 14 '18 at 17:39
  • @omegastripes yes it is. I did think it probably would be via a proxy however I googled how to check this and performed the check which seemed to indicate that i wasn't using a proxy. Based on my IP I probably am. Interested as to why this causes the code to fail, and if there is a solution. – Christian T Mar 15 '18 at 08:54
  • @ChristianT WinHTTP doesn't use IE proxy settings, so some additional code should be added, which retrieves proxy settings from IE and puts to WinHTTP. I will try and then back. – omegastripes Mar 15 '18 at 09:04
  • @ChristianT Could you please test if WinHTTP works with proxy for you using the solution from [this answer](https://stackoverflow.com/a/31076675/2165759)? You need to download [VBA-Web - Blank.xlsm from GitHub](https://github.com/VBA-tools/VBA-Web/raw/master/VBA-Web%20-%20Blank.xlsm), then to change the URL to `.BaseUrl = "https://twitter.com/"` and add `.Insecure = True` for `client` properties, set breakpoint at the last line, aftere the line `client.Execute()`, and [check `response.StatusCode` in Locals window](https://i.stack.imgur.com/pE1kD.png). – omegastripes Mar 19 '18 at 15:47
  • @omegastripes so sorry, I am unable to download from this machine. The code is telling me webclient is undefined, is there any specific reference i need? I already have microsoft win http services... – Christian T Mar 19 '18 at 16:06
  • @ChristianT Will you be able to copy somehow that file [VBA-Web - Blank.xlsm](https://github.com/VBA-tools/VBA-Web/raw/master/VBA-Web%20-%20Blank.xlsm) to your machine? – omegastripes Mar 19 '18 at 18:18
  • @omegastripes done as you specified, response status code is OK. Is this helpful? – Christian T Mar 20 '18 at 10:11
  • @ChristianT Yes, that means that solution implemented in VBA-Web can be used for requests in the above answer. I will modify the code and then back here. – omegastripes Mar 20 '18 at 11:39
  • @omegastripes thankyou so much, you are going above and beyond. I only wish I knew how you managed all this! – Christian T Mar 20 '18 at 11:52