2 /* SPDX-License-Identifier: GPL-3.0-or-later
3 * SPDX-FileCopyrightText: 2018-2019, 2021-2023, 2025-2026 Soren Stoutner <soren@stoutner.com>
5 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
7 * This program is free software: you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License as published by the Free Software
9 * Foundation, either version 3 of the License, or (at your option) any later
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * You should have received a copy of the GNU General Public License along with
18 * this program. If not, see <https://www.gnu.org/licenses/>.
21 package com.stoutner.privacybrowser.helpers
23 import android.util.Log
24 import com.stoutner.privacybrowser.dataclasses.FilterListDataClass
25 import com.stoutner.privacybrowser.dataclasses.FilterListEntryDataClass
26 import com.stoutner.privacybrowser.dataclasses.FilterOptionDisposition
27 import com.stoutner.privacybrowser.dataclasses.MatchedUrlType
28 import com.stoutner.privacybrowser.dataclasses.RequestDataClass
29 import com.stoutner.privacybrowser.dataclasses.RequestDisposition
30 import com.stoutner.privacybrowser.dataclasses.Sublist
32 import java.util.regex.Pattern
35 // The public filter list data classes.
36 var easyListDataClass = FilterListDataClass()
37 var easyPrivacyDataClass = FilterListDataClass()
38 var fanboysAnnoyanceDataClass: FilterListDataClass? = null // This is nullable so that the app can check to see if it has been restarted.
39 var ultraListDataClass = FilterListDataClass()
40 var ultraPrivacyDataClass = FilterListDataClass()
42 class CheckFilterListHelper {
43 fun checkFilterList(requestDataClass: RequestDataClass, filterListDataClass: FilterListDataClass): Boolean {
44 // Create a continue checking tracker. If the tracker changes to false, all further checking of the request will be bypassed.
45 var continueChecking = true
47 // Check the main allow list.
48 for (filterListEntryDataClass in filterListDataClass.mainAllowList) {
49 // Check the applied entry against the request URL.
50 continueChecking = checkAppliedEntry(requestDataClass, MatchedUrlType.Url, filterListEntryDataClass)
52 // Check the applied entry against the request URL with separators.
54 continueChecking = checkAppliedEntry(requestDataClass, MatchedUrlType.UrlWithSeparators, filterListEntryDataClass)
56 // Exit the loop if checking should no longer be continued.
57 if (!continueChecking)
61 // Check the initial domain allow list unless a match has already been found.
62 if (continueChecking) {
63 // Check the initial domain allow list.
64 for (filterListEntryDataClass in filterListDataClass.initialDomainAllowList) {
65 // Check the applied entry against the truncated URL.
66 continueChecking = checkAppliedEntry(requestDataClass, MatchedUrlType.TruncatedUrl, filterListEntryDataClass)
68 //Check the applied entry against the truncated URL with separators.
70 continueChecking = checkAppliedEntry(requestDataClass, MatchedUrlType.TruncatedUrlWithSeparators, filterListEntryDataClass)
72 // Exit the loop if checking should no longer be continued.
73 if (!continueChecking)
78 // Check the regular expression allow list unless a match has already been found.
79 if (continueChecking) {
80 // Check the regular expression allow list.
81 for (filterListEntryDataClass in filterListDataClass.regularExpressionAllowList) {
82 // Check the applied entries.
83 continueChecking = checkRegularExpression(requestDataClass, filterListEntryDataClass)
85 // Exit the loop if checking should no longer be continued.
86 if (!continueChecking)
91 // Check the main block list unless a match has already been found.
92 if (continueChecking) {
93 // Check the main block list.
94 for (filterListEntryDataClass in filterListDataClass.mainBlockList) {
95 // Check the applied entry against the request URL.
96 continueChecking = checkAppliedEntry(requestDataClass, MatchedUrlType.Url, filterListEntryDataClass)
98 // Check the applied entry against the request URL with separators.
100 continueChecking = checkAppliedEntry(requestDataClass, MatchedUrlType.UrlWithSeparators, filterListEntryDataClass)
102 // Exit the loop if checking should no longer be continued.
103 if (!continueChecking)
108 // Check the initial domain block list unless a match has already been found.
109 if (continueChecking) {
110 // Check the initial domain block list.
111 for (filterListEntryDataClass in filterListDataClass.initialDomainBlockList) {
112 // Check the applied entry against the truncated URL.
113 continueChecking = checkAppliedEntry(requestDataClass, MatchedUrlType.TruncatedUrl, filterListEntryDataClass)
115 // Check the applied entry against the truncated URL with separators.
116 if (continueChecking)
117 continueChecking = checkAppliedEntry(requestDataClass, MatchedUrlType.TruncatedUrlWithSeparators, filterListEntryDataClass)
119 // Exit the loop if checking should no longer be continued.
120 if (!continueChecking)
125 // Check the regular expression block list unless a match has already been found.
126 if (continueChecking) {
127 // Check the regular expression block list.
128 for (filterListEntryDataClass in filterListDataClass.regularExpressionBlockList) {
129 // Check the applied entries.
130 continueChecking = checkRegularExpression(requestDataClass, filterListEntryDataClass)
132 // Exit the loop if checking should no longer be continued.
133 if (!continueChecking)
138 // Return the continue checking status.
139 return continueChecking
142 private fun checkAppliedEntry(requestDataClass : RequestDataClass, matchedUrlType : MatchedUrlType, filterListEntryDataClass : FilterListEntryDataClass) : Boolean {
143 // Populate the URL string according to the matched URL type.
144 var urlString = when (matchedUrlType) {
145 MatchedUrlType.Url -> requestDataClass.requestUrlString
146 MatchedUrlType.UrlWithSeparators -> requestDataClass.requestUrlWithSeparatorsString
147 MatchedUrlType.TruncatedUrl -> requestDataClass.truncatedRequestUrlString
148 MatchedUrlType.TruncatedUrlWithSeparators -> requestDataClass.truncatedRequestUrlWithSeparatorsString
151 // Check the entries according to the number.
152 if (filterListEntryDataClass.singleAppliedEntry) { // There is a single entry.
153 // Process the initial and final matches.
154 if (filterListEntryDataClass.initialMatch && filterListEntryDataClass.finalMatch) { // This is both an initial and final match.
155 // Check the URL against the applied entry.
156 if (urlString == filterListEntryDataClass.appliedEntryList[0]) {
157 // Set the matched URL type.
158 requestDataClass.matchedUrlType = matchedUrlType
160 // Check the domain status.
161 return checkDomain(requestDataClass, filterListEntryDataClass)
163 } else if (filterListEntryDataClass.initialMatch) { // This is an initial match.
164 // Check the URL against the applied entry.
165 if (urlString.startsWith(filterListEntryDataClass.appliedEntryList[0])) {
166 // Set the matched URL type.
167 requestDataClass.matchedUrlType = matchedUrlType
169 // Check the domain status.
170 return checkDomain(requestDataClass, filterListEntryDataClass)
172 } else if (filterListEntryDataClass.finalMatch) { // This is a final match.
173 // Check the URL against the applied entry.
174 if (urlString.endsWith(filterListEntryDataClass.appliedEntryList[0])) {
175 // Set the matched URL type.
176 requestDataClass.matchedUrlType = matchedUrlType
178 // Check the domain status.
179 return checkDomain(requestDataClass, filterListEntryDataClass)
181 } else { // There is no initial or final matching.
182 // Check if the URL string contains the applied entry.
183 if (urlString.contains(filterListEntryDataClass.appliedEntryList[0])) {
184 // Set the matches URL type.
185 requestDataClass.matchedUrlType = matchedUrlType
187 return checkDomain(requestDataClass, filterListEntryDataClass)
190 } else { // There are multiple entries.
191 // Create a URL matches flag.
192 var urlMatches = true
194 // Process initial and final matches.
195 if (filterListEntryDataClass.initialMatch && filterListEntryDataClass.finalMatch) { // This is both an initial and final match.
197 if (urlString.startsWith(filterListEntryDataClass.appliedEntryList[0])) {
198 // Get the number of characters to remove from the front of the URL string.
199 val charactersToRemove = filterListEntryDataClass.appliedEntryList[0].length
201 // Remove the entry from the front of the modified URL string.
202 urlString = urlString.substring(charactersToRemove)
204 // Get the entry locations.
205 val penultimateEntryNumber = (filterListEntryDataClass.sizeOfAppliedEntryList - 1)
206 val ultimateEntryIndex: Int = penultimateEntryNumber
208 // Check all the middle entries.
209 for (i in 0 until penultimateEntryNumber) {
210 // Get the applied entry index, which will be `-1` if it doesn't exist.
211 val appliedEntryIndex = urlString.indexOf(filterListEntryDataClass.appliedEntryList[i])
213 // Check if the entry was found.
214 if (appliedEntryIndex >= 0) { // The entry is contained in the URL string.
215 // Get the number of characters to remove from teh from of the URL string.
216 val charactersToRemove = (appliedEntryIndex + filterListEntryDataClass.appliedEntryList[i].length)
218 // Remove the entry from the fron of the modified URL string.
219 urlString = urlString.substring(charactersToRemove)
220 } else { // The entry is not contained in the URL string.
221 // Mark the URL matches flag as false.
226 // Check the final entry if the URL still matches.
228 // Check if the URL string ends with the last applied entry.
229 if (urlString.endsWith(filterListEntryDataClass.appliedEntryList[ultimateEntryIndex])) {
230 // Set the matched URL type.
231 requestDataClass.matchedUrlType = matchedUrlType
233 // Check the domain status.
234 return checkDomain(requestDataClass, filterListEntryDataClass)
238 } else if (filterListEntryDataClass.initialMatch) { // This is an initial match.
240 if (urlString.startsWith(filterListEntryDataClass.appliedEntryList[0])) {
241 // Get the number of characters to remove from the front of the URL string.
242 val charactersToRemove = filterListEntryDataClass.appliedEntryList[0].length
244 // Remove the entry from the front of the modified URL string.
245 urlString = urlString.substring(charactersToRemove)
247 // Check the rest of the entries.
248 for (i in 1 until filterListEntryDataClass.sizeOfAppliedEntryList) {
249 // Get the applied entry index, which will be `-1` if it doesn't exist.
250 val appliedEntryIndex = urlString.indexOf(filterListEntryDataClass.appliedEntryList[i])
252 // Check if the entry was found.
253 if (appliedEntryIndex >= 0) { // The entry is contained in the URL string.
254 // Get the number of characters to remove from the front of the URL string.
255 val charactersToRemove = appliedEntryIndex + filterListEntryDataClass.appliedEntryList[i].length
257 // Remove the entry from the front of the modified URL string.
258 urlString = urlString.substring(charactersToRemove)
259 } else { // The entry is not contained in the URL string.
260 // Mark the URL matches flag as false.
265 // Check the domain status if the URL still matches.
267 // Set the matched URL type.
268 requestDataClass.matchedUrlType = matchedUrlType
270 // Check the domain status.
271 return checkDomain(requestDataClass, filterListEntryDataClass)
274 } else if (filterListEntryDataClass.finalMatch) { // This is a final match.
275 // Get the entry locations.
276 val penultimateEntryNumber= (filterListEntryDataClass.sizeOfAppliedEntryList - 1)
277 val ultimateEntryIndex: Int = penultimateEntryNumber
279 // Check all the entries except the last one.
280 for (i in 0 until penultimateEntryNumber - 1) {
281 // Get the applied entry index, which will be `-1` if it doesn't exist.
282 val appliedEntryIndex =urlString.indexOf(filterListEntryDataClass.appliedEntryList[i])
284 // Check if the entry was found.
285 if (appliedEntryIndex >= 0) { // The entry is contained in the URL string.
286 // Get the number of characters to remove from teh front of the URL string.
287 val charactersToRemove = (appliedEntryIndex + filterListEntryDataClass.appliedEntryList[i].length)
289 // Remove the entry from the front of the modified URL string.
290 urlString = urlString.substring(charactersToRemove)
291 } else { // The entry is not contained in the URL string.
292 // Mark the URL matches flag as false.
297 // Check the final entry if the URL still matches.
299 // Check if the URL string ends with the last applied entry.
300 if (urlString.endsWith(filterListEntryDataClass.appliedEntryList[ultimateEntryIndex])) {
301 // Set the matched URL type.
302 requestDataClass.matchedUrlType = matchedUrlType
304 // Check the domain status.
305 return checkDomain(requestDataClass, filterListEntryDataClass)
308 } else { // There is no initial or final matching.
310 for (i in 0 until filterListEntryDataClass.sizeOfAppliedEntryList) {
311 // Get the applied entry index, which will be `-1` if it doesn't exist.
312 val appliedEntryIndex = urlString.indexOf(filterListEntryDataClass.appliedEntryList[i])
314 // Check if the entry was found.
315 if (appliedEntryIndex >= 0) { // The entry is contained in the URL string.
316 // Get the number of characters to remove from the front of the URL string.
317 val charactersToRemove = (appliedEntryIndex + filterListEntryDataClass.appliedEntryList[i].length)
319 // Remove the entry from the front of the modified URL string.
320 urlString = urlString.substring(charactersToRemove)
321 } else { // The entry is not contained in the URL string.
322 // Mark the URL matches flag as false.
327 // Check the domain status if the URL still matches.
329 // Set the matched URL type.
330 requestDataClass.matchedUrlType = matchedUrlType
332 // Check the domain status.
333 return checkDomain(requestDataClass, filterListEntryDataClass)
338 // The applied entry doesn't match. Return `true` to continue processing the URL request.
342 private fun checkRegularExpression(requestDataClass : RequestDataClass, filterListEntryDataClass : FilterListEntryDataClass) : Boolean {
343 // Create an applied entry regular expression pattern.
344 val appliedEntryRegularExpressionPattern = Pattern.compile(filterListEntryDataClass.appliedEntryList[0])
346 // Check if the regular expression matches the URL string.
347 val regularExpressionMatcher = appliedEntryRegularExpressionPattern.matcher(requestDataClass.requestUrlString)
349 // Check the domain status if the regular expression matches.
350 return if (regularExpressionMatcher.matches())
351 checkDomain(requestDataClass, filterListEntryDataClass)
352 else // If the regular expression doesn't match, return `true` to continue processing the URL request.
356 private fun checkDomain(requestDataClass : RequestDataClass, filterListEntryDataClass : FilterListEntryDataClass) : Boolean {
357 // Check the domain status.
358 if (filterListEntryDataClass.domain == FilterOptionDisposition.Null) { // Ignore the domain status.
359 // Check the third party status.
360 return checkThirdParty(requestDataClass, filterListEntryDataClass)
361 } else if (filterListEntryDataClass.domain == FilterOptionDisposition.Apply) { // Block the enumerated domains.
362 // Check each domain.
363 for (blockedDomain in filterListEntryDataClass.domainList) {
364 // Check if the request came from a blocked domain.
365 if (requestDataClass.firstPartyHostString.endsWith(blockedDomain)) {
366 // Check the third-party status.
367 return checkThirdParty(requestDataClass, filterListEntryDataClass)
370 } else if (filterListEntryDataClass.domain == FilterOptionDisposition.Override) { // Block domains that are not overridden.
371 // Create a block domain flag.
372 var blockDomain = true
374 // Check each overridden domain.
375 for (overriddenDomain in filterListEntryDataClass.domainList) {
376 // Check if the request came from an overridden domain.
377 if (requestDataClass.firstPartyHostString.endsWith(overriddenDomain)) {
378 // Don't block the domain.
383 // Continue checking if the domain is blocked.
385 // Check the third-party status.
386 return checkThirdParty(requestDataClass, filterListEntryDataClass)
390 // There is a domain specified that doesn't match this request. Return `true` to continue processing the URL request.
394 private fun checkThirdParty(requestDataClass : RequestDataClass, filterListEntryDataClass : FilterListEntryDataClass) : Boolean {
395 // Check the third-party status.
396 return if ((filterListEntryDataClass.thirdParty == FilterOptionDisposition.Null) || // Ignore the third-party status.
397 ((filterListEntryDataClass.thirdParty == FilterOptionDisposition.Apply) && requestDataClass.isThirdPartyRequest) || // Block third-party requests.
398 ((filterListEntryDataClass.thirdParty == FilterOptionDisposition.Override) && !requestDataClass.isThirdPartyRequest)) // Block first-party requests.
400 // Process the request.
401 processRequest(requestDataClass, filterListEntryDataClass)
402 } else { // The third-party option specified doesn't match this request. Return `true` to continue processing the URL request.
407 private fun processRequest(requestDataClass : RequestDataClass, filterListEntryDataClass : FilterListEntryDataClass) : Boolean {
408 // Populate the filter list and sublist.
409 requestDataClass.filterList = filterListEntryDataClass.filterList
410 requestDataClass.sublist = filterListEntryDataClass.sublist
412 Log.i("FilterList", "Filter List: ${filterListEntryDataClass.filterList}")
414 // Set the request disposition.
415 when (requestDataClass.sublist) {
416 Sublist.MainAllowList -> requestDataClass.disposition = RequestDisposition.Allowed
417 Sublist.InitialDomainAllowList -> requestDataClass.disposition = RequestDisposition.Allowed
418 Sublist.RegularExpressionAllowList -> requestDataClass.disposition = RequestDisposition.Allowed
419 Sublist.MainBlockList -> requestDataClass.disposition = RequestDisposition.Blocked
420 Sublist.InitialDomainBlockList -> requestDataClass.disposition = RequestDisposition.Blocked
421 Sublist.RegularExpressionBlockList -> requestDataClass.disposition = RequestDisposition.Blocked
424 // Populate the filter list entry data class items.
425 requestDataClass.filterListOriginalEntryString = filterListEntryDataClass.originalEntryString
426 requestDataClass.filterListOriginalFilterOptionsString = filterListEntryDataClass.originalFilterOptionsString
427 requestDataClass.filterListAppliedEntryList = filterListEntryDataClass.appliedEntryList
428 requestDataClass.filterListAppliedFilterOptionsList = filterListEntryDataClass.appliedFilterOptionsList
429 requestDataClass.filterListDomainList = filterListEntryDataClass.domainList
430 requestDataClass.filterListFinalMatch = filterListEntryDataClass.finalMatch
431 requestDataClass.filterListInitialMatch = filterListEntryDataClass.initialMatch
432 requestDataClass.filterListSingleAppliedEntry = filterListEntryDataClass.singleAppliedEntry
433 requestDataClass.filterListSizeOfAppliedEntryList = filterListEntryDataClass.sizeOfAppliedEntryList
434 requestDataClass.filterListDomain = filterListEntryDataClass.domain
435 requestDataClass.filterListThirdParty = filterListEntryDataClass.thirdParty
437 // Log the disposition.
438 //Log.i("Request processed", "${requestDataClass.requestUrlString} - Filter list entry: ${requestDataClass.filterListAppliedEntryList}")
440 // Returning `false` stops all processing of the request.