
When working with the WebBrowser component, it is often necessary to fill in the form fields on the site. There are no problems with the usual form fields, there are standard methods for filling them, but the file type field the browser stubbornly refuses to fill out. The reason for this is user protection. If the browser allowed to freely substitute in this field the path to the file on the user's computer, then using a simple JavaScript embedded in the page of the site could easily take any file.
But since the program using the browser already has access to user files, theoretically, such protection should not work against it. But we have what we have, and therefore it is necessary to look for workarounds.
Solutions to the problem
- Fill in all necessary fields of the form, except for the file sending field and intercept the data sending event, modifying the POST. In essence, it is easy to complement the request with data about the file we need.
- To form and send a POST request manually (without using a browser) without forgetting that you need to take all browser cookies, and after sending the request, rewrite their new values back to the browser
I dropped the second option immediately: a lot of work and pitfalls. Stopped at first. But here's a bad luck: it turns out that during the interception of the request, we are not given the entire request, but only the part that goes to the place where the contents of the file should be in spite of the fact that we did not fill out the field. Google suggested that this was a mistake of Microsoft programmers who made a COM object, and this error was not fixed in new versions (tested on IE 9).
A little more scary, I realized that I don’t have an ideal solution (I’m generally silent about ready-made solutions), because the only acceptable option is to completely form a POST request myself and send it by means of a browser (it turns out some combination of the first and second solutions to the problem) so that in the future this could all be conveniently used, I decided to immediately write a convenient class for forming and sending such requests (a link to the source code at the end of the article).
')
Class development
First of all, I determined for myself the scheme of class work. In brief, it is as follows: creating an array of parameters, forming a request, sending a request. And also described the structure of the element of the array of parameters:
TCustomPostParam = record name : string; value : string; filename : string; content_type : string; end;
Then I decided what should be the methods of filling the array of parameters. I got 3:
- Adding any parameters is a universal method that allows you to add any parameters, be it a text field or a file
- Adding files is a convenient method for adding files of any type. Itself considers the contents of the file and send the necessary data to the previous method
- Adding text files is a method for adding text files, the only difference from the previous one is that it fills the content type.
It turned out the following:
function TCustomPostDataSender.AddParam(name, value: string; content_type : string = ''; filename : string = ''):integer; var h : integer; begin SetLength(FParams, Count() + 1); h := high(FParams); FParams[h].name := name; FParams[h].value := value; FParams[h].content_type := content_type; FParams[h].filename := filename; CheckBoundary(name + ' ' + value + ' ' + content_type); result := h; end; procedure TCustomPostDataSender.AddFile(name, path, file_content_type: string); var s : TStringStream; buf : string; begin if not FileExists(path) then exit; file_content_type := Trim(file_content_type); if file_content_type = '' then file_content_type := 'text/plain'; s := TStringStream.Create; try s.LoadFromFile(path); buf := s.DataString; if Pos('text', LowerCase(file_content_type)) = 1 then CheckUTF8(buf); AddParam(name, buf, file_content_type, ExtractFileName(path)); finally s.Free; end; end; procedure TCustomPostDataSender.AddTextFile(name, path: string); begin if not FileExists(path) then exit; if LowerCase(ExtractFileExt(path)) = '.xml' then AddFile(name, path, 'text/xml') else AddFile(name, path, 'text/plain'); end;
Next thing is writing the function of forming and sending a request. I got this:
function TCustomPostDataSender.POST():integer; var sURL, sFlags, sTargetFrame, sPostData, sHeaders : OleVariant; i : integer; buf : string; begin Result := 1; if FURL = '' then exit; try sURL := FURL; sFlags := 64; // , sHeaders := FGetHeader(); buf := ''; for i := 0 to Count() - 1 do begin if i = 0 then buf := buf + '--' + FBoundary + sLineBreak; buf := buf + 'Content-Disposition: form-data; name="'+ FParams[i].name +'"'; // if FParams[i].filename <> '' then buf := buf + '; filename="'+ FParams[i].filename +'"' + sLineBreak + 'Content-Type: ' + FParams[i].content_type; buf := buf + sLineBreak + sLineBreak; buf := buf + FParams[i].value + sLineBreak; buf := buf + '--' + FBoundary; if i = Count() - 1 then buf := buf + '--'; buf := buf + sLineBreak; end; sPostData := StringToVariantArray(buf); WB.Navigate2(sURL, sFlags, sTargetFrame, sPostData, sHeaders); Clear(); Result := 0; except Result := 999; end; end;
I will not go into the details of the other methods; anyone who is interested can study the source code of the whole class in more detail. I can only say that the method is completely self-sufficient: it monitors the correctness and uniqueness of Boundary itself, forms the necessary headers itself and translates all the data into the necessary type.
As for the return value, it would be better to make the type
boolean , since in this case the function returns 0 if the request is sent and 999 - if there were errors (everything else still does not allow the user to show the text of the error, which is also very bad), but To justify myself, I’ll say that this method was developed with a reserve for the future - where each error has its own code, which is returned, and by this code the program must determine how to act and what to inform the user. In any case, to change this is not difficult even for a novice programmer.
Usage example
var CustomRequest : TCustomPostDataSender; begin CustomRequest := TCustomPostDataSender.Create(WebBrowser); try CustomRequest.AddTextFile('import', FILE_PATH); CustomRequest.AddParam('action', 'save'); CustomRequest.AddParam('submit', 'Import'); CustomRequest.SetURL('http://site.ru/import-file.php'); CustomRequest.SetContentType('multipart/form-data'); CustomRequest.POST(); finally CustomRequest.Free(); end; end;
Conclusion
Link to the full source code:
pastebin.com/DgykYAxKLink to md5hash (
DCPcrypt library is required for work):
pastebin.com/cTS3ttwZThe class works under the Delphi 2009+ version (that is, the Unicode version of the compiler) and with the EmbeddedWB component (which I strongly recommend using instead of the standard TWebBrowser).
The article can be useful not only for delphists, the problems described in it are found in other programming languages that use the COM object of the Microsoft Internet Explorer browser, and the solution itself (with some skills) is not difficult to translate into another language.