Use Content-Type to guess files extensions for downloads with unknown names. https...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / asynctasks / PrepareSaveDialog.java
1 /*
2  * Copyright © 2020 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.asynctasks;
21
22 import android.app.Activity;
23 import android.content.Context;
24 import android.net.Uri;
25 import android.os.AsyncTask;
26 import android.webkit.CookieManager;
27 import android.webkit.MimeTypeMap;
28
29 import androidx.fragment.app.DialogFragment;
30 import androidx.fragment.app.FragmentManager;
31
32 import com.stoutner.privacybrowser.R;
33 import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog;
34 import com.stoutner.privacybrowser.helpers.ProxyHelper;
35
36 import java.lang.ref.WeakReference;
37 import java.net.HttpURLConnection;
38 import java.net.Proxy;
39 import java.net.URL;
40 import java.text.NumberFormat;
41
42 public class PrepareSaveDialog extends AsyncTask<String, Void, String[]> {
43     // Define weak references.
44     private WeakReference<Activity> activityWeakReference;
45     private WeakReference<Context> contextWeakReference;
46     private WeakReference<FragmentManager> fragmentManagerWeakReference;
47
48     // Define the class variables.
49     private int saveType;
50     private String userAgent;
51     private boolean cookiesEnabled;
52     private String urlString;
53
54     // The public constructor.
55     public PrepareSaveDialog(Activity activity, Context context, FragmentManager fragmentManager, int saveType, String userAgent, boolean cookiesEnabled) {
56         // Populate the weak references.
57         activityWeakReference = new WeakReference<>(activity);
58         contextWeakReference = new WeakReference<>(context);
59         fragmentManagerWeakReference = new WeakReference<>(fragmentManager);
60
61         // Store the class variables.
62         this.saveType = saveType;
63         this.userAgent = userAgent;
64         this.cookiesEnabled = cookiesEnabled;
65     }
66
67     @Override
68     protected String[] doInBackground(String... urlToSave) {
69         // Get a handle for the activity and context.
70         Activity activity = activityWeakReference.get();
71         Context context = contextWeakReference.get();
72
73         // Abort if the activity is gone.
74         if (activity == null || activity.isFinishing()) {
75             // Return a null string array.
76             return null;
77         }
78
79         // Get the URL string.
80         urlString = urlToSave[0];
81
82         // Define the file name string.
83         String fileNameString;
84
85         // Initialize the formatted file size string.
86         String formattedFileSize = context.getString(R.string.unknown_size);
87
88         // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions.
89         try {
90             // Convert the URL string to a URL.
91             URL url = new URL(urlString);
92
93             // Instantiate the proxy helper.
94             ProxyHelper proxyHelper = new ProxyHelper();
95
96             // Get the current proxy.
97             Proxy proxy = proxyHelper.getCurrentProxy(context);
98
99             // Open a connection to the URL.  No data is actually sent at this point.
100             HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy);
101
102             // Add the user agent to the header property.
103             httpUrlConnection.setRequestProperty("User-Agent", userAgent);
104
105             // Add the cookies if they are enabled.
106             if (cookiesEnabled) {
107                 // Get the cookies for the current domain.
108                 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
109
110                 // only add the cookies if they are not null.
111                 if (cookiesString != null) {
112                     // Add the cookies to the header property.
113                     httpUrlConnection.setRequestProperty("Cookie", cookiesString);
114                 }
115             }
116
117             // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
118             try {
119                 // Get the status code.  This initiates a network connection.
120                 int responseCode = httpUrlConnection.getResponseCode();
121
122                 // Check the response code.
123                 if (responseCode >= 400) {  // The response code is an error message.
124                     // Set the formatted file size to indicate a bad URL.
125                     formattedFileSize = context.getString(R.string.invalid_url);
126
127                     // Set the file name according to the URL.
128                     fileNameString = getFileNameFromUrl(context, urlString, null);
129                 } else {  // The response code is not an error message.
130                     // Get the headers.
131                     String contentLengthString = httpUrlConnection.getHeaderField("Content-Length");
132                     String contentDispositionString = httpUrlConnection.getHeaderField("Content-Disposition");
133                     String contentTypeString = httpUrlConnection.getContentType();
134
135                     // Remove anything after the MIME type in the content type string.
136                     if (contentTypeString.contains(";")) {
137                         // Remove everything beginning with the `;`.
138                         contentTypeString = contentTypeString.substring(0, contentTypeString.indexOf(";"));
139                     }
140
141                     // Only process the content length string if it isn't null.
142                     if (contentLengthString != null) {
143                         // Convert the content length string to a long.
144                         long fileSize = Long.parseLong(contentLengthString);
145
146                         // Format the file size.
147                         formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes);
148                     }
149
150                     // Get the file name string from the content disposition.
151                     fileNameString = getFileNameFromHeaders(context, contentDispositionString, contentTypeString, urlString);
152                 }
153             } finally {
154                 // Disconnect the HTTP URL connection.
155                 httpUrlConnection.disconnect();
156             }
157         } catch (Exception exception) {
158             // Set the formatted file size to indicate a bad URL.
159             formattedFileSize = context.getString(R.string.invalid_url);
160
161             // Set the file name according to the URL.
162             fileNameString = getFileNameFromUrl(context, urlString, null);
163         }
164
165         // Return the formatted file size and name as a string array.
166         return new String[] {formattedFileSize, fileNameString};
167     }
168
169     // `onPostExecute()` operates on the UI thread.
170     @Override
171     protected void onPostExecute(String[] fileStringArray) {
172         // Get a handle for the activity and the fragment manager.
173         Activity activity = activityWeakReference.get();
174         FragmentManager fragmentManager = fragmentManagerWeakReference.get();
175
176         // Abort if the activity is gone.
177         if (activity == null || activity.isFinishing()) {
178             // Exit.
179             return;
180         }
181
182         // Instantiate the save dialog.
183         DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(saveType, urlString, fileStringArray[0], fileStringArray[1], userAgent, cookiesEnabled);
184
185         // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
186         saveDialogFragment.show(fragmentManager, activity.getString(R.string.save_dialog));
187     }
188
189     // Content dispositions can contain other text besides the file name, and they can be in any order.
190     // Elements are separated by semicolons.  Sometimes the file names are contained in quotes.
191     public static String getFileNameFromHeaders(Context context, String contentDispositionString, String contentTypeString, String urlString) {
192         // Define a file name string.
193         String fileNameString;
194
195         // Only process the content disposition string if it isn't null.
196         if (contentDispositionString != null) {  // The content disposition is not null.
197             // Check to see if the content disposition contains a file name.
198             if (contentDispositionString.contains("filename=")) {  // The content disposition contains a filename.
199                 // Get the part of the content disposition after `filename=`.
200                 fileNameString = contentDispositionString.substring(contentDispositionString.indexOf("filename=") + 9);
201
202                 // Remove any `;` and anything after it.  This removes any entries after the filename.
203                 if (fileNameString.contains(";")) {
204                     // Remove the first `;` and everything after it.
205                     fileNameString = fileNameString.substring(0, fileNameString.indexOf(";") - 1);
206                 }
207
208                 // Remove any `"` at the beginning of the string.
209                 if (fileNameString.startsWith("\"")) {
210                     // Remove the first character.
211                     fileNameString = fileNameString.substring(1);
212                 }
213
214                 // Remove any `"` at the end of the string.
215                 if (fileNameString.endsWith("\"")) {
216                     // Remove the last character.
217                     fileNameString = fileNameString.substring(0, fileNameString.length() - 1);
218                 }
219             } else {  // The headers contain no useful information.
220                 // Get the file name string from the URL.
221                 fileNameString = getFileNameFromUrl(context, urlString, contentTypeString);
222             }
223         } else {  // The content disposition is null.
224             // Get the file name string from the URL.
225             fileNameString = getFileNameFromUrl(context, urlString, contentTypeString);
226         }
227
228         // Return the file name string.
229         return fileNameString;
230     }
231
232     private static String getFileNameFromUrl(Context context, String urlString, String contentTypeString) {
233         // Convert the URL string to a URI.
234         Uri uri = Uri.parse(urlString);
235
236         // Get the last path segment.
237         String lastPathSegment = uri.getLastPathSegment();
238
239         // Use a default file name if the last path segment is null.
240         if (lastPathSegment == null) {
241             lastPathSegment = context.getString(R.string.file);
242
243             if (MimeTypeMap.getSingleton().hasMimeType(contentTypeString)) {  // The content type contains a MIME type.
244                 // Add the file extension that matches the MIME type.
245                 lastPathSegment = lastPathSegment + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentTypeString);
246             }
247         }
248
249         // Return the last path segment as the file name.
250         return lastPathSegment;
251     }
252 }