View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.attachment;
16  
17  import java.io.ByteArrayInputStream;
18  import java.io.InputStream;
19  import java.util.ArrayList;
20  import java.util.List;
21  
22  import org.htmlunit.HttpWebConnectionTest;
23  import org.htmlunit.MockWebConnection;
24  import org.htmlunit.Page;
25  import org.htmlunit.SimpleWebTestCase;
26  import org.htmlunit.TextPage;
27  import org.htmlunit.WebClient;
28  import org.htmlunit.WebResponse;
29  import org.htmlunit.html.HtmlAnchor;
30  import org.htmlunit.html.HtmlElement;
31  import org.htmlunit.html.HtmlPage;
32  import org.htmlunit.util.MimeType;
33  import org.htmlunit.util.NameValuePair;
34  import org.junit.jupiter.api.Test;
35  
36  /**
37   * Tests for {@link Attachment}.
38   *
39   * @author Bruce Chapman
40   * @author Sudhan Moghe
41   * @author Daniel Gredler
42   * @author Ronald Brill
43   * @author Lai Quang Duong
44   */
45  public class AttachmentTest extends SimpleWebTestCase {
46  
47      /**
48       * Tests attachment callbacks and the contents of attachments.
49       * @throws Exception if an error occurs
50       */
51      @Test
52      public void basic() throws Exception {
53          final String content1 = DOCTYPE_HTML
54              + "<html><body>\n"
55              + "<form method='POST' name='form' action='" + URL_SECOND + "'>\n"
56              + "<input type='submit' value='ok'>\n"
57              + "</form>\n"
58              + "<a href='#' onclick='document.form.submit()'>click me</a>\n"
59              + "</body></html>";
60          final String content2 = "download file contents";
61  
62          final WebClient client = getWebClient();
63          final List<Attachment> attachments = new ArrayList<>();
64          client.setAttachmentHandler(new CollectingAttachmentHandler(attachments));
65  
66          final List<NameValuePair> headers = new ArrayList<>();
67          headers.add(new NameValuePair("Content-Disposition", "attachment"));
68  
69          final MockWebConnection conn = new MockWebConnection();
70          conn.setResponse(URL_FIRST, content1);
71          conn.setResponse(URL_SECOND, content2, 200, "OK", MimeType.TEXT_HTML, headers);
72          client.setWebConnection(conn);
73          assertTrue(attachments.isEmpty());
74  
75          final HtmlPage result = client.getPage(URL_FIRST);
76          final HtmlAnchor anchor = result.getAnchors().get(0);
77          final Page clickResult = anchor.click();
78          assertEquals(result, clickResult);
79          assertEquals(1, attachments.size());
80          assertTrue(HtmlPage.class.isInstance(attachments.get(0).getPage()));
81          // the attachment is opened inside a new window
82          assertEquals(2, client.getWebWindows().size());
83  
84          final Attachment attachment = attachments.get(0);
85          final Page attachedPage = attachment.getPage();
86          final WebResponse attachmentResponse = attachedPage.getWebResponse();
87          final InputStream attachmentStream = attachmentResponse.getContentAsStream();
88          HttpWebConnectionTest.assertEquals(new ByteArrayInputStream(content2.getBytes()), attachmentStream);
89          assertEquals(MimeType.TEXT_HTML, attachmentResponse.getContentType());
90          assertEquals(200, attachmentResponse.getStatusCode());
91          assertEquals(URL_SECOND, attachmentResponse.getWebRequest().getUrl());
92      }
93  
94      /**
95       * Tests attachment callbacks and the contents of attachments.
96       * @throws Exception if an error occurs
97       */
98      @Test
99      public void contentDispositionCaseInsensitive() throws Exception {
100         final String content1 = DOCTYPE_HTML
101             + "<html><body>\n"
102             + "<form method='POST' name='form' action='" + URL_SECOND + "'>\n"
103             + "<input type='submit' value='ok'>\n"
104             + "</form>\n"
105             + "<a href='#' onclick='document.form.submit()'>click me</a>\n"
106             + "</body></html>";
107         final String content2 = "download file contents";
108 
109         final WebClient client = getWebClient();
110         final List<Attachment> attachments = new ArrayList<>();
111         client.setAttachmentHandler(new CollectingAttachmentHandler(attachments));
112 
113         final List<NameValuePair> headers = new ArrayList<>();
114         headers.add(new NameValuePair("Content-Disposition", "AttachMent"));
115 
116         final MockWebConnection conn = new MockWebConnection();
117         conn.setResponse(URL_FIRST, content1);
118         conn.setResponse(URL_SECOND, content2, 200, "OK", MimeType.TEXT_HTML, headers);
119         client.setWebConnection(conn);
120         assertTrue(attachments.isEmpty());
121 
122         final HtmlPage result = client.getPage(URL_FIRST);
123         final HtmlAnchor anchor = result.getAnchors().get(0);
124         final Page clickResult = anchor.click();
125         assertEquals(result, clickResult);
126         assertEquals(1, attachments.size());
127     }
128 
129     /**
130      * Tests {@link Attachment#getSuggestedFilename()}.
131      * @throws Exception if an error occurs
132      */
133     @Test
134     public void filename() throws Exception {
135         final String content = DOCTYPE_HTML + "<html>But is it really?</html>";
136 
137         final WebClient client = getWebClient();
138         final MockWebConnection conn = new MockWebConnection();
139         client.setWebConnection(conn);
140         final List<Attachment> attachments = new ArrayList<>();
141         client.setAttachmentHandler(new CollectingAttachmentHandler(attachments));
142 
143         final List<NameValuePair> headers1 = new ArrayList<>();
144         headers1.add(new NameValuePair("Content-Disposition", "attachment;filename=\"hello.html\""));
145         conn.setResponse(URL_FIRST, content, 200, "OK", MimeType.TEXT_HTML, headers1);
146         client.getPage(URL_FIRST);
147         final Attachment result = attachments.get(0);
148         assertEquals(result.getSuggestedFilename(), "hello.html");
149         attachments.clear();
150 
151         final List<NameValuePair> headers2 = new ArrayList<>();
152         headers2.add(new NameValuePair("Content-Disposition", "attachment; filename=hello2.html; something=else"));
153         conn.setResponse(URL_SECOND, content, 200, "OK", MimeType.TEXT_PLAIN, headers2);
154         client.getPage(URL_SECOND);
155         final Attachment result2 = attachments.get(0);
156         assertEquals(result2.getSuggestedFilename(), "hello2.html");
157         assertEquals(content, ((TextPage) result2.getPage()).getContent());
158         attachments.clear();
159 
160         final List<NameValuePair> headers3 = new ArrayList<>();
161         headers3.add(new NameValuePair("Content-Disposition", "attachment; filename="));
162         conn.setResponse(URL_SECOND, content, 200, "OK", MimeType.TEXT_PLAIN, headers3);
163         client.getPage(URL_SECOND);
164         final Attachment result3 = attachments.get(0);
165         assertNull(result3.getSuggestedFilename());
166         assertEquals(content, ((TextPage) result3.getPage()).getContent());
167         attachments.clear();
168 
169         final List<NameValuePair> headers4 = new ArrayList<>();
170         headers4.add(new NameValuePair("Content-Disposition", "attachment"));
171         final byte[] contentb = new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE};
172         conn.setResponse(URL_THIRD, contentb, 200, "OK", "application/x-rubbish", headers4);
173         client.getPage(URL_THIRD);
174         final Attachment result4 = attachments.get(0);
175         final InputStream result4Stream = result4.getPage().getWebResponse().getContentAsStream();
176         assertNull(result4.getSuggestedFilename());
177         assertEquals(result4.getPage().getWebResponse().getContentType(), "application/x-rubbish");
178         HttpWebConnectionTest.assertEquals(new ByteArrayInputStream(contentb), result4Stream);
179         attachments.clear();
180     }
181 
182     /**
183      * @throws Exception if an error occurs
184      */
185     @Test
186     public void filenameFromAnchor() throws Exception {
187         final WebClient client = getWebClient();
188         final MockWebConnection conn = new MockWebConnection();
189         client.setWebConnection(conn);
190         final List<Attachment> attachments = new ArrayList<>();
191         client.setAttachmentHandler(new CollectingAttachmentHandler(attachments));
192 
193         final String content = DOCTYPE_HTML
194                 + "<html>\n"
195                 + "<head>\n"
196                 + "<script>\n"
197                 + "var blob = new Blob(['foo'], {type: 'text/plain'}),\n"
198                 + "  url = URL.createObjectURL(blob),\n"
199                 + "  elem = document.createElement('a');\n"
200                 + "elem.href = url, elem.download = 'foo.txt', elem.click();\n"
201                 + "</script>\n"
202                 + "</head>\n"
203                 + "</html>\n";
204 
205         conn.setResponse(URL_FIRST, content);
206         client.getPage(URL_FIRST);
207 
208         final Attachment result = attachments.get(0);
209         assertEquals(result.getSuggestedFilename(), "foo.txt");
210         assertEquals("foo", ((TextPage) result.getPage()).getContent());
211         attachments.clear();
212     }
213 
214     /**
215      * @throws Exception if an error occurs
216      */
217     @Test
218     public void filenameFromFile() throws Exception {
219         final WebClient client = getWebClient();
220         final MockWebConnection conn = new MockWebConnection();
221         client.setWebConnection(conn);
222         final List<Attachment> attachments = new ArrayList<>();
223         client.setAttachmentHandler(new CollectingAttachmentHandler(attachments));
224 
225         final String content = DOCTYPE_HTML
226                 + "<html>\n"
227                 + "<head>\n"
228                 + "<script>\n"
229                 + "var blob = new File(['bar'], 'bar.txt', {type: 'text/plain'}),\n"
230                 + "  url = URL.createObjectURL(blob),\n"
231                 + "  elem = document.createElement('a');\n"
232                 + "elem.href = url, elem.download = '', elem.click();\n"
233                 + "</script>\n"
234                 + "</head>\n"
235                 + "</html>\n";
236 
237         conn.setResponse(URL_FIRST, content);
238         client.getPage(URL_FIRST);
239 
240         final Attachment result = attachments.get(0);
241         assertEquals(result.getSuggestedFilename(), "bar.txt");
242         assertEquals("bar", ((TextPage) result.getPage()).getContent());
243         attachments.clear();
244     }
245 
246 
247     /**
248      * Prioritize name from Anchor.download over File.name.
249      * @throws Exception if an error occurs
250      */
251     @Test
252     public void filenameFromFilePrioritizeAnchorDownloadOverFileName() throws Exception {
253         final WebClient client = getWebClient();
254         final MockWebConnection conn = new MockWebConnection();
255         client.setWebConnection(conn);
256         final List<Attachment> attachments = new ArrayList<>();
257         client.setAttachmentHandler(new CollectingAttachmentHandler(attachments));
258 
259         final String content = DOCTYPE_HTML
260                 + "<html>\n"
261                 + "<head>\n"
262                 + "<script>\n"
263                 + "var blob = new File(['bar'], 'bar.txt', {type: 'text/plain'}),\n"
264                 + "  url = URL.createObjectURL(blob),\n"
265                 + "  elem = document.createElement('a');\n"
266                 + "elem.href = url, elem.download = 'foobar.txt', elem.click();\n"
267                 + "</script>\n"
268                 + "</head>\n"
269                 + "</html>\n";
270 
271         conn.setResponse(URL_FIRST, content);
272         client.getPage(URL_FIRST);
273 
274         final Attachment result = attachments.get(0);
275         assertEquals(result.getSuggestedFilename(), "foobar.txt");
276         assertEquals("bar", ((TextPage) result.getPage()).getContent());
277         attachments.clear();
278     }
279 
280     /**
281      * This was causing a ClassCastException in Location.setHref as of 2013-10-08 because the TextPage
282      * used for the attachment was wrongly associated to the HTMLDocument of the first page.
283      * @throws Exception if an error occurs
284      */
285     @Test
286     public void jsChangeLocationAfterReceptionOfAttachment() throws Exception {
287         final String html = DOCTYPE_HTML
288             + "<html><body>\n"
289             + "<form action='action'>\n"
290             + "<input type='submit' onclick='window.location=\"foo\"; return false'>\n"
291             + "</form>\n"
292             + "<a href='" + URL_SECOND + "'>download</a>\n"
293             + "</body></html>";
294 
295         final WebClient client = getWebClient();
296         final List<Attachment> attachments = new ArrayList<>();
297         client.setAttachmentHandler(new CollectingAttachmentHandler(attachments));
298 
299         final List<NameValuePair> headers = new ArrayList<>();
300         headers.add(new NameValuePair("Content-Disposition", "attachment"));
301 
302         final MockWebConnection conn = getMockWebConnection();
303         conn.setDefaultResponse("");
304         conn.setResponse(URL_SECOND, "some text", 200, "OK", MimeType.TEXT_PLAIN, headers);
305 
306         final HtmlPage page = loadPage(html);
307         // download text attachment
308         page.getAnchors().get(0).click();
309         assertEquals(1, attachments.size());
310 
311         final HtmlElement htmlElement = (HtmlElement) page.getFirstByXPath("//input");
312         htmlElement.click(); // exception was occurring here
313     }
314 
315     /**
316      * Tests attachment callbacks and the contents of attachments.
317      * @throws Exception if an error occurs
318      */
319     @Test
320     public void handleResponseFromHandler() throws Exception {
321         final String content1 = DOCTYPE_HTML
322             + "<html><body>\n"
323             + "<form method='POST' name='form' action='" + URL_SECOND + "'>\n"
324             + "<input type='submit' value='ok'>\n"
325             + "</form>\n"
326             + "<a href='#' onclick='document.form.submit()'>click me</a>\n"
327             + "</body></html>";
328         final String content2 = "download file contents";
329 
330         final WebClient client = getWebClient();
331         final List<WebResponse> attachments = new ArrayList<>();
332         final List<String> attachmentFilenames = new ArrayList<>();
333 
334         client.setAttachmentHandler(new AttachmentHandler() {
335             @Override
336             public boolean handleAttachment(final WebResponse response, final String attachmentFilename) {
337                 attachments.add(response);
338                 attachmentFilenames.add(attachmentFilename);
339                 return true;
340             }
341 
342             @Override
343             public void handleAttachment(final Page page, final String attachmentFilename) {
344                 throw new IllegalAccessError("handleAttachment(Page, String) called");
345             }
346         });
347 
348         final List<NameValuePair> headers = new ArrayList<>();
349         headers.add(new NameValuePair("Content-Disposition", "attachment"));
350 
351         final MockWebConnection conn = new MockWebConnection();
352         conn.setResponse(URL_FIRST, content1);
353         conn.setResponse(URL_SECOND, content2, 200, "OK", MimeType.TEXT_HTML, headers);
354         client.setWebConnection(conn);
355         assertTrue(attachments.isEmpty());
356         assertTrue(attachmentFilenames.isEmpty());
357 
358         final HtmlPage result = client.getPage(URL_FIRST);
359         final HtmlAnchor anchor = result.getAnchors().get(0);
360         final Page clickResult = anchor.click();
361         assertEquals(result, clickResult);
362         assertEquals(1, attachments.size());
363         assertEquals(1, attachmentFilenames.size());
364         assertEquals(1, client.getWebWindows().size());
365 
366         final WebResponse attachmentResponse = attachments.get(0);
367         final InputStream attachmentStream = attachmentResponse.getContentAsStream();
368         HttpWebConnectionTest.assertEquals(new ByteArrayInputStream(content2.getBytes()), attachmentStream);
369         assertEquals(MimeType.TEXT_HTML, attachmentResponse.getContentType());
370         assertEquals(200, attachmentResponse.getStatusCode());
371         assertEquals(URL_SECOND, attachmentResponse.getWebRequest().getUrl());
372 
373         assertNull(attachmentFilenames.get(0));
374     }
375 
376     /**
377      * Tests attachment callbacks and the contents of attachments.
378      * @throws Exception if an error occurs
379      */
380     @Test
381     public void handleResponseFromHandlerWithFileName() throws Exception {
382         final String content1 = DOCTYPE_HTML
383             + "<html><body>\n"
384             + "<form method='POST' name='form' action='" + URL_SECOND + "'>\n"
385             + "<input type='submit' value='ok'>\n"
386             + "</form>\n"
387             + "<a href='#' onclick='document.form.submit()'>click me</a>\n"
388             + "</body></html>";
389         final String content2 = "download file contents";
390 
391         final WebClient client = getWebClient();
392         final List<WebResponse> attachments = new ArrayList<>();
393         final List<String> attachmentFilenames = new ArrayList<>();
394 
395         client.setAttachmentHandler(new AttachmentHandler() {
396             @Override
397             public boolean handleAttachment(final WebResponse response, final String attachmentFilename) {
398                 attachments.add(response);
399                 attachmentFilenames.add(attachmentFilename);
400                 return true;
401             }
402 
403             @Override
404             public void handleAttachment(final Page page, final String attachmentFilename) {
405                 throw new IllegalAccessError("handleAttachment(Page, String) called");
406             }
407         });
408 
409         final List<NameValuePair> headers = new ArrayList<>();
410         headers.add(new NameValuePair("Content-Disposition", "attachment; filename=htmlunit.zip"));
411 
412         final MockWebConnection conn = new MockWebConnection();
413         conn.setResponse(URL_FIRST, content1);
414         conn.setResponse(URL_SECOND, content2, 200, "OK", MimeType.TEXT_HTML, headers);
415         client.setWebConnection(conn);
416         assertTrue(attachments.isEmpty());
417         assertTrue(attachmentFilenames.isEmpty());
418 
419         final HtmlPage result = client.getPage(URL_FIRST);
420         final HtmlAnchor anchor = result.getAnchors().get(0);
421         final Page clickResult = anchor.click();
422         assertEquals(result, clickResult);
423         assertEquals(1, attachments.size());
424         assertEquals(1, attachmentFilenames.size());
425         assertEquals(1, client.getWebWindows().size());
426 
427         final WebResponse attachmentResponse = attachments.get(0);
428         final InputStream attachmentStream = attachmentResponse.getContentAsStream();
429         HttpWebConnectionTest.assertEquals(new ByteArrayInputStream(content2.getBytes()), attachmentStream);
430         assertEquals(MimeType.TEXT_HTML, attachmentResponse.getContentType());
431         assertEquals(200, attachmentResponse.getStatusCode());
432         assertEquals(URL_SECOND, attachmentResponse.getWebRequest().getUrl());
433 
434         assertEquals("htmlunit.zip", attachmentFilenames.get(0));
435     }
436 
437     /**
438      * Tests attachment callbacks and the contents of attachments.
439      * @throws Exception if an error occurs
440      */
441     @Test
442     public void handleResponseOnlyApplicationOctetstream() throws Exception {
443         final String content1 = DOCTYPE_HTML
444             + "<html><body>\n"
445             + "<form method='POST' name='form' action='" + URL_SECOND + "'>\n"
446             + "<input type='submit' value='ok'>\n"
447             + "</form>\n"
448             + "<a href='#' onclick='document.form.submit()'>click me</a>\n"
449             + "</body></html>";
450         final String content2 = "download file contents";
451 
452         final WebClient client = getWebClient();
453         final List<WebResponse> attachments = new ArrayList<>();
454         final List<String> attachmentFilenames = new ArrayList<>();
455 
456         client.setAttachmentHandler(new AttachmentHandler() {
457             @Override
458             public boolean handleAttachment(final WebResponse response, final String attachmentFilename) {
459                 attachments.add(response);
460                 attachmentFilenames.add(attachmentFilename);
461                 return true;
462             }
463 
464             @Override
465             public void handleAttachment(final Page page, final String attachmentFilename) {
466                 throw new IllegalAccessError("handleAttachment(Page, String) called");
467             }
468         });
469 
470         final List<NameValuePair> headers = new ArrayList<>();
471         headers.add(new NameValuePair("Content-Type", MimeType.APPLICATION_OCTET_STREAM));
472 
473         final MockWebConnection conn = new MockWebConnection();
474         conn.setResponse(URL_FIRST, content1);
475         conn.setResponse(URL_SECOND, content2, 200, "OK", MimeType.TEXT_HTML, headers);
476         client.setWebConnection(conn);
477         assertTrue(attachments.isEmpty());
478         assertTrue(attachmentFilenames.isEmpty());
479 
480         final HtmlPage result = client.getPage(URL_FIRST);
481         final HtmlAnchor anchor = result.getAnchors().get(0);
482         final Page clickResult = anchor.click();
483         assertEquals(result, clickResult);
484         assertEquals(1, attachments.size());
485         assertEquals(1, attachmentFilenames.size());
486         assertEquals(1, client.getWebWindows().size());
487 
488         final WebResponse attachmentResponse = attachments.get(0);
489         final InputStream attachmentStream = attachmentResponse.getContentAsStream();
490         HttpWebConnectionTest.assertEquals(new ByteArrayInputStream(content2.getBytes()), attachmentStream);
491         assertEquals(MimeType.APPLICATION_OCTET_STREAM, attachmentResponse.getContentType());
492         assertEquals(200, attachmentResponse.getStatusCode());
493         assertEquals(URL_SECOND, attachmentResponse.getWebRequest().getUrl());
494 
495         assertNull(attachmentFilenames.get(0));
496     }
497 }