florentbr / SeleniumBasic

A Selenium based browser automation framework for VB.Net, VBA and VBScript
BSD 3-Clause "New" or "Revised" License
428 stars 197 forks source link

IEDriver download folder #71

Closed longpants closed 8 years ago

longpants commented 8 years ago

Hi Florent,

Is there a way to change the download folder for IEDriver?

As with Chrome: driver.SetPreference "download.default_directory", sDownloadFolder

florentbr commented 8 years ago

For IE, you'll have to edit this registry key manually: HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\Default Download Directory

Or with some code:

Const reg_key = "HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\Default Download Directory"
Const reg_val = "C:\Downloads"
CreateObject("WScript.Shell").RegWrite reg_key, reg_val, "REG_SZ"
longpants commented 8 years ago

hmm bummer :-)

But thanks!

longpants commented 8 years ago

Sorry to ask, but how can I trigger this download from IE? IE shows a popup. Can I work around this?

dornech commented 8 years ago

I was struggling with the same problem and there are some hints around to control the IE download dialog via internal object handles. This was not my option of choice due to the lack of browser independence and having in mind potential migration effort to Edge IMHO this is today even less an option of choice.

I decided a kind of "hybrid" approach: all data downloaded is requested from the respective server using a HTTP request. Instead of the remotely controlled browser sending the request, the application must be enabled to send this request directly and you are done. You just need the parameters ... but these ar enot to difficult to find out with some HTTP request sniffing using Fiddler or FireFox addins. To circumvene the IE dialog box I just simulate a natural user via IE remote control until I can retrieve all necessary information (like session IDs, authorization IDs etc.) to build the appropriate HTTP request and send it directly to the webserver. Advantage: this approach is independent of the webdriver.

florentbr commented 8 years ago

It looks like the automatic downloading has been removed from IE11. The automatic opening is still there so it is still possible by customizing the registry:


Private Sub Usage_Download_Link_IE()
  Dim driver As New IEDriver, ele As WebElement
  driver.Get "https://www.mozilla.org/en-US/foundation/documents"

  Set ele = driver.FindElementByLinkText("IRS Form 872-C")
  Download_Link_IE ele, "C:\Downloads\irs-form-872-c_2.pdf"

  driver.Quit
End Sub

Private Sub Download_Link_IE(ele As WebElement, save_as As String)
  ' Activate the automatic opening for the extension and set xcopy as default application
  Static reg As Object, shl As Object, progid$, extension$
  If shl Is Nothing Then
    Set shl = CreateObject("WScript.Shell")
    shl.RegWrite "HKCU\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_RESTRICT_FILEDOWNLOAD\iexplore.exe", 0, "REG_DWORD"
  End If
  extension = Mid$(save_as, InStrRev(save_as, "."))
  progid = shl.RegRead("HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" & extension & "\UserChoice\ProgId")
  Shell "reg ADD HKCU\Software\Microsoft\Windows\Shell\AttachmentExecute\{0002DF01-0000-0000-C000-000000000046} /v " & progid & " /t REG_BINARY", vbHide
  shl.RegWrite "HKCU\Software\Classes\" & progid & "\shell\", "download", "REG_SZ"
  shl.RegWrite "HKCU\Software\Classes\" & progid & "\shell\download\command\", "xcopy /Y ""%1"" """ & save_as & "*""", "REG_SZ"

  ' Click the file to download
  If Len(Dir$(save_as)) Then Kill save_as
  ele.Click

  ' Wait fo the downloading to finish
  Dim Waiter As New Selenium.Waiter
  Waiter.WaitForFile save_as, 10000

  ' Restore the default application
  shl.RegWrite "HKCU\Software\Classes\" & progid & "\shell\", "open", "REG_SZ"
End Sub

You could also download the file with a simple http request. However this solution only works if the href attribute has a direct link:


Private Sub Usage_Download_Link_IE_2()
  Dim driver As New IEDriver, ele As WebElement

  driver.Get "https://www.mozilla.org/en-US/foundation/documents"
  Set ele = driver.FindElementByLinkText("IRS Form 872-C")
  Download_LinkHref ele, "C:\Downloads\irs-form-872-c_1.pdf"

  driver.Quit
End Sub

