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