]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/helpers/UrlHelper.kt
Migrate five classes to Kotlin. https://redmine.stoutner.com/issues/950
[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     /*  This entire method might not be needed.
200     fun getSize(context: Context, url: URL, userAgent: String, cookiesEnabled: Boolean): String {
201         // Initialize the formatted file size string.
202         var formattedFileSize = context.getString(R.string.unknown_size)
203
204         // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions.
205         try {
206             // Instantiate the proxy helper.
207             val proxyHelper = ProxyHelper()
208
209             // Get the current proxy.
210             val proxy = proxyHelper.getCurrentProxy(context)
211
212             // Open a connection to the URL.  No data is actually sent at this point.
213             val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
214
215             // Add the user agent to the header property.
216             httpUrlConnection.setRequestProperty("User-Agent", userAgent)
217
218             // Add the cookies if they are enabled.
219             if (cookiesEnabled) {
220                 // Get the cookies for the current domain.
221                 val cookiesString = CookieManager.getInstance().getCookie(url.toString())
222
223                 // Only add the cookies if they are not null.
224                 if (cookiesString != null) {
225                     // Add the cookies to the header property.
226                     httpUrlConnection.setRequestProperty("Cookie", cookiesString)
227                 }
228             }
229
230             // 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.
231             try {
232                 // Get the status code.  This initiates a network connection.
233                 val responseCode = httpUrlConnection.responseCode
234
235                 // Check the response code.
236                 if (responseCode >= 400) {  // The response code is an error message.
237                     // Set the formatted file size to indicate a bad URL.
238                     formattedFileSize = context.getString(R.string.invalid_url)
239                 } else {  // The response code is not an error message.
240                     // Get the content length header.
241                     val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
242
243                     // Only process the content length string if it isn't null.
244                     if (contentLengthString != null) {
245                         // Convert the content length string to a long.
246                         val fileSize = contentLengthString.toLong()
247
248                         // Format the file size.
249                         formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes)
250                     }
251                 }
252             } finally {
253                 // Disconnect the HTTP URL connection.
254                 httpUrlConnection.disconnect()
255             }
256         } catch (exception: Exception) {
257             // Set the formatted file size to indicate a bad URL.
258             formattedFileSize = context.getString(R.string.invalid_url)
259         }
260
261         // Return the formatted file size.
262         return formattedFileSize
263     }
264     */
265
266     @JvmStatic
267     fun highlightSyntax(urlEditText: EditText, initialGrayColorSpan: ForegroundColorSpan, finalGrayColorSpan: ForegroundColorSpan, redColorSpan: ForegroundColorSpan) {
268         // Get the URL string.
269         val urlString: String = urlEditText.text.toString()
270
271         // Highlight the URL according to the protocol.
272         if (urlString.startsWith("file://") || urlString.startsWith("content://")) {  // This is a file or content URL.
273             // De-emphasize everything before the file name.
274             urlEditText.text.setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
275         } else {  // This is a web URL.
276             // Get the index of the `/` immediately after the domain name.
277             val endOfDomainName = urlString.indexOf("/", urlString.indexOf("//") + 2)
278
279             // Get the base URL.
280             val baseUrl: String = if (endOfDomainName > 0)  // There is at least one character after the base URL.
281                 urlString.substring(0, endOfDomainName)
282             else  // There are no characters after the base URL.
283                 urlString
284
285             // Get the index of the last `.` in the domain.
286             val lastDotIndex = baseUrl.lastIndexOf(".")
287
288             // Get the index of the penultimate `.` in the domain.
289             val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1)
290
291             // Markup the beginning of the URL.
292             if (urlString.startsWith("http://")) {  // The protocol is not encrypted.
293                 // Highlight the protocol in red.
294                 urlEditText.text.setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
295
296                 // De-emphasize subdomains.
297                 if (penultimateDotIndex > 0) // There is more than one subdomain in the domain name.
298                     urlEditText.text.setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
299             } else if (urlString.startsWith("https://")) {  // The protocol is encrypted.
300                 // De-emphasize the protocol of connections that are encrypted.
301                 if (penultimateDotIndex > 0)  // There is more than one subdomain in the domain name.  De-emphasize the protocol and the additional subdomains.
302                     urlEditText.text.setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
303                 else  // There is only one subdomain in the domain name.  De-emphasize only the protocol.
304                     urlEditText.text.setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
305             }
306
307             // De-emphasize the text after the domain name.
308             if (endOfDomainName > 0)
309                 urlEditText.text.setSpan(finalGrayColorSpan, endOfDomainName, urlString.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
310         }
311     }
312 }