2 * Copyright 2020-2022 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.helpers
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
30 import com.stoutner.privacybrowser.R
32 import java.net.HttpURLConnection
34 import java.text.NumberFormat
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.
40 fun getFileName(context: Context, contentDispositionString: String?, contentTypeString: String?, urlString: String): String {
41 // Define a file name string.
42 var fileNameString: String
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)
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)
55 // Remove any `"` at the beginning of the string.
56 if (fileNameString.startsWith("\""))
57 fileNameString = fileNameString.substring(1)
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)
66 } else { // The content disposition is null.
67 // Get the file name string from the URL.
68 fileNameString = getFileNameFromUrl(context, urlString, contentTypeString)
71 // Return the file name string.
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)
79 // Get the last path segment.
80 var lastPathSegment = uri.lastPathSegment
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)
87 // Add a file extension if it can be detected.
88 if (MimeTypeMap.getSingleton().hasMimeType(contentTypeString))
89 lastPathSegment = lastPathSegment + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentTypeString)
92 // Return the last path segment as the file name.
93 return lastPathSegment
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
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)
106 // Get the URL MIME type, which ends with a `;`.
107 val urlMimeType = urlWithoutData.substring(0, urlWithoutData.indexOf(";"))
109 // Get the Base64 data, which begins after a `,`.
110 val base64DataString = urlWithoutData.substring(urlWithoutData.indexOf(",") + 1)
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)
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)
121 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions.
123 // Convert the URL string to a URL.
124 val url = URL(urlString)
126 // Instantiate the proxy helper.
127 val proxyHelper = ProxyHelper()
129 // Get the current proxy.
130 val proxy = proxyHelper.getCurrentProxy(context)
132 // Open a connection to the URL. No data is actually sent at this point.
133 val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
135 // Add the user agent to the header property.
136 httpUrlConnection.setRequestProperty("User-Agent", userAgent)
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())
143 // Add the cookies if they are not null.
144 if (cookiesString != null)
145 httpUrlConnection.setRequestProperty("Cookie", cookiesString)
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.
150 // Get the status code. This initiates a network connection.
151 val responseCode = httpUrlConnection.responseCode
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)
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.
162 val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
163 val contentDispositionString = httpUrlConnection.getHeaderField("Content-Disposition")
164 var contentTypeString = httpUrlConnection.contentType
166 // Remove anything after the MIME type in the content type string.
167 if (contentTypeString.contains(";"))
168 contentTypeString = contentTypeString.substring(0, contentTypeString.indexOf(";"))
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()
175 // Format the file size.
176 formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes)
179 // Get the file name string from the content disposition.
180 fileNameString = getFileName(context, contentDispositionString, contentTypeString, urlString)
183 // Disconnect the HTTP URL connection.
184 httpUrlConnection.disconnect()
186 } catch (exception: Exception) {
187 // Set the formatted file size to indicate a bad URL.
188 formattedFileSize = context.getString(R.string.invalid_url)
190 // Set the file name according to the URL.
191 fileNameString = getFileNameFromUrl(context, urlString, null)
195 // Return the file name and size.
196 return Pair(fileNameString, formattedFileSize)
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)
203 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions.
205 // Instantiate the proxy helper.
206 val proxyHelper = ProxyHelper()
208 // Get the current proxy.
209 val proxy = proxyHelper.getCurrentProxy(context)
211 // Open a connection to the URL. No data is actually sent at this point.
212 val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
214 // Add the user agent to the header property.
215 httpUrlConnection.setRequestProperty("User-Agent", userAgent)
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())
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)
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.
231 // Get the status code. This initiates a network connection.
232 val responseCode = httpUrlConnection.responseCode
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")
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()
247 // Format the file size.
248 formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes)
252 // Disconnect the HTTP URL connection.
253 httpUrlConnection.disconnect()
255 } catch (exception: Exception) {
256 // Set the formatted file size to indicate a bad URL.
257 formattedFileSize = context.getString(R.string.invalid_url)
260 // Return the formatted file size.
261 return formattedFileSize
265 fun highlightSyntax(urlEditText: EditText, initialGrayColorSpan: ForegroundColorSpan, finalGrayColorSpan: ForegroundColorSpan, redColorSpan: ForegroundColorSpan) {
266 // Get the URL string.
267 val urlString: String = urlEditText.text.toString()
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)
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.
283 // Get the index of the last `.` in the domain.
284 val lastDotIndex = baseUrl.lastIndexOf(".")
286 // Get the index of the penultimate `.` in the domain.
287 val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1)
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)
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)
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)