Private Sub Download_LinkHref(ele As WebElement, save_as As String)
  ' Extract the data to build the request (link, user-agent, language, cookie)
  Dim info As Selenium.Dictionary
  Set info = ele.ExecuteScript("return {" & _
                  "'link': this.href," & _
                  "'agent': navigator.userAgent," & _
                  "'lang': navigator.userLanguage," & _
                  "'cookie': document.cookie };")

  ' Send the request
  Static xhr As Object
  If xhr Is Nothing Then Set xhr = CreateObject("Msxml2.ServerXMLHTTP.6.0")
  xhr.Open "GET", info("link")
  xhr.setRequestHeader "User-Agent", info("agent")
  xhr.setRequestHeader "Accept-Language", info("lang")
  xhr.setRequestHeader "Cookie", info("cookie")
  xhr.Send
  If (xhr.Status \ 100) - 2 Then Err.Raise 5, , xhr.Status & " " & xhr.StatusText

  ' Save the response to a file
  Static bin As Object
  If bin Is Nothing Then Set bin = CreateObject("ADODB.Stream")
  If Len(Dir$(save_as)) Then Kill save_as
  bin.Open
  bin.Type = 1
  bin.Write xhr.ResponseBody
  bin.Position = 0
  bin.SaveToFile save_as
  bin.Close
End Sub
longpants commented 8 years ago

Hi Florent, Thanks for your input (again)!

Unfortunately the timer times-out, and no file pops. (And the a href doesnt have a direct URL) After spending almost whole day googling for solution I came up to this solution: (modified and made suitable for my project)

Function fnSaveIEDownload(v As Variant) As Boolean
Dim i As Integer
Dim h As Long

'Make sure UIAutomationClient is enabled in References
'C:\Windows\System32\UIAutomationCore.dll
i = -1
restart:
i = i + 1
On Error GoTo errhandler:
    Dim o As IUIAutomation
    Dim e As IUIAutomationElement
    Set o = New CUIAutomation
    'SetForegroundWindow h
    h = FindWindowEx(testobjectWindowTitle, 0, "Frame Notification Bar", vbNullString)

    If h = 0 Then GoTo errhandler

    Set e = o.ElementFromHandle(ByVal h)
    Dim iCnd As IUIAutomationCondition

    Set iCnd = o.CreatePropertyCondition(UIA_NamePropertyId, v(i))

    Dim Button As IUIAutomationElement
    Set Button = e.FindFirst(TreeScope_Subtree, iCnd)
    Dim InvokePattern As IUIAutomationInvokePattern
    Set InvokePattern = Button.GetCurrentPattern(UIA_InvokePatternId)

    InvokePattern.Invoke

    If Not Button Is Nothing Then fnSaveIEDownload = True
Exit Function

errhandler:
    If i < UBound(v) Then GoTo restart
    fnSaveIEDownload = False
    MsgBox "Check VBA reference: 'UIAutomationClient' is installed." & Chr(10) _
        & Chr(10) & "Default path: 'C:\Windows\System32\UIAutomationCore.dll'" & Chr(10) & _
        Chr(10) & "Note that this auto download only works from IE10, else: manual action required" _
        , vbCritical, "Error loading VBA component"

End Function

