]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/helpers/UrlHelper.kt
Fix a crash on restart by a new intent. https://redmine.stoutner.com/issues/981
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / helpers / UrlHelper.kt
1 /*
2  * Copyright 2020-2023 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
6  * Privacy Browser Android 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 Android 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 Android.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.helpers
21
22 import android.content.Context
23 import android.net.Uri
24 import android.text.Spanned
25 import android.text.style.ForegroundColorSpan
26 import android.webkit.CookieManager
27 import android.webkit.MimeTypeMap
28 import android.widget.EditText
29
30 import com.stoutner.privacybrowser.R
31
32 import java.net.HttpURLConnection
33 import java.net.URL
34 import java.text.NumberFormat
35
36 object UrlHelper {
37     // Content dispositions can contain other text besides the file name, and they can be in any order.
38     // Elements are separated by semicolons.  Sometimes the file names are contained in quotes.
39     @JvmStatic
40     fun getFileName(context: Context, contentDispositionString: String?, contentTypeString: String?, urlString: String): String {
41         // Define a file name string.
42         var fileNameString: String
43
44         // Only process the content disposition string if it isn't null.
45         if (contentDispositionString != null) {  // The content disposition is not null.
46             // Check to see if the content disposition contains a file name.
47             if (contentDispositionString.contains("filename=")) {  // The content disposition contains a filename.
48                 // Get the part of the content disposition after `filename=`.
49                 fileNameString = contentDispositionString.substring(contentDispositionString.indexOf("filename=") + 9)
50
51                 // Remove any `;` and anything after it.  This removes any entries after the filename.
52                 if (fileNameString.contains(";"))
53                     fileNameString = fileNameString.substring(0, fileNameString.indexOf(";") - 1)
54
55                 // Remove any `"` at the beginning of the string.
56                 if (fileNameString.startsWith("\""))
57                     fileNameString = fileNameString.substring(1)
58
59                 // Remove any `"` at the end of the string.
60                 if (fileNameString.endsWith("\""))
61                     fileNameString = fileNameString.substring(0, fileNameString.length - 1)
62             } else {  // The headers contain no useful information.
63                 // Get the file name string from the URL.
64                 fileNameString = getFileNameFromUrl(context, urlString, contentTypeString)
65             }
66         } else {  // The content disposition is null.
67             // Get the file name string from the URL.
68             fileNameString = getFileNameFromUrl(context, urlString, contentTypeString)
69         }
70
71         // Return the file name string.
72         return fileNameString
73     }
74
75     private fun getFileNameFromUrl(context: Context, urlString: String, contentTypeString: String?): String {
76         // Convert the URL string to a URI.
77         val uri = Uri.parse(urlString)
78
79         // Get the last path segment.
80         var lastPathSegment = uri.lastPathSegment
81
82         // Use a default file name if the last path segment is null.
83         if (lastPathSegment == null) {
84             // Set the last path segment to be the generic file name.
85             lastPathSegment = context.getString(R.string.file)
86
87             // Add a file extension if it can be detected.
88             if (MimeTypeMap.getSingleton().hasMimeType(contentTypeString))
89                 lastPathSegment = lastPathSegment + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentTypeString)
90         }
91
92         // Return the last path segment as the file name.
93         return lastPathSegment
94     }
95
96     fun getNameAndSize(context: Context, urlString: String, userAgent: String, cookiesEnabled: Boolean): Pair<String, String> {
97         // Define the strings.
98         var fileNameString: String
99         var formattedFileSize: String
100
101         // Populate the file size and name strings.
102         if (urlString.startsWith("data:")) {  // The URL contains the entire data of an image.
103             // Remove `data:` from the beginning of the URL.
104             val urlWithoutData = urlString.substring(5)
105
106             // Get the URL MIME type, which ends with a `;`.
107             val urlMimeType = urlWithoutData.substring(0, urlWithoutData.indexOf(";"))
108
109             // Get the Base64 data, which begins after a `,`.
110             val base64DataString = urlWithoutData.substring(urlWithoutData.indexOf(",") + 1)
111
112             // Calculate the file size of the data URL.  Each Base64 character represents 6 bits.
113             formattedFileSize = NumberFormat.getInstance().format(base64DataString.length * 3L / 4) + " " + context.getString(R.string.bytes)
114
115             // Set the file name according to the MIME type.
116             fileNameString = context.getString(R.string.file) + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(urlMimeType)
117         } else {  // The URL refers to the location of the data.
118             // Initialize the formatted file size string.
119             formattedFileSize = context.getString(R.string.unknown_size)
120
121             // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions.
122             try {
123                 // Convert the URL string to a URL.
124                 val url = URL(urlString)
125
126                 // Instantiate the proxy helper.
127                 val proxyHelper = ProxyHelper()
128
129                 // Get the current proxy.
130                 val proxy = proxyHelper.getCurrentProxy(context)
131
132                 // Open a connection to the URL.  No data is actually sent at this point.
133                 val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
134
135                 // Add the user agent to the header property.
136                 httpUrlConnection.setRequestProperty("User-Agent", userAgent)
137
138                 // Add the cookies if they are enabled.
139                 if (cookiesEnabled) {
140                     // Get the cookies for the current domain.
141                     val cookiesString = CookieManager.getInstance().getCookie(url.toString())
142
143                     // Add the cookies if they are not null.
144                     if (cookiesString != null)
145                         httpUrlConnection.setRequestProperty("Cookie", cookiesString)
146                 }
147
148                 // 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.
149                 try {
150                     // Get the status code.  This initiates a network connection.
151                     val responseCode = httpUrlConnection.responseCode
152
153                     // Check the response code.
154                     if (responseCode >= 400) {  // The response code is an error message.
155                         // Set the formatted file size to indicate a bad URL.
156                         formattedFileSize = context.getString(R.string.invalid_url)
157
158                         // Set the file name according to the URL.
159                         fileNameString = getFileNameFromUrl(context, urlString, null)
160                     } else {  // The response code is not an error message.
161                         // Get the headers.
162                         val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
163                         val contentDispositionString = httpUrlConnection.getHeaderField("Content-Disposition")
164                         var contentTypeString = httpUrlConnection.contentType
165
166                         // Remove anything after the MIME type in the content type string.
167                         if (contentTypeString.contains(";"))
168                             contentTypeString = contentTypeString.substring(0, contentTypeString.indexOf(";"))
169
170                         // Only process the content length string if it isn't null.
171                         if (contentLengthString != null) {
172                             // Convert the content length string to a long.
173                             val fileSize = contentLengthString.toLong()
174
175                             // Format the file size.
176                             formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes)
177                         }
178
179                         // Get the file name string from the content disposition.
180                         fileNameString = getFileName(context, contentDispositionString, contentTypeString, urlString)
181                     }
182                 } finally {
183                     // Disconnect the HTTP URL connection.
184                     httpUrlConnection.disconnect()
185                 }
186             } catch (exception: Exception) {
187                 // Set the formatted file size to indicate a bad URL.
188                 formattedFileSize = context.getString(R.string.invalid_url)
189
190                 // Set the file name according to the URL.
191                 fileNameString = getFileNameFromUrl(context, urlString, null)
192             }
193         }
194
195         // Return the file name and size.
196         return Pair(fileNameString, formattedFileSize)
197     }
198
199     @JvmStatic
200     fun highlightSyntax(urlEditText: EditText, initialGrayColorSpan: ForegroundColorSpan, finalGrayColorSpan: ForegroundColorSpan, redColorSpan: ForegroundColorSpan) {
201         // Get the URL string.
202         val urlString: String = urlEditText.text.toString()
203
204         // Highlight the URL according to the protocol.
205         if (urlString.startsWith("file://") || urlString.startsWith("content://")) {  // This is a file or content URL.
206             // De-emphasize everything before the file name.
207             urlEditText.text.setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
208         } else {  // This is a web URL.
209             // Get the index of the `/` immediately after the domain name.
210             val endOfDomainName = urlString.indexOf("/", urlString.indexOf("//") + 2)
211
212             // Get the base URL.
213             val baseUrl: String = if (endOfDomainName > 0)  // There is at least one character after the base URL.
214                 urlString.substring(0, endOfDomainName)
215             else  // There are no characters after the base URL.
216                 urlString
217
218             // Get the index of the last `.` in the domain.
219             val lastDotIndex = baseUrl.lastIndexOf(".")
220
221             // Get the index of the penultimate `.` in the domain.
222             val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1)
223
224             // Markup the beginning of the URL.
225             if (urlString.startsWith("http://")) {  // The protocol is not encrypted.
226                 // Highlight the protocol in red.
227                 urlEditText.text.setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
228
229                 // De-emphasize subdomains.
230                 if (penultimateDotIndex > 0) // There is more than one subdomain in the domain name.
231                     urlEditText.text.setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
232             } else if (urlString.startsWith("https://")) {  // The protocol is encrypted.
233                 // De-emphasize the protocol of connections that are encrypted.
234                 if (penultimateDotIndex > 0)  // There is more than one subdomain in the domain name.  De-emphasize the protocol and the additional subdomains.
235                     urlEditText.text.setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
236                 else  // There is only one subdomain in the domain name.  De-emphasize only the protocol.
237                     urlEditText.text.setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
238             }
239
240             // De-emphasize the text after the domain name.
241             if (endOfDomainName > 0)
242                 urlEditText.text.setSpan(finalGrayColorSpan, endOfDomainName, urlString.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
243         }
244     }
245 }