Fix the ViewPager not always moving to new tabs. https://redmine.stoutner.com/issues/798
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / SslCertificateErrorDialog.kt
1 /*
2  * Copyright © 2016-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.dialogs
21
22 import android.app.Activity
23 import android.app.Dialog
24 import android.content.DialogInterface
25 import android.content.res.Configuration
26 import android.net.Uri
27 import android.net.http.SslError
28 import android.os.AsyncTask
29 import android.os.Bundle
30 import android.text.SpannableStringBuilder
31 import android.text.Spanned
32 import android.text.style.ForegroundColorSpan
33 import android.view.WindowManager
34 import android.widget.TextView
35
36 import androidx.appcompat.app.AlertDialog
37 import androidx.fragment.app.DialogFragment
38 import androidx.preference.PreferenceManager
39
40 import com.stoutner.privacybrowser.R
41 import com.stoutner.privacybrowser.activities.MainWebViewActivity
42 import com.stoutner.privacybrowser.views.NestedScrollWebView
43
44 import java.lang.ref.WeakReference
45 import java.net.InetAddress
46 import java.net.UnknownHostException
47 import java.text.DateFormat
48
49 // Define the class constants.
50 private const val PRIMARY_ERROR_INT = "primary_error_int"
51 private const val URL_WITH_ERRORS = "url_with_errors"
52 private const val ISSUED_TO_CNAME = "issued_to_cname"
53 private const val ISSUED_TO_ONAME = "issued_to_oname"
54 private const val ISSUED_TO_UNAME = "issued_to_uname"
55 private const val ISSUED_BY_CNAME = "issued_by_cname"
56 private const val ISSUED_BY_ONAME = "issued_by_oname"
57 private const val ISSUED_BY_UNAME = "issued_by_uname"
58 private const val START_DATE = "start_date"
59 private const val END_DATE = "end_date"
60 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
61
62 class SslCertificateErrorDialog : DialogFragment() {
63     companion object {
64         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
65         @JvmStatic
66         fun displayDialog(sslError: SslError, webViewFragmentId: Long): SslCertificateErrorDialog {
67             // Get the various components of the SSL error message.
68             val primaryErrorInt = sslError.primaryError
69             val urlWithErrors = sslError.url
70             val sslCertificate = sslError.certificate
71             val issuedToCName = sslCertificate.issuedTo.cName
72             val issuedToOName = sslCertificate.issuedTo.oName
73             val issuedToUName = sslCertificate.issuedTo.uName
74             val issuedByCName = sslCertificate.issuedBy.cName
75             val issuedByOName = sslCertificate.issuedBy.oName
76             val issuedByUName = sslCertificate.issuedBy.uName
77             val startDate = sslCertificate.validNotBeforeDate
78             val endDate = sslCertificate.validNotAfterDate
79
80             // Create an arguments bundle.
81             val argumentsBundle = Bundle()
82
83             // Store the SSL error message components in the bundle.
84             argumentsBundle.putInt(PRIMARY_ERROR_INT, primaryErrorInt)
85             argumentsBundle.putString(URL_WITH_ERRORS, urlWithErrors)
86             argumentsBundle.putString(ISSUED_TO_CNAME, issuedToCName)
87             argumentsBundle.putString(ISSUED_TO_ONAME, issuedToOName)
88             argumentsBundle.putString(ISSUED_TO_UNAME, issuedToUName)
89             argumentsBundle.putString(ISSUED_BY_CNAME, issuedByCName)
90             argumentsBundle.putString(ISSUED_BY_ONAME, issuedByOName)
91             argumentsBundle.putString(ISSUED_BY_UNAME, issuedByUName)
92             argumentsBundle.putString(START_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
93             argumentsBundle.putString(END_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
94             argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
95
96             // Create a new instance of the SSL certificate error dialog.
97             val thisSslCertificateErrorDialog = SslCertificateErrorDialog()
98
99             // Add the arguments bundle to the new dialog.
100             thisSslCertificateErrorDialog.arguments = argumentsBundle
101
102             // Return the new dialog.
103             return thisSslCertificateErrorDialog
104         }
105     }
106
107     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
108         // Get the variables from the bundle.
109         val primaryErrorInt = requireArguments().getInt(PRIMARY_ERROR_INT)
110         val urlWithErrors = requireArguments().getString(URL_WITH_ERRORS)
111         val issuedToCName = requireArguments().getString(ISSUED_TO_CNAME)
112         val issuedToOName = requireArguments().getString(ISSUED_TO_ONAME)
113         val issuedToUName = requireArguments().getString(ISSUED_TO_UNAME)
114         val issuedByCName = requireArguments().getString(ISSUED_BY_CNAME)
115         val issuedByOName = requireArguments().getString(ISSUED_BY_ONAME)
116         val issuedByUName = requireArguments().getString(ISSUED_BY_UNAME)
117         val startDate = requireArguments().getString(START_DATE)
118         val endDate = requireArguments().getString(END_DATE)
119         val webViewFragmentId = requireArguments().getLong(WEBVIEW_FRAGMENT_ID)
120
121         // Get the current position of this WebView fragment.
122         val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId)
123
124         // Get the WebView tab fragment.
125         val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
126
127         // Get the fragment view.
128         val fragmentView = webViewTabFragment.requireView()
129
130         // Get a handle for the current WebView.
131         val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
132
133         // Get a handle for the SSL error handler.
134         val sslErrorHandler = nestedScrollWebView.sslErrorHandler
135
136         // Use an alert dialog builder to create the alert dialog.
137         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
138
139         // Get the current theme status.
140         val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
141
142         // Set the icon according to the theme.
143         dialogBuilder.setIconAttribute(R.attr.sslCertificateBlueIcon)
144
145         // Set the title.
146         dialogBuilder.setTitle(R.string.ssl_certificate_error)
147
148         // Set the view.
149         dialogBuilder.setView(R.layout.ssl_certificate_error)
150
151         // Set the cancel button listener.
152         dialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int ->
153             // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
154             if (sslErrorHandler != null) {
155                 // Cancel the request.
156                 sslErrorHandler.cancel()
157
158                 // Reset the SSL error handler.
159                 nestedScrollWebView.resetSslErrorHandler()
160             }
161         }
162
163         // Set the proceed button listener.
164         dialogBuilder.setPositiveButton(R.string.proceed) { _: DialogInterface?, _: Int ->
165             // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
166             if (sslErrorHandler != null) {
167                 // Proceed to the website.
168                 sslErrorHandler.proceed()
169
170                 // Reset the SSL error handler.
171                 nestedScrollWebView.resetSslErrorHandler()
172             }
173         }
174
175         // Create an alert dialog from the builder.
176         val alertDialog = dialogBuilder.create()
177
178         // Get a handle for the shared preferences.
179         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
180
181         // Get the screenshot preference.
182         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
183
184         // Disable screenshots if not allowed.
185         if (!allowScreenshots) {
186             // Disable screenshots.
187             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
188         }
189
190         // Get a URI for the URL with errors.
191         val uriWithErrors = Uri.parse(urlWithErrors)
192
193         // Get the IP addresses for the URI.
194         GetIpAddresses(requireActivity(), alertDialog).execute(uriWithErrors.host)
195
196         // The alert dialog must be shown before the contents can be modified.
197         alertDialog.show()
198
199         // Get handles for the views.
200         val primaryErrorTextView = alertDialog.findViewById<TextView>(R.id.primary_error)!!
201         val urlTextView = alertDialog.findViewById<TextView>(R.id.url)!!
202         val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
203         val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
204         val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
205         val issuedByTextView = alertDialog.findViewById<TextView>(R.id.issued_by_textview)!!
206         val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
207         val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
208         val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
209         val validDatesTextView = alertDialog.findViewById<TextView>(R.id.valid_dates_textview)!!
210         val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
211         val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
212
213         // Setup the common strings.
214         val urlLabel = getString(R.string.url_label) + "  "
215         val cNameLabel = getString(R.string.common_name) + "  "
216         val oNameLabel = getString(R.string.organization) + "  "
217         val uNameLabel = getString(R.string.organizational_unit) + "  "
218         val startDateLabel = getString(R.string.start_date) + "  "
219         val endDateLabel = getString(R.string.end_date) + "  "
220
221         // Create a spannable string builder for each text view that needs multiple colors of text.
222         val urlStringBuilder = SpannableStringBuilder(urlLabel + urlWithErrors)
223         val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
224         val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
225         val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
226         val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
227         val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
228         val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
229         val startDateStringBuilder = SpannableStringBuilder(startDateLabel + startDate)
230         val endDateStringBuilder = SpannableStringBuilder(endDateLabel + endDate)
231
232         // Define the color spans.
233         val blueColorSpan: ForegroundColorSpan
234         val redColorSpan: ForegroundColorSpan
235
236         // Set the color spans according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
237         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
238             @Suppress("DEPRECATION")
239             blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
240             @Suppress("DEPRECATION")
241             redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
242         } else {
243             @Suppress("DEPRECATION")
244             blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
245             @Suppress("DEPRECATION")
246             redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
247         }
248
249         // Setup the spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
250         urlStringBuilder.setSpan(blueColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
251         issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
252         issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
253         issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
254         issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
255         issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
256         issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
257         startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
258         endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
259
260         // Define the primary error string.
261         var primaryErrorString = ""
262
263         // Highlight the primary error in red and store it in the primary error string.
264         when (primaryErrorInt) {
265             SslError.SSL_IDMISMATCH -> {
266                 // Change the URL span colors to red.
267                 urlStringBuilder.setSpan(redColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
268                 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
269
270                 // Store the primary error string.
271                 primaryErrorString = getString(R.string.cn_mismatch)
272             }
273
274             SslError.SSL_UNTRUSTED -> {
275                 // Change the issued by text view text to red.  The deprecated `getColor()` must be used until the minimum API >= 23.
276                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
277                     @Suppress("DEPRECATION")
278                     issuedByTextView.setTextColor(resources.getColor(R.color.red_a700))
279                 } else {
280                     @Suppress("DEPRECATION")
281                     issuedByTextView.setTextColor(resources.getColor(R.color.red_900))
282                 }
283
284                 // Change the issued by span color to red.
285                 issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
286                 issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
287                 issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
288
289                 // Store the primary error string.
290                 primaryErrorString = getString(R.string.untrusted)
291             }
292
293             SslError.SSL_DATE_INVALID -> {
294                 // Change the valid dates text view text to red.  The deprecated `getColor()` must be used until the minimum API >= 23.
295                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
296                     @Suppress("DEPRECATION")
297                     validDatesTextView.setTextColor(resources.getColor(R.color.red_a700))
298                 } else {
299                     @Suppress("DEPRECATION")
300                     validDatesTextView.setTextColor(resources.getColor(R.color.red_900))
301                 }
302
303                 // Change the date span colors to red.
304                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
305                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
306
307                 // Store the primary error string.
308                 primaryErrorString = getString(R.string.invalid_date)
309             }
310
311             SslError.SSL_NOTYETVALID -> {
312                 // Change the start date span color to red.
313                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
314
315                 // Store the primary error string.
316                 primaryErrorString = getString(R.string.future_certificate)
317             }
318
319             SslError.SSL_EXPIRED -> {
320                 // Change the end date span color to red.
321                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
322
323                 // Store the primary error string.
324                 primaryErrorString = getString(R.string.expired_certificate)
325             }
326
327             SslError.SSL_INVALID ->
328                 // Store the primary error string.
329                 primaryErrorString = getString(R.string.invalid_certificate)
330         }
331
332         // Display the strings.
333         primaryErrorTextView.text = primaryErrorString
334         urlTextView.text = urlStringBuilder
335         issuedToCNameTextView.text = issuedToCNameStringBuilder
336         issuedToONameTextView.text = issuedToONameStringBuilder
337         issuedToUNameTextView.text = issuedToUNameStringBuilder
338         issuedByCNameTextView.text = issuedByCNameStringBuilder
339         issuedByONameTextView.text = issuedByONameStringBuilder
340         issuedByUNameTextView.text = issuedByUNameStringBuilder
341         startDateTextView.text = startDateStringBuilder
342         endDateTextView.text = endDateStringBuilder
343
344         // Return the alert dialog.
345         return alertDialog
346     }
347
348     // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `SpannableStringBuilder` contains the results.
349     private class GetIpAddresses constructor(activity: Activity, alertDialog: AlertDialog) : AsyncTask<String, Void?, SpannableStringBuilder>() {
350         // Define the weak references.
351         private val activityWeakReference: WeakReference<Activity> = WeakReference(activity)
352         private val alertDialogWeakReference: WeakReference<AlertDialog> = WeakReference(alertDialog)
353
354         override fun doInBackground(vararg domainName: String): SpannableStringBuilder {
355             // Get handles for the activity and the alert dialog.
356             val activity = activityWeakReference.get()
357             val alertDialog = alertDialogWeakReference.get()
358
359             // Abort if the activity or the dialog is gone.
360             if (activity == null || activity.isFinishing || alertDialog == null) {
361                 return SpannableStringBuilder()
362             }
363
364             // Initialize an IP address string builder.
365             val ipAddresses = StringBuilder()
366
367             // Get an array with the IP addresses for the host.
368             try {
369                 // Get an array with all the IP addresses for the domain.
370                 val inetAddressesArray = InetAddress.getAllByName(domainName[0])
371
372                 // Add each IP address to the string builder.
373                 for (inetAddress in inetAddressesArray) {
374                     // Check to see if this is not the first IP address.
375                     if (ipAddresses.isNotEmpty()) {
376                         // Add a line break to the string builder first.
377                         ipAddresses.append("\n")
378                     }
379
380                     // Add the IP Address to the string builder.
381                     ipAddresses.append(inetAddress.hostAddress)
382                 }
383             } catch (exception: UnknownHostException) {
384                 // Do nothing.
385             }
386
387             // Set the label.
388             val ipAddressesLabel = activity.getString(R.string.ip_addresses) + "  "
389
390             // Create a spannable string builder.
391             val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
392
393             // Create a blue foreground color span.
394             val blueColorSpan: ForegroundColorSpan
395
396             // Get the current theme status.
397             val currentThemeStatus = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
398
399             // Set the blue color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
400             blueColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
401                 @Suppress("DEPRECATION")
402                 ForegroundColorSpan(activity.resources.getColor(R.color.blue_700))
403             } else {
404                 @Suppress("DEPRECATION")
405                 ForegroundColorSpan(activity.resources.getColor(R.color.violet_500))
406             }
407
408             // Set the string builder to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
409             ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
410
411             // Return the formatted string.
412             return ipAddressesStringBuilder
413         }
414
415         // `onPostExecute()` operates on the UI thread.
416         override fun onPostExecute(ipAddresses: SpannableStringBuilder) {
417             // Get handles for the activity and the alert dialog.
418             val activity = activityWeakReference.get()
419             val alertDialog = alertDialogWeakReference.get()
420
421             // Abort if the activity or the alert dialog is gone.
422             if (activity == null || activity.isFinishing || alertDialog == null) {
423                 return
424             }
425
426             // Get a handle for the IP addresses text view.
427             val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
428
429             // Populate the IP addresses text view.
430             ipAddressesTextView.text = ipAddresses
431         }
432     }
433 }