better url risk scorer, icon, language, views reduced.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
package com.clean.scanner
|
||||
package de.softwareapp_hb.privateqrscanner
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.clean.scanner.data.local.CleanScannerDatabase
|
||||
import com.clean.scanner.data.repository.ScanRepository
|
||||
import com.clean.scanner.settings.SettingsRepository
|
||||
import de.softwareapp_hb.privateqrscanner.data.local.CleanScannerDatabase
|
||||
import de.softwareapp_hb.privateqrscanner.data.repository.ScanRepository
|
||||
import de.softwareapp_hb.privateqrscanner.settings.SettingsRepository
|
||||
|
||||
class AppContainer(context: Context) {
|
||||
private val appContext = context.applicationContext
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner
|
||||
package de.softwareapp_hb.privateqrscanner
|
||||
|
||||
import android.app.Application
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.clean.scanner
|
||||
package de.softwareapp_hb.privateqrscanner
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import com.clean.scanner.ui.CleanScannerAppRoot
|
||||
import com.clean.scanner.ui.theme.CleanScannerTheme
|
||||
import de.softwareapp_hb.privateqrscanner.ui.CleanScannerAppRoot
|
||||
import de.softwareapp_hb.privateqrscanner.ui.theme.CleanScannerTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.data.local
|
||||
package de.softwareapp_hb.privateqrscanner.data.local
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.data.local
|
||||
package de.softwareapp_hb.privateqrscanner.data.local
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.data.local
|
||||
package de.softwareapp_hb.privateqrscanner.data.local
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.clean.scanner.data.repository
|
||||
package de.softwareapp_hb.privateqrscanner.data.repository
|
||||
|
||||
import com.clean.scanner.data.local.ScanDao
|
||||
import com.clean.scanner.data.local.ScanEntity
|
||||
import com.clean.scanner.domain.ScanRecord
|
||||
import com.clean.scanner.settings.SettingsRepository
|
||||
import de.softwareapp_hb.privateqrscanner.data.local.ScanDao
|
||||
import de.softwareapp_hb.privateqrscanner.data.local.ScanEntity
|
||||
import de.softwareapp_hb.privateqrscanner.domain.ScanRecord
|
||||
import de.softwareapp_hb.privateqrscanner.settings.SettingsRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.clean.scanner.data.scanner
|
||||
package de.softwareapp_hb.privateqrscanner.data.scanner
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.SystemClock
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import com.clean.scanner.domain.ScanResult
|
||||
import com.clean.scanner.util.readablePayload
|
||||
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
||||
import de.softwareapp_hb.privateqrscanner.util.readablePayload
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.domain
|
||||
package de.softwareapp_hb.privateqrscanner.domain
|
||||
|
||||
data class ScanRecord(
|
||||
val id: Long,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.domain
|
||||
package de.softwareapp_hb.privateqrscanner.domain
|
||||
|
||||
data class ScanResult(
|
||||
val content: String,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.domain
|
||||
package de.softwareapp_hb.privateqrscanner.domain
|
||||
|
||||
data class UrlRiskResult(
|
||||
val score: Int,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.clean.scanner.settings
|
||||
package de.softwareapp_hb.privateqrscanner.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.clean.scanner.ui.UseCaseView
|
||||
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.clean.scanner.ui
|
||||
package de.softwareapp_hb.privateqrscanner.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.clean.scanner.AppContainer
|
||||
import com.clean.scanner.domain.ScanRecord
|
||||
import de.softwareapp_hb.privateqrscanner.AppContainer
|
||||
import de.softwareapp_hb.privateqrscanner.domain.ScanRecord
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui
|
||||
package de.softwareapp_hb.privateqrscanner.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -15,11 +15,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.clean.scanner.AppContainer
|
||||
import com.clean.scanner.R
|
||||
import com.clean.scanner.ui.screens.HistoryScreen
|
||||
import com.clean.scanner.ui.screens.ScannerScreen
|
||||
import com.clean.scanner.ui.screens.SettingsScreen
|
||||
import de.softwareapp_hb.privateqrscanner.AppContainer
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.ui.screens.HistoryScreen
|
||||
import de.softwareapp_hb.privateqrscanner.ui.screens.ScannerScreen
|
||||
import de.softwareapp_hb.privateqrscanner.ui.screens.SettingsScreen
|
||||
|
||||
private enum class RootTab { Scanner, History, Settings }
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.clean.scanner.ui
|
||||
package de.softwareapp_hb.privateqrscanner.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.clean.scanner.AppContainer
|
||||
import com.clean.scanner.domain.ScanResult
|
||||
import de.softwareapp_hb.privateqrscanner.AppContainer
|
||||
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.clean.scanner.ui
|
||||
package de.softwareapp_hb.privateqrscanner.ui
|
||||
|
||||
import com.clean.scanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
|
||||
enum class UseCaseView(
|
||||
val storageKey: String,
|
||||
@@ -13,38 +13,6 @@ enum class UseCaseView(
|
||||
EventTicketing(
|
||||
storageKey = "event_ticketing",
|
||||
titleRes = R.string.use_case_event_ticketing
|
||||
),
|
||||
InventoryOperations(
|
||||
storageKey = "inventory_operations",
|
||||
titleRes = R.string.use_case_inventory_operations
|
||||
),
|
||||
FieldWorkServiceTeams(
|
||||
storageKey = "field_work_service_teams",
|
||||
titleRes = R.string.use_case_field_work
|
||||
),
|
||||
OfficeAdmin(
|
||||
storageKey = "office_admin",
|
||||
titleRes = R.string.use_case_office_admin
|
||||
),
|
||||
CommunicationShortcuts(
|
||||
storageKey = "communication_shortcuts",
|
||||
titleRes = R.string.use_case_communication_shortcuts
|
||||
),
|
||||
SecurityBrowsing(
|
||||
storageKey = "security_browsing",
|
||||
titleRes = R.string.use_case_security_browsing
|
||||
),
|
||||
OfflineLowConnectivity(
|
||||
storageKey = "offline_low_connectivity",
|
||||
titleRes = R.string.use_case_offline_low_connectivity
|
||||
),
|
||||
AccessibilitySpeed(
|
||||
storageKey = "accessibility_speed",
|
||||
titleRes = R.string.use_case_accessibility_speed
|
||||
),
|
||||
TeamHandoverTransfer(
|
||||
storageKey = "team_handover_transfer",
|
||||
titleRes = R.string.use_case_team_handover_transfer
|
||||
);
|
||||
|
||||
companion object {
|
||||
@@ -90,85 +58,5 @@ fun UseCaseView.capabilities(): UseCaseCapabilities {
|
||||
allowOpenUrl = false,
|
||||
allowBatchShare = true
|
||||
)
|
||||
|
||||
UseCaseView.InventoryOperations -> UseCaseCapabilities(
|
||||
allowScanFromImage = true,
|
||||
allowBatchMode = true,
|
||||
allowCopy = true,
|
||||
allowShare = true,
|
||||
allowOpenUrl = false,
|
||||
allowHistoryExport = true,
|
||||
allowBatchShare = true
|
||||
)
|
||||
|
||||
UseCaseView.FieldWorkServiceTeams -> UseCaseCapabilities(
|
||||
allowScanFromImage = true,
|
||||
allowBatchMode = true,
|
||||
allowCopy = true,
|
||||
allowShare = true,
|
||||
allowOpenUrl = true,
|
||||
allowHistoryExport = true,
|
||||
allowBatchShare = true
|
||||
)
|
||||
|
||||
UseCaseView.OfficeAdmin -> UseCaseCapabilities(
|
||||
allowScanFromImage = true,
|
||||
allowBatchMode = false,
|
||||
allowCopy = true,
|
||||
allowShare = true,
|
||||
allowOpenUrl = true,
|
||||
allowAddContact = true,
|
||||
allowDialPhone = true,
|
||||
allowSendSms = true,
|
||||
allowSendEmail = true,
|
||||
allowOpenWifiSettings = true,
|
||||
allowAddCalendarEvent = true
|
||||
)
|
||||
|
||||
UseCaseView.CommunicationShortcuts -> UseCaseCapabilities(
|
||||
allowScanFromImage = true,
|
||||
allowBatchMode = false,
|
||||
allowCopy = true,
|
||||
allowShare = true,
|
||||
allowOpenUrl = false,
|
||||
allowDialPhone = true,
|
||||
allowSendSms = true,
|
||||
allowSendEmail = true
|
||||
)
|
||||
|
||||
UseCaseView.SecurityBrowsing -> UseCaseCapabilities(
|
||||
allowScanFromImage = true,
|
||||
allowBatchMode = false,
|
||||
allowCopy = true,
|
||||
allowShare = true,
|
||||
allowOpenUrl = true
|
||||
)
|
||||
|
||||
UseCaseView.OfflineLowConnectivity -> UseCaseCapabilities(
|
||||
allowScanFromImage = true,
|
||||
allowBatchMode = true,
|
||||
allowCopy = true,
|
||||
allowShare = true,
|
||||
allowOpenUrl = false,
|
||||
allowBatchShare = true
|
||||
)
|
||||
|
||||
UseCaseView.AccessibilitySpeed -> UseCaseCapabilities(
|
||||
allowScanFromImage = true,
|
||||
allowBatchMode = false,
|
||||
allowCopy = true,
|
||||
allowShare = true,
|
||||
allowOpenUrl = true
|
||||
)
|
||||
|
||||
UseCaseView.TeamHandoverTransfer -> UseCaseCapabilities(
|
||||
allowScanFromImage = true,
|
||||
allowBatchMode = true,
|
||||
allowCopy = true,
|
||||
allowShare = true,
|
||||
allowOpenUrl = false,
|
||||
allowHistoryExport = true,
|
||||
allowBatchShare = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.components
|
||||
package de.softwareapp_hb.privateqrscanner.ui.components
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Size
|
||||
@@ -22,8 +22,8 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.clean.scanner.data.scanner.DetectionBox
|
||||
import com.clean.scanner.data.scanner.MlKitBarcodeAnalyzer
|
||||
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionBox
|
||||
import de.softwareapp_hb.privateqrscanner.data.scanner.MlKitBarcodeAnalyzer
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.screens
|
||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.screens
|
||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -24,12 +24,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.clean.scanner.R
|
||||
import com.clean.scanner.domain.ScanRecord
|
||||
import com.clean.scanner.ui.UseCaseView
|
||||
import com.clean.scanner.ui.capabilities
|
||||
import com.clean.scanner.util.HistoryExportFormatter
|
||||
import com.clean.scanner.util.Intents
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.domain.ScanRecord
|
||||
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
||||
import de.softwareapp_hb.privateqrscanner.ui.capabilities
|
||||
import de.softwareapp_hb.privateqrscanner.util.HistoryExportFormatter
|
||||
import de.softwareapp_hb.privateqrscanner.util.Intents
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.screens
|
||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -20,7 +20,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.clean.scanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.screens
|
||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
@@ -50,11 +50,11 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.clean.scanner.R
|
||||
import com.clean.scanner.data.scanner.DetectionBox
|
||||
import com.clean.scanner.data.scanner.DetectionPoint
|
||||
import com.clean.scanner.domain.ScanResult
|
||||
import com.clean.scanner.util.readablePayload
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionBox
|
||||
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionPoint
|
||||
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
||||
import de.softwareapp_hb.privateqrscanner.util.readablePayload
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanner
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.screens
|
||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -27,10 +27,10 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.clean.scanner.R
|
||||
import com.clean.scanner.ui.BatchScanRecord
|
||||
import com.clean.scanner.util.ClipboardUtil
|
||||
import com.clean.scanner.util.Intents
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.ui.BatchScanRecord
|
||||
import de.softwareapp_hb.privateqrscanner.util.ClipboardUtil
|
||||
import de.softwareapp_hb.privateqrscanner.util.Intents
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.screens
|
||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.background
|
||||
@@ -25,10 +25,10 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.clean.scanner.domain.ScanResult
|
||||
import com.clean.scanner.util.ParsedContact
|
||||
import com.clean.scanner.util.ScanContentParsers
|
||||
import com.clean.scanner.util.UrlRiskScorer
|
||||
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
||||
import de.softwareapp_hb.privateqrscanner.util.ParsedContact
|
||||
import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers
|
||||
import de.softwareapp_hb.privateqrscanner.util.UrlRiskScorer
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.screens
|
||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
@@ -72,20 +72,20 @@ import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.clean.scanner.R
|
||||
import com.clean.scanner.data.scanner.DetectionBox
|
||||
import com.clean.scanner.data.scanner.DetectionPoint
|
||||
import com.clean.scanner.domain.ScanResult
|
||||
import com.clean.scanner.ui.BatchScanRecord
|
||||
import com.clean.scanner.ui.EventTicketScanDecision
|
||||
import com.clean.scanner.ui.UseCaseView
|
||||
import com.clean.scanner.ui.components.CameraPreview
|
||||
import com.clean.scanner.ui.capabilities
|
||||
import com.clean.scanner.util.ClipboardUtil
|
||||
import com.clean.scanner.util.Intents
|
||||
import com.clean.scanner.util.ScanContentParsers
|
||||
import com.clean.scanner.util.UrlRiskScorer
|
||||
import com.clean.scanner.util.readablePayload
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionBox
|
||||
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionPoint
|
||||
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
||||
import de.softwareapp_hb.privateqrscanner.ui.BatchScanRecord
|
||||
import de.softwareapp_hb.privateqrscanner.ui.EventTicketScanDecision
|
||||
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
||||
import de.softwareapp_hb.privateqrscanner.ui.components.CameraPreview
|
||||
import de.softwareapp_hb.privateqrscanner.ui.capabilities
|
||||
import de.softwareapp_hb.privateqrscanner.util.ClipboardUtil
|
||||
import de.softwareapp_hb.privateqrscanner.util.Intents
|
||||
import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers
|
||||
import de.softwareapp_hb.privateqrscanner.util.UrlRiskScorer
|
||||
import de.softwareapp_hb.privateqrscanner.util.readablePayload
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
@@ -127,15 +127,7 @@ fun ScannerScreen(
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val capabilities = remember(useCaseView) { useCaseView.capabilities() }
|
||||
val forceBatchMode = useCaseView == UseCaseView.EventTicketing
|
||||
val showBatchModeToggle = remember(useCaseView) {
|
||||
when (useCaseView) {
|
||||
UseCaseView.InventoryOperations,
|
||||
UseCaseView.FieldWorkServiceTeams,
|
||||
UseCaseView.OfflineLowConnectivity,
|
||||
UseCaseView.TeamHandoverTransfer -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
val showBatchModeToggle = capabilities.allowBatchMode && !forceBatchMode
|
||||
val isBatchModeActive = forceBatchMode || batchMode
|
||||
val duplicateSnackbarHostState = remember { SnackbarHostState() }
|
||||
val toneGenerator = remember { ToneGenerator(AudioManager.STREAM_NOTIFICATION, 70) }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.screens
|
||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -20,9 +20,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.clean.scanner.R
|
||||
import com.clean.scanner.ui.UseCaseView
|
||||
import com.clean.scanner.util.Intents
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
||||
import de.softwareapp_hb.privateqrscanner.util.Intents
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.ui.theme
|
||||
package de.softwareapp_hb.privateqrscanner.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.util
|
||||
package de.softwareapp_hb.privateqrscanner.util
|
||||
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.util
|
||||
package de.softwareapp_hb.privateqrscanner.util
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.clean.scanner.util
|
||||
package de.softwareapp_hb.privateqrscanner.util
|
||||
|
||||
import com.clean.scanner.domain.ScanRecord
|
||||
import de.softwareapp_hb.privateqrscanner.domain.ScanRecord
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.util
|
||||
package de.softwareapp_hb.privateqrscanner.util
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.clean.scanner.util
|
||||
package de.softwareapp_hb.privateqrscanner.util
|
||||
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import java.io.StringReader
|
||||
|
||||
@@ -1,45 +1,598 @@
|
||||
package com.clean.scanner.util
|
||||
package de.softwareapp_hb.privateqrscanner.util
|
||||
|
||||
import com.clean.scanner.domain.UrlRiskResult
|
||||
import de.softwareapp_hb.privateqrscanner.domain.UrlRiskResult
|
||||
import java.net.IDN
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.util.Locale
|
||||
|
||||
object UrlRiskScorer {
|
||||
fun score(raw: String): UrlRiskResult {
|
||||
val uri = runCatching { URI(raw.trim()) }.getOrNull() ?: return UrlRiskResult(0, emptyList())
|
||||
val host = uri.host.orEmpty()
|
||||
val trimmed = raw.trim()
|
||||
val uri = runCatching { URI(trimmed) }.getOrNull() ?: return UrlRiskResult(0, emptyList())
|
||||
val scheme = uri.scheme?.lowercase(Locale.US) ?: return UrlRiskResult(0, emptyList())
|
||||
val hostDetails = extractHost(uri)
|
||||
val host = hostDetails.asciiHost
|
||||
val labels = host.split('.').filter { it.isNotBlank() }
|
||||
val reasons = mutableListOf<String>()
|
||||
var score = 0
|
||||
|
||||
if (host.matches(Regex("^\\d{1,3}(\\.\\d{1,3}){3}$"))) {
|
||||
score += 2
|
||||
reasons += "Host is an IP address"
|
||||
fun add(points: Int, reason: String) {
|
||||
if (reason !in reasons) {
|
||||
score += points
|
||||
reasons += reason
|
||||
}
|
||||
}
|
||||
if (uri.scheme.equals("http", ignoreCase = true)) {
|
||||
score += 2
|
||||
reasons += "URL uses HTTP"
|
||||
|
||||
if (trimmed.any { it.isISOControl() || it.isWhitespace() }) {
|
||||
add(3, "Contains whitespace or control characters")
|
||||
}
|
||||
if (host.contains("xn--", ignoreCase = true)) {
|
||||
score += 2
|
||||
reasons += "Host contains punycode"
|
||||
|
||||
when (scheme) {
|
||||
"http" -> add(2, "URL uses HTTP")
|
||||
"https" -> Unit
|
||||
in HIGH_RISK_SCHEMES -> add(5, "Uses a potentially unsafe URL scheme")
|
||||
else -> add(2, "Uses a non-web URL scheme")
|
||||
}
|
||||
|
||||
if (scheme in WEB_SCHEMES && hostDetails.rawHost.isBlank()) {
|
||||
add(3, "Web URL has no host")
|
||||
}
|
||||
|
||||
if (!uri.userInfo.isNullOrBlank() || uri.rawAuthority.orEmpty().contains("@")) {
|
||||
add(2, "Contains userinfo")
|
||||
}
|
||||
|
||||
if (PERCENT_ENCODING.containsMatchIn(hostDetails.rawHost)) {
|
||||
add(2, "Host contains percent-encoding")
|
||||
}
|
||||
|
||||
if (hostDetails.hasPunycode) {
|
||||
add(2, "Host contains punycode")
|
||||
}
|
||||
|
||||
if (hostDetails.hasUnicode) {
|
||||
add(2, "Host contains non-ASCII characters")
|
||||
}
|
||||
|
||||
if (host.length > 40) {
|
||||
score += 1
|
||||
reasons += "Host is unusually long"
|
||||
add(1, "Host is unusually long")
|
||||
}
|
||||
if ((uri.rawQuery?.length ?: 0) > 120) {
|
||||
score += 1
|
||||
reasons += "Query is unusually long"
|
||||
}
|
||||
val percentEncodedCount = Regex("%[0-9a-fA-F]{2}").findAll(raw).count()
|
||||
if (percentEncodedCount > 10) {
|
||||
score += 1
|
||||
reasons += "Many percent-encodings"
|
||||
}
|
||||
if (!uri.userInfo.isNullOrBlank()) {
|
||||
score += 2
|
||||
reasons += "Contains userinfo"
|
||||
|
||||
val ipv4 = parseIpv4(host)
|
||||
if (ipv4 != null) {
|
||||
add(2, "Host is an IP address")
|
||||
if (isPrivateOrReservedIpv4(ipv4)) {
|
||||
add(2, "Host is a private or reserved IP address")
|
||||
}
|
||||
} else if (isIpv6Literal(hostDetails.rawHost)) {
|
||||
add(2, "Host is an IP address")
|
||||
if (isPrivateOrReservedIpv6(hostDetails.rawHost)) {
|
||||
add(2, "Host is a private or reserved IP address")
|
||||
}
|
||||
} else if (host.isNotBlank()) {
|
||||
scoreDomainShape(host, labels, ::add)
|
||||
scoreKnownRiskyDomains(host, labels, ::add)
|
||||
scoreBrandImpersonation(host, labels, ::add)
|
||||
}
|
||||
|
||||
scoreUrlStructure(trimmed, uri, host, ::add)
|
||||
|
||||
return UrlRiskResult(score = score, reasons = reasons)
|
||||
}
|
||||
|
||||
private fun scoreDomainShape(
|
||||
host: String,
|
||||
labels: List<String>,
|
||||
add: (Int, String) -> Unit
|
||||
) {
|
||||
val registeredDomain = registrableDomain(labels)
|
||||
val registeredLabel = registeredDomain?.substringBefore('.').orEmpty()
|
||||
|
||||
if (host == "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
|
||||
add(2, "Host points to a local name")
|
||||
}
|
||||
|
||||
if (labels.size == 1 && host != "localhost") {
|
||||
add(1, "Host has no public suffix")
|
||||
}
|
||||
|
||||
if (labels.size > 4) {
|
||||
add(1, "Host has many subdomains")
|
||||
}
|
||||
|
||||
if (labels.any { it.length > 30 }) {
|
||||
add(1, "Host contains an unusually long label")
|
||||
}
|
||||
|
||||
if (labels.any { it.contains('_') }) {
|
||||
add(1, "Host contains invalid hostname characters")
|
||||
}
|
||||
|
||||
if (registeredLabel.count { it == '-' } >= 3) {
|
||||
add(1, "Registered domain contains many hyphens")
|
||||
}
|
||||
|
||||
if (registeredLabel.length >= 10 && registeredLabel.count(Char::isDigit) * 100 / registeredLabel.length >= 35) {
|
||||
add(1, "Registered domain contains many digits")
|
||||
}
|
||||
|
||||
if (labels.any { REPEATED_CHARACTER.containsMatchIn(it) }) {
|
||||
add(1, "Host contains repeated characters")
|
||||
}
|
||||
}
|
||||
|
||||
private fun scoreKnownRiskyDomains(
|
||||
host: String,
|
||||
labels: List<String>,
|
||||
add: (Int, String) -> Unit
|
||||
) {
|
||||
val tld = labels.lastOrNull().orEmpty()
|
||||
val registeredDomain = registrableDomain(labels)
|
||||
|
||||
if (registeredDomain in URL_SHORTENERS) {
|
||||
add(2, "Host is a URL shortener")
|
||||
}
|
||||
|
||||
if (tld in WATCHLIST_TLDS) {
|
||||
add(1, "Host uses a commonly abused top-level domain")
|
||||
}
|
||||
}
|
||||
|
||||
private fun scoreBrandImpersonation(
|
||||
host: String,
|
||||
labels: List<String>,
|
||||
add: (Int, String) -> Unit
|
||||
) {
|
||||
val registeredDomain = registrableDomain(labels)
|
||||
val labelsToInspect = labels.filterNot { it in PUBLIC_SUFFIX_PARTS }
|
||||
|
||||
for (brand in BRAND_PROFILES) {
|
||||
if (brand.officialDomains.any { host == it || host.endsWith(".$it") }) continue
|
||||
|
||||
val registeredLabel = registeredDomain?.substringBefore('.').orEmpty()
|
||||
if (registeredLabel == brand.name) continue
|
||||
|
||||
val exactBrandLabel = labelsToInspect.any { it == brand.name }
|
||||
val embeddedBrand = labelsToInspect.any { label ->
|
||||
label != brand.name && label.contains(brand.name)
|
||||
}
|
||||
if (exactBrandLabel || embeddedBrand) {
|
||||
add(3, "Host embeds a known brand outside its official domain")
|
||||
return
|
||||
}
|
||||
|
||||
val candidateLabels = (labelsToInspect + registeredLabel).filter { it.length >= 4 }.distinct()
|
||||
if (candidateLabels.any { isLookalikeBrandLabel(it, brand.name) }) {
|
||||
add(3, "Host resembles a known brand")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scoreUrlStructure(
|
||||
raw: String,
|
||||
uri: URI,
|
||||
host: String,
|
||||
add: (Int, String) -> Unit
|
||||
) {
|
||||
val rawPath = uri.rawPath.orEmpty()
|
||||
val rawQuery = uri.rawQuery.orEmpty()
|
||||
val lowerPathAndQuery = "$rawPath?$rawQuery".lowercase(Locale.US)
|
||||
val lowerRaw = raw.lowercase(Locale.US)
|
||||
|
||||
when {
|
||||
raw.length > 500 -> add(2, "URL is extremely long")
|
||||
raw.length > 250 -> add(1, "URL is unusually long")
|
||||
}
|
||||
|
||||
if (uri.port != -1 && !isStandardPort(uri.scheme.orEmpty(), uri.port)) {
|
||||
add(1, "URL uses a non-standard port")
|
||||
}
|
||||
|
||||
if (rawPath.length > 120) {
|
||||
add(1, "Path is unusually long")
|
||||
}
|
||||
|
||||
if ((uri.rawQuery?.length ?: 0) > 120) {
|
||||
add(1, "Query is unusually long")
|
||||
}
|
||||
|
||||
if (rawPath.split('/').count { it.isNotBlank() } > 8) {
|
||||
add(1, "Path has many segments")
|
||||
}
|
||||
|
||||
val percentEncodedCount = PERCENT_ENCODING.findAll(raw).count()
|
||||
if (percentEncodedCount > 10) {
|
||||
add(1, "Many percent-encodings")
|
||||
}
|
||||
|
||||
if (ENCODED_CONTROL_CHARACTER.containsMatchIn(raw)) {
|
||||
add(3, "URL contains encoded control characters")
|
||||
}
|
||||
|
||||
if (raw.contains('\\')) {
|
||||
add(2, "URL contains backslashes")
|
||||
}
|
||||
|
||||
if (rawPath.contains("//")) {
|
||||
add(1, "Path contains nested URL separators")
|
||||
}
|
||||
|
||||
if (rawPath.contains('@') || rawQuery.contains('@')) {
|
||||
add(1, "Path or query contains an at-sign")
|
||||
}
|
||||
|
||||
if (DANGEROUS_FILE_EXTENSIONS.any { lowerPathAndQuery.contains(it) }) {
|
||||
add(2, "URL points to a potentially executable file")
|
||||
} else if (ARCHIVE_FILE_EXTENSIONS.any { lowerPathAndQuery.contains(it) }) {
|
||||
add(1, "URL points to an archive file")
|
||||
}
|
||||
|
||||
if (CREDENTIAL_KEYWORDS.any { lowerPathAndQuery.contains(it) }) {
|
||||
add(1, "URL contains account or credential keywords")
|
||||
}
|
||||
|
||||
if (hasExternalRedirect(rawQuery, host)) {
|
||||
add(2, "Query redirects to another domain")
|
||||
}
|
||||
|
||||
if (hasLongOpaqueToken(rawPath) || hasLongOpaqueToken(rawQuery)) {
|
||||
add(1, "URL contains a long opaque token")
|
||||
}
|
||||
|
||||
if (lowerRaw.contains("%2f%2f") || lowerRaw.contains("%5c")) {
|
||||
add(1, "URL contains encoded path separators")
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractHost(uri: URI): HostDetails {
|
||||
val rawAuthority = uri.rawAuthority.orEmpty()
|
||||
val rawHostFromAuthority = if (rawAuthority.isBlank()) {
|
||||
""
|
||||
} else {
|
||||
val withoutUserInfo = rawAuthority.substringAfterLast('@')
|
||||
when {
|
||||
withoutUserInfo.startsWith("[") -> withoutUserInfo.substringAfter('[').substringBefore(']')
|
||||
withoutUserInfo.count { it == ':' } == 1 &&
|
||||
withoutUserInfo.substringAfterLast(':').all(Char::isDigit) ->
|
||||
withoutUserInfo.substringBeforeLast(':')
|
||||
else -> withoutUserInfo
|
||||
}
|
||||
}
|
||||
val rawHost = (uri.host ?: rawHostFromAuthority)
|
||||
.trim()
|
||||
.trim('.')
|
||||
.replace('\u3002', '.')
|
||||
.replace('\uFF0E', '.')
|
||||
.replace('\uFF61', '.')
|
||||
val asciiHost = toAsciiHost(rawHost)
|
||||
return HostDetails(
|
||||
rawHost = rawHost,
|
||||
asciiHost = asciiHost,
|
||||
hasUnicode = rawHost.any { it.code > 127 },
|
||||
hasPunycode = asciiHost.split('.').any { it.startsWith("xn--") } ||
|
||||
rawHost.contains("xn--", ignoreCase = true)
|
||||
)
|
||||
}
|
||||
|
||||
private fun toAsciiHost(rawHost: String): String {
|
||||
if (rawHost.isBlank()) return ""
|
||||
return rawHost
|
||||
.removeSurrounding("[", "]")
|
||||
.split('.')
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(".") { label ->
|
||||
runCatching { IDN.toASCII(label, IDN.ALLOW_UNASSIGNED).lowercase(Locale.US) }
|
||||
.getOrElse { label.lowercase(Locale.US) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseIpv4(host: String): IntArray? {
|
||||
if (!IPV4_PATTERN.matches(host)) return null
|
||||
val octets = host.split('.').mapNotNull { it.toIntOrNull() }
|
||||
if (octets.size != 4 || octets.any { it !in 0..255 }) return null
|
||||
return octets.toIntArray()
|
||||
}
|
||||
|
||||
private fun isPrivateOrReservedIpv4(octets: IntArray): Boolean {
|
||||
val first = octets[0]
|
||||
val second = octets[1]
|
||||
val third = octets[2]
|
||||
return first == 0 ||
|
||||
first == 10 ||
|
||||
first == 127 ||
|
||||
first >= 224 ||
|
||||
(first == 100 && second in 64..127) ||
|
||||
(first == 169 && second == 254) ||
|
||||
(first == 172 && second in 16..31) ||
|
||||
(first == 192 && second == 168) ||
|
||||
(first == 192 && second == 0 && third == 0) ||
|
||||
(first == 192 && second == 0 && third == 2) ||
|
||||
(first == 198 && second in 18..19) ||
|
||||
(first == 198 && second == 51 && third == 100) ||
|
||||
(first == 203 && second == 0 && third == 113)
|
||||
}
|
||||
|
||||
private fun isIpv6Literal(rawHost: String): Boolean {
|
||||
val host = rawHost.removeSurrounding("[", "]")
|
||||
return host.contains(':') && host.all {
|
||||
it == ':' || it == '.' || it.isDigit() || it.lowercaseChar() in 'a'..'f'
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPrivateOrReservedIpv6(rawHost: String): Boolean {
|
||||
val host = rawHost.removeSurrounding("[", "]").lowercase(Locale.US)
|
||||
return host == "::" ||
|
||||
host == "::1" ||
|
||||
host.startsWith("fc") ||
|
||||
host.startsWith("fd") ||
|
||||
host.startsWith("fe80") ||
|
||||
host.startsWith("2001:db8")
|
||||
}
|
||||
|
||||
private fun registrableDomain(labels: List<String>): String? {
|
||||
if (labels.isEmpty()) return null
|
||||
if (labels.size == 1) return labels.first()
|
||||
val lastTwo = labels.takeLast(2).joinToString(".")
|
||||
return if (lastTwo in COMMON_SECOND_LEVEL_SUFFIXES && labels.size >= 3) {
|
||||
labels.takeLast(3).joinToString(".")
|
||||
} else {
|
||||
lastTwo
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLookalikeBrandLabel(label: String, brand: String): Boolean {
|
||||
if (label == brand) return false
|
||||
val normalized = normalizeLookalikes(label)
|
||||
if (normalized == brand) return true
|
||||
if (kotlin.math.abs(normalized.length - brand.length) > 1) return false
|
||||
return levenshteinDistance(normalized, brand) <= 1
|
||||
}
|
||||
|
||||
private fun normalizeLookalikes(value: String): String {
|
||||
return value
|
||||
.replace('0', 'o')
|
||||
.replace('1', 'l')
|
||||
.replace('3', 'e')
|
||||
.replace('5', 's')
|
||||
.replace('7', 't')
|
||||
.replace("rn", "m")
|
||||
}
|
||||
|
||||
private fun levenshteinDistance(left: String, right: String): Int {
|
||||
if (left == right) return 0
|
||||
if (left.isEmpty()) return right.length
|
||||
if (right.isEmpty()) return left.length
|
||||
|
||||
var previous = IntArray(right.length + 1) { it }
|
||||
var current = IntArray(right.length + 1)
|
||||
for (i in left.indices) {
|
||||
current[0] = i + 1
|
||||
for (j in right.indices) {
|
||||
val substitutionCost = if (left[i] == right[j]) 0 else 1
|
||||
current[j + 1] = minOf(
|
||||
current[j] + 1,
|
||||
previous[j + 1] + 1,
|
||||
previous[j] + substitutionCost
|
||||
)
|
||||
}
|
||||
val swap = previous
|
||||
previous = current
|
||||
current = swap
|
||||
}
|
||||
return previous[right.length]
|
||||
}
|
||||
|
||||
private fun isStandardPort(scheme: String, port: Int): Boolean {
|
||||
return (scheme.equals("http", ignoreCase = true) && port == 80) ||
|
||||
(scheme.equals("https", ignoreCase = true) && port == 443)
|
||||
}
|
||||
|
||||
private fun hasExternalRedirect(rawQuery: String, sourceHost: String): Boolean {
|
||||
if (rawQuery.isBlank()) return false
|
||||
return queryParams(rawQuery).any { (key, value) ->
|
||||
key in REDIRECT_PARAMETER_NAMES && value.startsWith("http", ignoreCase = true) &&
|
||||
runCatching {
|
||||
val destinationHost = extractHost(URI(value)).asciiHost
|
||||
val sourceDomain = registrableDomain(sourceHost.split('.').filter { it.isNotBlank() })
|
||||
val destinationDomain = registrableDomain(destinationHost.split('.').filter { it.isNotBlank() })
|
||||
destinationDomain != null && sourceDomain != null && destinationDomain != sourceDomain
|
||||
}.getOrDefault(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryParams(rawQuery: String): List<Pair<String, String>> {
|
||||
return rawQuery
|
||||
.split('&', ';')
|
||||
.mapNotNull { token ->
|
||||
val key = token.substringBefore('=', "").takeIf { it.isNotBlank() } ?: return@mapNotNull null
|
||||
val value = token.substringAfter('=', "")
|
||||
decodeUrlComponent(key).lowercase(Locale.US) to decodeUrlComponent(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeUrlComponent(value: String): String {
|
||||
return runCatching { URLDecoder.decode(value, Charsets.UTF_8.name()) }.getOrDefault(value)
|
||||
}
|
||||
|
||||
private fun hasLongOpaqueToken(value: String): Boolean {
|
||||
return value
|
||||
.split('/', '&', '=', '?', '-', '_')
|
||||
.any { token ->
|
||||
token.length >= 48 &&
|
||||
token.toSet().size >= 8 &&
|
||||
token.count { it.isLetterOrDigit() || it == '+' || it == '/' } * 100 / token.length >= 85
|
||||
}
|
||||
}
|
||||
|
||||
private data class HostDetails(
|
||||
val rawHost: String,
|
||||
val asciiHost: String,
|
||||
val hasUnicode: Boolean,
|
||||
val hasPunycode: Boolean
|
||||
)
|
||||
|
||||
private data class BrandProfile(
|
||||
val name: String,
|
||||
val officialDomains: Set<String>
|
||||
)
|
||||
|
||||
private val WEB_SCHEMES = setOf("http", "https")
|
||||
private val HIGH_RISK_SCHEMES = setOf("javascript", "data", "file", "content", "intent")
|
||||
private val IPV4_PATTERN = Regex("""^\d{1,3}(\.\d{1,3}){3}$""")
|
||||
private val PERCENT_ENCODING = Regex("""%[0-9a-fA-F]{2}""")
|
||||
private val ENCODED_CONTROL_CHARACTER = Regex("""(?i)%(00|0a|0d|09)""")
|
||||
private val REPEATED_CHARACTER = Regex("""(.)\1{3,}""")
|
||||
|
||||
private val COMMON_SECOND_LEVEL_SUFFIXES = setOf(
|
||||
"ac.uk",
|
||||
"co.in",
|
||||
"co.jp",
|
||||
"co.nz",
|
||||
"co.uk",
|
||||
"com.au",
|
||||
"com.br",
|
||||
"com.cn",
|
||||
"com.hk",
|
||||
"com.mx",
|
||||
"com.sg",
|
||||
"com.tr",
|
||||
"ne.jp",
|
||||
"net.au",
|
||||
"org.uk"
|
||||
)
|
||||
|
||||
private val PUBLIC_SUFFIX_PARTS = setOf(
|
||||
"ac",
|
||||
"co",
|
||||
"com",
|
||||
"edu",
|
||||
"gov",
|
||||
"mil",
|
||||
"ne",
|
||||
"net",
|
||||
"org"
|
||||
)
|
||||
|
||||
private val URL_SHORTENERS = setOf(
|
||||
"bit.ly",
|
||||
"buff.ly",
|
||||
"cutt.ly",
|
||||
"goo.gl",
|
||||
"is.gd",
|
||||
"lnkd.in",
|
||||
"ow.ly",
|
||||
"rebrand.ly",
|
||||
"s.id",
|
||||
"t.co",
|
||||
"tiny.cc",
|
||||
"tinyurl.com",
|
||||
"trib.al",
|
||||
"v.gd"
|
||||
)
|
||||
|
||||
private val WATCHLIST_TLDS = setOf(
|
||||
"cam",
|
||||
"click",
|
||||
"country",
|
||||
"download",
|
||||
"gq",
|
||||
"kim",
|
||||
"loan",
|
||||
"men",
|
||||
"ml",
|
||||
"mov",
|
||||
"quest",
|
||||
"rest",
|
||||
"surf",
|
||||
"tk",
|
||||
"top",
|
||||
"work",
|
||||
"xyz",
|
||||
"zip"
|
||||
)
|
||||
|
||||
private val CREDENTIAL_KEYWORDS = setOf(
|
||||
"2fa",
|
||||
"account",
|
||||
"auth",
|
||||
"billing",
|
||||
"confirm",
|
||||
"login",
|
||||
"mfa",
|
||||
"password",
|
||||
"payment",
|
||||
"recovery",
|
||||
"reset",
|
||||
"secure",
|
||||
"security",
|
||||
"signin",
|
||||
"sign-in",
|
||||
"unlock",
|
||||
"update",
|
||||
"verify",
|
||||
"wallet"
|
||||
)
|
||||
|
||||
private val REDIRECT_PARAMETER_NAMES = setOf(
|
||||
"continue",
|
||||
"dest",
|
||||
"destination",
|
||||
"next",
|
||||
"redirect",
|
||||
"redirect_uri",
|
||||
"return",
|
||||
"return_url",
|
||||
"target",
|
||||
"u",
|
||||
"url"
|
||||
)
|
||||
|
||||
private val DANGEROUS_FILE_EXTENSIONS = setOf(
|
||||
".apk",
|
||||
".bat",
|
||||
".cmd",
|
||||
".dmg",
|
||||
".exe",
|
||||
".jar",
|
||||
".js",
|
||||
".msi",
|
||||
".ps1",
|
||||
".scr",
|
||||
".vbs"
|
||||
)
|
||||
|
||||
private val ARCHIVE_FILE_EXTENSIONS = setOf(
|
||||
".7z",
|
||||
".gz",
|
||||
".rar",
|
||||
".tar",
|
||||
".zip"
|
||||
)
|
||||
|
||||
private val BRAND_PROFILES = listOf(
|
||||
BrandProfile("adobe", setOf("adobe.com")),
|
||||
BrandProfile("amazon", setOf("amazon.com")),
|
||||
BrandProfile("apple", setOf("apple.com")),
|
||||
BrandProfile("binance", setOf("binance.com")),
|
||||
BrandProfile("chase", setOf("chase.com")),
|
||||
BrandProfile("citibank", setOf("citibank.com")),
|
||||
BrandProfile("coinbase", setOf("coinbase.com")),
|
||||
BrandProfile("discord", setOf("discord.com", "discord.gg")),
|
||||
BrandProfile("docusign", setOf("docusign.com")),
|
||||
BrandProfile("dropbox", setOf("dropbox.com")),
|
||||
BrandProfile("ebay", setOf("ebay.com")),
|
||||
BrandProfile("facebook", setOf("facebook.com", "fb.com")),
|
||||
BrandProfile("github", setOf("github.com")),
|
||||
BrandProfile("google", setOf("google.com")),
|
||||
BrandProfile("instagram", setOf("instagram.com")),
|
||||
BrandProfile("microsoft", setOf("microsoft.com", "microsoftonline.com", "office.com", "live.com")),
|
||||
BrandProfile("netflix", setOf("netflix.com")),
|
||||
BrandProfile("paypal", setOf("paypal.com")),
|
||||
BrandProfile("steam", setOf("steampowered.com", "steamcommunity.com")),
|
||||
BrandProfile("telegram", setOf("telegram.org", "t.me")),
|
||||
BrandProfile("whatsapp", setOf("whatsapp.com", "wa.me")),
|
||||
BrandProfile("wellsfargo", setOf("wellsfargo.com"))
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user