Perform your click action on the Webelement and fnSaveIEDownload(array("Opslaan","Save") will do the rest. I use the e.Executescript ("this.click();") since this doesn't freeze the VBA code. In which v can be "Save" or "Opslaan", depending on local language...

Pro:

Con:

I hope this helps other people as well.

arhellman commented 8 years ago

Interesting idea longpants. Thanks for pasting the code. I'm stuck using IE11 and trying to get it download selected files. I can't alter my registry and the file don't have their specific URL for download so I can't do the HTTP request from Florent

Have you or any tried just using the sendkeys to navigate to the download dialog? I've been trying but with no luck. (I know sendkeys is not a great alternative for automation) I can't seem to to get the shift+tab to move to the dialog box and then move through the options to open save as. If you the send keys for CTRL+J it appears as directed, but I can't seem to get it to focus on that new popup.

Any ideas on if its possible to set the focus for CTRL+J?

driver.FindElementByClass("MyDownloadButton").Click
driver.Wait 3000
'driver.SendKeys (Keys.Shift & Keys.Tab)
driver.SendKeys Keys.Control, "j" 'Open the view downloads box
driver.SendKeys Keys.ArrowRight 'Navigate to the Save As
driver.SendKeys Keys.ArrowRight 'Navigate to the Save As
driver.SendKeys Keys.ArrowDown 'Navigate to the Save As
driver.SendKeys Keys.ArrowDown 'Navigate to the Save As
driver.SendKeys Keys.Enter 'Enter the Save As Dialog
driver.Wait 200
driver.SendKeys " & MyStringLabel & " & "_" & "ALL" & ".zip" 'Setting the file name and type
driver.Wait 200
driver.SendKeys Keys.Tab 'Navigating to the file path input
driver.SendKeys Keys.Tab 'Navigating to the file path input
driver.SendKeys Keys.Tab 'Navigating to the file path input
driver.SendKeys Keys.Tab 'Navigating to the file path input
driver.SendKeys Keys.Tab 'Navigating to the file path input
driver.SendKeys " & Path & " 'Paste in the string path for saving to the folder
driver.Wait 200
driver.SendKeys Keys.Enter 'Save the file(s)
longpants commented 8 years ago

Thanks, not sure, but I think sending keys by the Driver sends the keys to the HTML, not the browser or IE window. But I can be wrong. You can change that to Application.SendKeys. Check Google or i.e.: http://www.contextures.com/excelvbasendkeys.html for options. With my solution there is no need to change windows register, only check the VBA References to make the automation DLL enabled. I used some other feature to change the default download folder for IE. Every time I run SeleniumBasic I start and end with running this script. Based on the RunCleaner.vbs by Florent:

The code below is called by: fnKillWebDriverSessions

Function fnKillWebDriverSessions()
On Error GoTo errFunction

If Not testObject Is Nothing Then
    testObject.Quit
    Set testObject = Nothing '.Quit

End If
    Dim wsh As Object

Set wsh = VBA.CreateObject("WScript.Shell")
Dim waitOnReturn As Boolean: waitOnReturn = True
'Dim windowStyle As Integer: windowStyle = 1

wsh.run "wscript " & Range("rSelenium").value & "\Scripts\RunCleanerSilent.vbs", vbHide, waitOnReturn
'wsh.run "C:\folder\runbat.bat", windowStyle, waitOnReturn
'    Shell "wscript " & Range("rSelenium").value & "\Scripts\RunCleanerSilent.vbs", vbHide ' vbNormalFocus
    Application.Wait 2000

    Exit Function

errFunction:
If blDebug Then
    Stop
    Resume
End If
End Function

And the RunCleanerSilent.vbs script contains:


' Utility script to 
'  Call driver.Quit on each active session
'  Terminate all the background drivers (chromedriver, iedriver, operadriver, phantomjs)
'  Delete the temporary folder (%TEMP%\Selenium)

Sub Main()
    Call QuitSessions
    Call TerminateDrivers
    Call DeleteTemporaryFolder
    Call ResetIEDownloadFolder
    'Wscript.Echo "Done!"
End Sub

'Quits all the registered sessions
Sub QuitSessions()
    Err.Clear
    On Error Resume Next
    Do
        GetObject("Selenium.WebDriver").Quit
    Loop Until Err.Number
End Sub

'Terminates all the drivers and all the child processes
Sub TerminateDrivers()
    names = Array("chromedriver.exe", "iedriver.exe", "operadriver.exe", "phantomjs.exe", "edgedriver.exe")
    Set mgt = GetObject("winmgmts:")
    On Error Resume Next
    For Each p In mgt.ExecQuery("Select * from Win32_Process Where Name='" & Join(names, "' Or Name='") & "'")
        For Each cp In mgt.ExecQuery("Select * from Win32_Process Where ParentProcessId=" & p.ProcessId)
            cp.Terminate
        Next
        p.Terminate
    Next
End Sub

'Deletes all the files and folders in "%TEMP%\Selenium"
Sub DeleteTemporaryFolder()
    Set sho = CreateObject("WScript.Shell")
    Set fso = CreateObject("Scripting.FileSystemObject")
    folder = sho.ExpandEnvironmentStrings("%TEMP%\Selenium")
    If fso.FolderExists(folder) Then
        Set folderObj = fso.GetFolder(folder)
        On Error Resume Next
        For Each subfolderObj in folderObj.SubFolders
            subfolderObj.Delete True
        Next
        For Each fileObj in folderObj.Files
            fileObj.Delete True
        Next
        folderObj.Delete True
    End If
End Sub

'call main

'exit

Sub ResetIEDownloadFolder()
    Const reg_key_download_backup = "HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\Default Download Directory Backup XLS Webdriver"
    Const reg_key_download_present = "HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\Default Download Directory Present XLS Webdriver"
    Const reg_key_download_default = "HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\Default Download Directory"
    Dim reg_val 'As String

    'On Error GoTo ExitSub

    'check default key present, check if empty then delete
    If isKeyPresent(reg_key_download_default) Then
        If CreateObject("WScript.Shell").RegRead(reg_key_download_default) = "" Then CreateObject("WScript.Shell").RegDelete reg_key_download_default
    End If

    If isKeyPresent(reg_key_download_default) Then
        If isKeyPresent(reg_key_download_backup) Then
            'if backup is present, there was running instance, so reset (and delete backup)
            regv_val = CreateObject("WScript.Shell").RegRead(reg_key_download_backup)
            CreateObject("WScript.Shell").RegWrite reg_key_download_default, regv_val, "REG_SZ"
            CreateObject("WScript.Shell").RegDelete reg_key_download_backup
            'if default was not present initially, remove as well
            If CreateObject("WScript.Shell").RegRead(reg_key_download_present) = 0 Then CreateObject("WScript.Shell").RegDelete reg_key_download_default
        Else
            'backup not present (so new start), store default in backup and set Present was true (1)
            regv_val = CreateObject("WScript.Shell").RegRead(reg_key_download_default)
            CreateObject("WScript.Shell").RegWrite reg_key_download_backup, regv_val, "REG_SZ"
            CreateObject("WScript.Shell").RegWrite reg_key_download_present, 1, "REG_SZ"
        End If

    Else
        'default key not present, store this information
        CreateObject("WScript.Shell").RegWrite reg_key_download_present, 0, "REG_SZ"
        CreateObject("WScript.Shell").RegWrite reg_key_download_backup, 0, "REG_SZ"
    End If

'ExitSub:

End Sub

Function isKeyPresent(sKey) 'As Boolean
    On Error Resume Next
    isKeyPresent = False
    Dim myKey
    Dim keyValue
    Set myKey = CreateObject("WScript.Shell")
    keyValue = myKey.RegRead(sKey)

    If keyValue = "" Then
        isKeyPresent = False
    Else
        isKeyPresent = True
    End If

End Function

Call Main

Logic: Check whether default IE path is present, save this status and backup the path. Then during runtime the default path gets updated to a specific folder with timestamp. And on closure or when starting after abnormal quit of code the execution of the script checks whether default folder was present and removes all otherwise. I'm working on 'protected' laptop as well by the company, but I can change my register on local user key. You can give it a try?

arhellman commented 8 years ago

Thanks for the reply Longpants. Let me take some time to digest it. I understand the logic and the code behind it, I will try to come up with a similar idea for my project. I need to do some reading and learn more about the UIAutomationCore reference.

And yes, I was being dumb and forgot the driver is controlling the HTML. Once I set it to the application sendkeys, I was able to get it to put in my desired path and filename in the save as dialog. Just obviously, I wan to avoid using send keys for something like this; especially in automation environment.

longpants commented 8 years ago

No thanks, I received a lot of help here (especially from Florent), happy to help you. Indeed, I'm trying to avoid the send keys as well, it's not very robust. I'm neither familiair with UIAutomationCore, but happy that it runs smoothly :-)

florentbr commented 8 years ago

For the record, it's also possible to press the Save button by sending keys (shortcut Alt+S). It should be possible with driver.SendKeys Keys.Alt, "s", but unfortunately the underlying IE driver doesn't send properly the Alt key. Here is an example that waits natively for the download dialogue and presses Save without the need to focus on the window:

Private Declare PtrSafe Function FindWindowExA Lib "user32.dll" ( _
  ByVal hwndParent As LongPtr, _
  ByVal hwndChildAfter As LongPtr, _
  ByVal lpszClass As String, _
  ByVal lpszWindow As String) As Long

Private Declare PtrSafe Function PostMessageA Lib "user32.dll" ( _
  ByVal Hwnd As LongPtr, _
  ByVal wMsg As LongPtr, _
  ByVal wParam As LongPtr, _
  ByVal lParama As LongPtr) As Long

Private Sub ConfirmSaveKeyboard()
  Const timeout = 5000

  ' wait for the download dialogue (IEFrame/Frame Notification Bar/DirectUIHWND)
  Dim ie_hwnd, frm_hwnd, dlg_hwnd, endtime#, i&
  ie_hwnd = FindWindowExA(0, 0, "IEFrame", vbNullString)
  endtime = Now + timeout / 86400#
  Do
    frm_hwnd = FindWindowExA(ie_hwnd, 0, "Frame Notification Bar", vbNullString)
    If frm_hwnd Then
      dlg_hwnd = FindWindowExA(frm_hwnd, 0, "DirectUIHWND", vbNullString)
      If dlg_hwnd Then Exit Do
    End If
    If Now > endtime Then Err.Raise 5, , "Failed to find the download dialogue"
    For i = 1 To 10000: DoEvents: Next
  Loop

  ' press Alt + S (Shortcut assigned to the Save button)
  PostMessageA ie_hwnd, &H104&, &H12, &H20000001  'WM_SYSKEYDOWN, VK_MENU
  PostMessageA ie_hwnd, &H104&, &H53, &H20000001  'WM_SYSKEYDOWN, S
  PostMessageA ie_hwnd, &H101&, &H53, &HC0000001  'WM_KEYUP, S
  PostMessageA ie_hwnd, &H101&, &H12, &HC0000001  'WM_KEYUP, VK_MENU
End Sub

And it's also possible to click on Save without the UIAutomationClient library. The trick is to directly work with the accessibility library:

Private Declare PtrSafe Function FindWindowExA Lib "user32.dll" ( _
  ByVal hwndParent As LongPtr, _
  ByVal hwndChildAfter As LongPtr, _
  ByVal lpszClass As String, _
  ByVal lpszWindow As String) As Long

Private Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc.dll" ( _
  ByVal Hwnd As LongPtr, _
  ByVal dwId As Long, _
  ByRef riid As Any, _
  ByRef ppvObject As IAccessible) As Long

