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