Private Declare PtrSafe Function AccessibleChildren Lib "oleacc.dll" ( _
  ByVal paccContainer As IAccessible, _
  ByVal iChildStart As Long, _
  ByVal cChildren As Long, _
  ByRef rgvarChildren As Variant, _
  ByRef pcObtained As Long) As Long

Private Sub ClickOnSave()
  Const timeout = 5000, bt_save = "Save", bt_close = "Close"

  ' wait for the download dialogue (IEFrame/Frame Notification Bar/DirectUIHWND)
  Dim ie_hwnd, frm_hwnd, dlg_hwnd, endtime#, i&
  ie_hwnd = FindWindowExA(0, 0, "IEFrame", vbNullString)
  endtime = Now + timeout / 86400#
  Do
    frm_hwnd = FindWindowExA(ie_hwnd, 0, "Frame Notification Bar", vbNullString)
    If frm_hwnd Then
      dlg_hwnd = FindWindowExA(frm_hwnd, 0, "DirectUIHWND", vbNullString)
      If dlg_hwnd Then Exit Do
    End If
    If Now > endtime Then Err.Raise 5, , "Failed to find the download dialogue"
     For i = 1 To 10000: DoEvents: Next
  Loop

  ' get the accessible interface for the download dialogue
  Dim iid&(0 To 3), acc As IAccessible, bt As IAccessible
  iid(0) = &H618736E0: iid(1) = &H11CF3C3D: iid(2) = &HAA000C81: iid(3) = &H719B3800
  AccessibleObjectFromWindow dlg_hwnd, 0&, iid(0), acc

  ' click on Save
  Set bt = acc_find_button(acc, bt_save)
  If bt Is Nothing Then Err.Raise 5, , "Failed to find the Save button"
  bt.accDoDefaultAction 0&

  ' clic on Close if present
  Set bt = acc_find_button(acc, bt_close)
  If Not bt Is Nothing Then bt.accDoDefaultAction 0&
End Sub

Private Function acc_find_button(ByVal acc As IAccessible, name$) As IAccessible
  If acc.accName(0&) Like name Then
    Set acc_find_button = acc
  ElseIf acc.accChildCount > 0 Then
    Dim children(0 To 15), count&, i&
    AccessibleChildren acc, 0, acc.accChildCount, children(0), count
    For i = 0 To count - 1
      If IsObject(children(i)) Then
        Set acc_find_button = acc_find_button(children(i), name)
        If Not acc_find_button Is Nothing Then Exit For
      End If
    Next
  End If
End Function

Note that all the provided examples were implemented for Windows8 / IE11

RobertoZa commented 7 years ago

Hi @florentbr I'm interesting in automatically download files from EI11 without direct link and i've test both your script but they can't find the button. I'm testing them on EI11 with text in Italian. Do you know how can i solve the problem ? thanks