diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt index c5ef7fdb189e..088f2641f07f 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt @@ -27,6 +27,8 @@ import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.domain.app.LoginTriggerType import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor +import com.duckduckgo.autofill.impl.AutofillStoredBackJavascriptInterface.BackfillResult.BackfillNotSupported +import com.duckduckgo.autofill.impl.AutofillStoredBackJavascriptInterface.BackfillResult.BackfillSupported import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator import com.duckduckgo.autofill.impl.domain.javascript.JavascriptCredentials import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextRecentInstallChecker @@ -34,7 +36,11 @@ import com.duckduckgo.autofill.impl.email.incontext.store.EmailProtectionInConte import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataCredentialsRequest import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataRequest +import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.FORM_SUBMISSION +import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.PARTIAL_SAVE +import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.UNKNOWN import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputMainType.CREDENTIALS import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.USERNAME @@ -42,6 +48,7 @@ import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerTyp import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.AUTOPROMPT import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.USER_INITIATED import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter +import com.duckduckgo.autofill.impl.partialsave.PartialCredentialSaveStore import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository @@ -114,6 +121,7 @@ class AutofillStoredBackJavascriptInterface @Inject constructor( private val loginDeduplicator: AutofillLoginDeduplicator, private val systemAutofillServiceSuppressor: SystemAutofillServiceSuppressor, private val neverSavedSiteRepository: NeverSavedSiteRepository, + private val partialCredentialSaveStore: PartialCredentialSaveStore, ) : AutofillJavascriptInterface { override var callback: Callback? = null @@ -182,6 +190,7 @@ class AutofillStoredBackJavascriptInterface @Inject constructor( emailProtectionInContextSignupFlowCallback?.closeInContextSignup() } + @Suppress("UNUSED_PARAMETER") @JavascriptInterface fun showInContextEmailProtectionSignupPrompt(data: String) { coroutineScope.launch(dispatcherProvider.io()) { @@ -297,25 +306,49 @@ class AutofillStoredBackJavascriptInterface @Inject constructor( return@launch } - val jsCredentials = JavascriptCredentials(request.credentials!!.username, request.credentials.password) - val credentials = jsCredentials.asLoginCredentials(currentUrl) + val credentials = request.credentials!! - val autologinId = autoSavedLoginsMonitor?.getAutoSavedLoginId(tabId) - Timber.i("Autogenerated? %s, Previous autostored login ID: %s", request.credentials.autogenerated, autologinId) - val autosavedLogin = autologinId?.let { autofillStore.getCredentialsWithId(it) } - - val autogenerated = request.credentials.autogenerated - val actions = passwordEventResolver.decideActions(autosavedLogin, autogenerated) - processStoreFormDataActions(actions, currentUrl, credentials) + when (request.trigger) { + FORM_SUBMISSION -> handleRequestForFormSubmission(credentials, currentUrl) + PARTIAL_SAVE -> handleRequestForPartialSave(credentials, currentUrl) + UNKNOWN -> Timber.e("Unknown trigger type %s") + } } } + private suspend fun handleRequestForFormSubmission( + requestCredentials: AutofillStoreFormDataCredentialsRequest, + currentUrl: String, + ) { + val jsCredentials = JavascriptCredentials(requestCredentials.username, requestCredentials.password) + + val (credentials, wasBackfilled) = jsCredentials + .asLoginCredentials(currentUrl) + .backfillUsernameIfRequired(currentUrl) + + val autologinId = autoSavedLoginsMonitor?.getAutoSavedLoginId(tabId) + val autogenerated = requestCredentials.autogenerated + val autosavedLogin = autologinId?.let { autofillStore.getCredentialsWithId(it) } + + val actions = passwordEventResolver.decideActions(autosavedLogin, autogenerated) + processStoreFormDataActions(actions, currentUrl, credentials) + } + + private suspend fun handleRequestForPartialSave( + requestCredentials: AutofillStoreFormDataCredentialsRequest, + currentUrl: String, + ) { + val username = requestCredentials.username ?: return + partialCredentialSaveStore.saveUsername(url = currentUrl, username = username) + Timber.d("Partial save: username: [%s] for %s", username, currentUrl) + } + private suspend fun processStoreFormDataActions( actions: List, currentUrl: String, credentials: LoginCredentials, ) { - Timber.d("%d actions to take: %s", actions.size, actions.joinToString()) + Timber.d("%d actions to take: %s", actions.size, actions.joinToString { it.javaClass.simpleName }) actions.forEach { when (it) { is DeleteAutoLogin -> { @@ -348,6 +381,24 @@ class AutofillStoredBackJavascriptInterface @Inject constructor( } } + private suspend fun backfillUsernameSupport( + credentialsFromJs: LoginCredentials, + currentUrl: String, + ): BackfillResult { + // if the incoming request has a username populated, that takes precedence + if (credentialsFromJs.username != null) { + Timber.v("Backfilling username from partial save not supported because username provided already, for %s", currentUrl) + return BackfillNotSupported + } + + val username = partialCredentialSaveStore.consumeUsernameFromBackFill(currentUrl) ?: return BackfillNotSupported.also { + Timber.v("Backfilling username from partial save not supported because eligible username not found, for %s", currentUrl) + } + + Timber.v("Backfilling username [%s] from partial save, for %s", username, currentUrl) + return BackfillSupported(username) + } + private fun isUpdateRequired( existingCredentials: LoginCredentials, credentials: LoginCredentials, @@ -421,6 +472,24 @@ class AutofillStoredBackJavascriptInterface @Inject constructor( ) } + private suspend fun LoginCredentials.backfillUsernameIfRequired(currentUrl: String): Pair { + // determine if we can and should use a partial previous submission's username + return when (val result = backfillUsernameSupport(this, currentUrl)) { + is BackfillSupported -> { + Pair(this.copy(username = result.username), true) + } + + is BackfillNotSupported -> { + Pair(this, false) + } + } + } + + sealed interface BackfillResult { + data class BackfillSupported(val username: String) : BackfillResult + data object BackfillNotSupported : BackfillResult + } + interface UrlProvider { suspend fun currentUrl(webView: WebView?): String? } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillDataRequest.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillDataRequest.kt index 7b772b0cc3e0..11672e3ff211 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillDataRequest.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillDataRequest.kt @@ -78,3 +78,15 @@ enum class SupportedAutofillTriggerType { @Json(name = "autoprompt") AUTOPROMPT, } + +enum class FormSubmissionTriggerType { + @Json(name = "formSubmission") + FORM_SUBMISSION, + + @Json(name = "partialSave") + PARTIAL_SAVE, + + UNKNOWN, + + ; +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillRequestParser.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillRequestParser.kt index 23f17dcfe443..35788d0305d5 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillRequestParser.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillRequestParser.kt @@ -16,13 +16,18 @@ package com.duckduckgo.autofill.impl.jsbridge.request -import com.duckduckgo.common.utils.DefaultDispatcherProvider +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillJsonRequestParser.AutofillStoreFormDataCredentialsJsonRequest +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillJsonRequestParser.AutofillStoreFormDataJsonRequest +import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.UNKNOWN import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import com.squareup.moshi.Moshi +import com.squareup.moshi.adapters.EnumJsonAdapter +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import javax.inject.Inject import kotlinx.coroutines.withContext +import timber.log.Timber interface AutofillRequestParser { suspend fun parseAutofillDataRequest(request: String): Result @@ -31,12 +36,18 @@ interface AutofillRequestParser { @ContributesBinding(AppScope::class) class AutofillJsonRequestParser @Inject constructor( - val moshi: Moshi, - private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), + private val dispatchers: DispatcherProvider, ) : AutofillRequestParser { private val autofillDataRequestParser by lazy { moshi.adapter(AutofillDataRequest::class.java) } - private val autofillStoreFormDataRequestParser by lazy { moshi.adapter(AutofillStoreFormDataRequest::class.java) } + private val autofillStoreFormDataRequestParser by lazy { moshi.adapter(AutofillStoreFormDataJsonRequest::class.java) } + + private val moshi by lazy { + Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .add(FormSubmissionTriggerType::class.java, EnumJsonAdapter.create(FormSubmissionTriggerType::class.java).withUnknownFallback(UNKNOWN)) + .build() + } override suspend fun parseAutofillDataRequest(request: String): Result { return withContext(dispatchers.io()) { @@ -56,13 +67,41 @@ class AutofillJsonRequestParser @Inject constructor( return withContext(dispatchers.io()) { val result = kotlin.runCatching { autofillStoreFormDataRequestParser.fromJson(request) - }.getOrNull() + } + .onFailure { Timber.w(it, "Failed to parse autofill JSON for AutofillStoreFormDataRequest") } + .getOrNull() return@withContext if (result == null) { Result.failure(IllegalArgumentException("Failed to parse autofill JSON for AutofillStoreFormDataRequest")) } else { - Result.success(result) + Result.success(result.mapToPublicType()) } } } + + internal data class AutofillStoreFormDataJsonRequest( + val credentials: AutofillStoreFormDataCredentialsJsonRequest?, + val trigger: FormSubmissionTriggerType?, + ) + + internal data class AutofillStoreFormDataCredentialsJsonRequest( + val username: String?, + val password: String?, + val autogenerated: Boolean = false, + ) +} + +private fun AutofillStoreFormDataJsonRequest?.mapToPublicType(): AutofillStoreFormDataRequest { + return AutofillStoreFormDataRequest( + credentials = this?.credentials?.mapToPublicType(), + trigger = this?.trigger ?: UNKNOWN, + ) +} + +private fun AutofillStoreFormDataCredentialsJsonRequest?.mapToPublicType(): AutofillStoreFormDataCredentialsRequest { + return AutofillStoreFormDataCredentialsRequest( + username = this?.username, + password = this?.password, + autogenerated = this?.autogenerated ?: false, + ) } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillStoreFormDataRequest.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillStoreFormDataRequest.kt index 1a72921a19aa..3adf2ae64e13 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillStoreFormDataRequest.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillStoreFormDataRequest.kt @@ -18,6 +18,7 @@ package com.duckduckgo.autofill.impl.jsbridge.request data class AutofillStoreFormDataRequest( val credentials: AutofillStoreFormDataCredentialsRequest?, + val trigger: FormSubmissionTriggerType, ) data class AutofillStoreFormDataCredentialsRequest( diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/partialsave/PartialCredentialSaveStore.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/partialsave/PartialCredentialSaveStore.kt new file mode 100644 index 000000000000..bde442c762e7 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/partialsave/PartialCredentialSaveStore.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.partialsave + +import androidx.collection.LruCache +import com.duckduckgo.autofill.impl.time.TimeProvider +import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import dagger.SingleInstanceIn +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlinx.coroutines.withContext +import timber.log.Timber + +interface PartialCredentialSaveStore { + suspend fun saveUsername( + url: String, + username: String, + ) + + suspend fun consumeUsernameFromBackFill(url: String): String? + suspend fun wasBackFilledRecently(url: String, username: String): Boolean +} + +@ContributesBinding(AppScope::class) +@SingleInstanceIn(AppScope::class) +class PartialCredentialSaveInMemoryStore @Inject constructor( + private val urlMatcher: AutofillUrlMatcher, + private val timeProvider: TimeProvider, + private val dispatchers: DispatcherProvider, +) : PartialCredentialSaveStore { + + private val backFillHistory = LruCache(5) + + override suspend fun saveUsername( + url: String, + username: String, + ) { + withContext(dispatchers.io()) { + val etldPlusOne = extractEtldPlusOne(url) ?: return@withContext + backFillHistory.put(etldPlusOne, PartialSave(username = username, creationTimestamp = timeProvider.currentTimeMillis())) + } + } + + /** + * If a potential backFill username can be used for the given URL it will be returned + */ + override suspend fun consumeUsernameFromBackFill(url: String): String? { + return withContext(dispatchers.io()) { + val etldPlusOne = extractEtldPlusOne(url) ?: return@withContext null + val activeBackFill = backFillHistory[etldPlusOne] ?: return@withContext null + + if (activeBackFill.isExpired()) { + Timber.v("Found expired username [%s] for %s. Not using for backFill.", activeBackFill.username, etldPlusOne) + return@withContext null + } + + backFillHistory.put(etldPlusOne, activeBackFill.copy(lastConsumedTimestamp = timeProvider.currentTimeMillis())) + activeBackFill.username + } + } + + override suspend fun wasBackFilledRecently(url: String, username: String): Boolean { + val etldPlusOne = extractEtldPlusOne(url) ?: return false + val partialSave = backFillHistory[etldPlusOne] ?: return false + if (partialSave.username != username) return false + + return partialSave.consumedRecently(timeProvider) + } + + private fun extractEtldPlusOne(url: String) = urlMatcher.extractUrlPartsForAutofill(url).eTldPlus1 + + private fun PartialSave.isExpired(): Boolean { + return (timeProvider.currentTimeMillis() - creationTimestamp) > MAX_VALIDITY_MS + } + + data class PartialSave( + val username: String, + val creationTimestamp: Long, + val lastConsumedTimestamp: Long? = null, + ) { + fun consumedRecently(timeProvider: TimeProvider): Boolean { + return lastConsumedTimestamp?.let { timeProvider.currentTimeMillis() - it < TIME_WINDOW_FOR_BEING_RECENT_MS } ?: false + } + } + + companion object { + val MAX_VALIDITY_MS = TimeUnit.MINUTES.toMillis(3) + val TIME_WINDOW_FOR_BEING_RECENT_MS = TimeUnit.SECONDS.toMillis(10) + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/AutofillUpdatingExistingCredentialsDialogFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/AutofillUpdatingExistingCredentialsDialogFragment.kt index ee6fd6d155df..fb331120ba90 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/AutofillUpdatingExistingCredentialsDialogFragment.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/AutofillUpdatingExistingCredentialsDialogFragment.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.setFragmentResult import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog @@ -32,6 +33,7 @@ import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.databinding.ContentAutofillUpdateExistingCredentialsBinding +import com.duckduckgo.autofill.impl.partialsave.PartialCredentialSaveStore import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_UPDATE_LOGIN_PROMPT_DISMISSED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_UPDATE_LOGIN_PROMPT_SAVED @@ -47,6 +49,9 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.android.support.AndroidSupportInjection import javax.inject.Inject +import kotlinx.coroutines.CoroutineStart.LAZY +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import timber.log.Timber @InjectWith(FragmentScope::class) @@ -63,6 +68,9 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm @Inject lateinit var autofillFireproofDialogSuppressor: AutofillFireproofDialogSuppressor + @Inject + lateinit var partialCredentialSaveStore: PartialCredentialSaveStore + /** * To capture all the ways the BottomSheet can be dismissed, we might end up with onCancel being called when we don't want it * This flag is set to true when taking an action which dismisses the dialog, but should not be treated as a cancellation. @@ -73,6 +81,13 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm ViewModelProvider(this, viewModelFactory)[AutofillUpdatingExistingCredentialViewModel::class.java] } + private val wasUsernameBackFilled: Deferred = lifecycleScope.async(start = LAZY) { + val usernameToSave = getCredentialsToSave().username ?: return@async false + partialCredentialSaveStore.wasBackFilledRecently(url = getOriginalUrl(), username = usernameToSave).also { + Timber.v("Determined that username was %sbackFilled", if (it) "" else "not ") + } + } + override fun onAttach(context: Context) { AndroidSupportInjection.inject(this) super.onAttach(context) diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt index 172112766201..e1cbd6d66d3c 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt @@ -34,11 +34,13 @@ import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataCredentialsRequest import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataRequest +import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.FORM_SUBMISSION import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputMainType.CREDENTIALS import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.USERNAME import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.USER_INITIATED import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter +import com.duckduckgo.autofill.impl.partialsave.PartialCredentialSaveStore import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository @@ -79,6 +81,7 @@ class AutofillStoredBackJavascriptInterfaceTest { private val loginDeduplicator: AutofillLoginDeduplicator = NoopDeduplicator() private val systemAutofillServiceSuppressor: SystemAutofillServiceSuppressor = mock() private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() + private val partialCredentialSaveStore: PartialCredentialSaveStore = mock() private lateinit var testee: AutofillStoredBackJavascriptInterface private val testCallback = TestCallback() @@ -107,6 +110,7 @@ class AutofillStoredBackJavascriptInterfaceTest { loginDeduplicator = loginDeduplicator, systemAutofillServiceSuppressor = systemAutofillServiceSuppressor, neverSavedSiteRepository = neverSavedSiteRepository, + partialCredentialSaveStore = partialCredentialSaveStore, ) testee.callback = testCallback testee.webView = testWebView @@ -340,7 +344,7 @@ class AutofillStoredBackJavascriptInterfaceTest { password: String?, ) { val credentials = AutofillStoreFormDataCredentialsRequest(username = username, password = password) - val topLevelRequest = AutofillStoreFormDataRequest(credentials) + val topLevelRequest = AutofillStoreFormDataRequest(credentials, FORM_SUBMISSION) whenever(requestParser.parseStoreFormDataRequest(any())).thenReturn(Result.success(topLevelRequest)) whenever(passwordEventResolver.decideActions(anyOrNull(), any())).thenReturn(listOf(Actions.PromptToSave)) } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillJsonRequestParserTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillJsonRequestParserTest.kt index fe6bb2c47558..f0ce5239a770 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillJsonRequestParserTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillJsonRequestParserTest.kt @@ -16,16 +16,22 @@ package com.duckduckgo.autofill.impl.jsbridge.request +import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.FORM_SUBMISSION +import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.PARTIAL_SAVE +import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.UNKNOWN +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.FileUtilities -import com.squareup.moshi.Moshi import kotlinx.coroutines.test.runTest import org.junit.Assert.* +import org.junit.Rule import org.junit.Test class AutofillJsonRequestParserTest { - private val moshi = Moshi.Builder().build() - private val testee = AutofillJsonRequestParser(moshi) + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val testee = AutofillJsonRequestParser(dispatchers = coroutineTestRule.testDispatcherProvider) @Test fun whenUsernameAndPasswordBothProvidedThenBothInResponse() = runTest { @@ -69,6 +75,14 @@ class AutofillJsonRequestParserTest { assertNull(parsed.credentials!!.password) } + @Test + fun whenPasswordMissingAndIsPartialSaveThenUsernamePopulatedAndPartialSaveFlagSet() = runTest { + val parsed = "storeFormData_passwordMissing_partialSave".parseStoreFormDataJson() + assertEquals("dax@duck.com", parsed.credentials!!.username) + assertNull(parsed.credentials!!.password) + assertEquals(PARTIAL_SAVE, parsed.trigger) + } + @Test fun whenTopLevelCredentialsObjectMissingThenParsesWithoutError() = runTest { val parsed = "storeFormData_topLevelDataMissing".parseStoreFormDataJson() @@ -87,6 +101,30 @@ class AutofillJsonRequestParserTest { assertTrue(result.isFailure) } + @Test + fun whenStoreFormDataRequestMissingTriggerThenIsUnknown() = runTest { + val parsed = "storeFormData_trigger_missing".parseStoreFormDataJson() + assertEquals(UNKNOWN, parsed.trigger) + } + + @Test + fun whenStoreFormDataRequestUnknownTriggerThenIsUnknown() = runTest { + val parsed = "storeFormData_trigger_unknown".parseStoreFormDataJson() + assertEquals(UNKNOWN, parsed.trigger) + } + + @Test + fun whenStoreFormDataRequestHasFormSubmissionTriggerThenIsPopulated() = runTest { + val parsed = "storeFormData_trigger_formSubmission".parseStoreFormDataJson() + assertEquals(FORM_SUBMISSION, parsed.trigger) + } + + @Test + fun whenStoreFormDataRequestHasPartialSaveTriggerThenIsPopulated() = runTest { + val parsed = "storeFormData_trigger_partialSave".parseStoreFormDataJson() + assertEquals(PARTIAL_SAVE, parsed.trigger) + } + private suspend fun String.parseStoreFormDataJson(): AutofillStoreFormDataRequest { val json = this.loadJsonFile() assertNotNull("Failed to load specified JSON file: $this") diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/partialsave/PartialCredentialSaveInMemoryStoreTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/partialsave/PartialCredentialSaveInMemoryStoreTest.kt new file mode 100644 index 000000000000..b20a796243b0 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/partialsave/PartialCredentialSaveInMemoryStoreTest.kt @@ -0,0 +1,150 @@ +package com.duckduckgo.autofill.impl.partialsave + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.autofill.impl.encoding.UrlUnicodeNormalizerImpl +import com.duckduckgo.autofill.impl.partialsave.PartialCredentialSaveInMemoryStore.Companion.MAX_VALIDITY_MS +import com.duckduckgo.autofill.impl.partialsave.PartialCredentialSaveInMemoryStore.Companion.TIME_WINDOW_FOR_BEING_RECENT_MS +import com.duckduckgo.autofill.impl.time.TimeProvider +import com.duckduckgo.autofill.impl.urlmatcher.AutofillDomainNameUrlMatcher +import com.duckduckgo.common.test.CoroutineTestRule +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class PartialCredentialSaveInMemoryStoreTest { + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val mockTimeProvider: TimeProvider = mock() + + private val testee = PartialCredentialSaveInMemoryStore( + urlMatcher = AutofillDomainNameUrlMatcher(unicodeNormalizer = UrlUnicodeNormalizerImpl()), + timeProvider = mockTimeProvider, + dispatchers = coroutineTestRule.testDispatcherProvider, + ) + + @Before + fun setup() { + setFixedTimestampForTimeProvider() + } + + @Test + fun whenNothingSavedThenReturnsNullUsername() = runTest { + assertNull(testee.consumeUsernameFromBackFill(URL)) + } + + @Test + fun whenEntrySavedALongTimeAgoWithMatchingEtldThenNullUsername() = runTest { + runWithSimulatedTimestamp(0L) { + testee.saveUsername(URL, "user") + } + assertNull(testee.consumeUsernameFromBackFill(URL)) + } + + @Test + fun whenEntrySaved3MinutesAnd1SecondAgoWithMatchingEtldThenNullUsername() = runTest { + // 1 second too long to be considered valid + val timestampTooOldToBeValid = timestampExactlyOnValidityWindow() - TimeUnit.SECONDS.toMillis(1) + runWithSimulatedTimestamp(timestampTooOldToBeValid) { + testee.saveUsername(URL, "user") + } + assertNull(testee.consumeUsernameFromBackFill(URL)) + } + + @Test + fun whenEntrySaved2MinutesAnd59SecondAgoWithMatchingEtldThenUsernameReturned() = runTest { + // still valid by only 1 second + val timestampWithinValidityWindow = timestampExactlyOnValidityWindow() + TimeUnit.SECONDS.toMillis(1) + runWithSimulatedTimestamp(timestampWithinValidityWindow) { + testee.saveUsername(URL, "user") + } + assertEquals("user", testee.consumeUsernameFromBackFill(URL)) + } + + @Test + fun whenEntrySavedExactly3MinutesAgoWithMatchingEtldThenUsernameReturned() = runTest { + runWithSimulatedTimestamp(timestampExactlyOnValidityWindow()) { + testee.saveUsername(URL, "user") + } + assertEquals("user", testee.consumeUsernameFromBackFill(URL)) + } + + @Test + fun whenUsernameRetrievedThenStillAvailableAfter() = runTest { + runWithSimulatedTimestamp(timestampExactlyOnValidityWindow()) { + testee.saveUsername(URL, "user") + } + assertEquals("user", testee.consumeUsernameFromBackFill(URL)) + + // check it's still there + assertEquals("user", testee.consumeUsernameFromBackFill(URL)) + } + + @Test + fun whenUsernameSavedButNotUsedThenRecentlyUsedCheckReturnsFalse() = runTest { + runWithSimulatedTimestamp(timestampExactlyOnValidityWindow()) { + testee.saveUsername(URL, "user") + } + assertFalse(testee.wasBackFilledRecently(URL, "user")) + } + + @Test + fun whenUsernameUsedRecentlyThenRecentlyUsedCheckReturnsTrue() = runTest { + runWithSimulatedTimestamp(timestampExactlyOnValidityWindow()) { + testee.saveUsername(URL, "user") + } + testee.consumeUsernameFromBackFill(URL) + assertTrue(testee.wasBackFilledRecently(URL, "user")) + } + + @Test + fun whenUsernameUsedButNotRecentlyThenRecentlyUsedCheckReturnsFalse() = runTest { + runWithSimulatedTimestamp(timestampExactlyOnValidityWindow()) { + testee.saveUsername(URL, "user") + } + + // outside window where we'd consider it 'recent' + runWithSimulatedTimestamp(timestampExactlyOnUsedRecentlyWindow() - TimeUnit.SECONDS.toMillis(1)) { + testee.consumeUsernameFromBackFill(URL) + } + + assertFalse(testee.wasBackFilledRecently(URL, "user")) + } + + @Test + fun whenEntrySavedButEldPlusOneNotAMatchThenNotReturned() = runTest { + runWithSimulatedTimestamp(timestampExactlyOnValidityWindow()) { + testee.saveUsername("example.org", "user") + } + assertNull(testee.consumeUsernameFromBackFill("example.com")) + } + + private suspend fun runWithSimulatedTimestamp( + timestamp: Long, + block: suspend () -> Unit, + ) { + whenever(mockTimeProvider.currentTimeMillis()).thenReturn(timestamp) + block() + setFixedTimestampForTimeProvider() + } + + private fun timestampExactlyOnValidityWindow(): Long = CURRENT_TIME_MS - MAX_VALIDITY_MS + private fun timestampExactlyOnUsedRecentlyWindow(): Long = CURRENT_TIME_MS - TIME_WINDOW_FOR_BEING_RECENT_MS + + private fun setFixedTimestampForTimeProvider() { + whenever(mockTimeProvider.currentTimeMillis()).thenReturn(CURRENT_TIME_MS) + } + + companion object { + private const val URL = "example.com" + private const val CURRENT_TIME_MS = 1733935378547L // 2024-12-11T16:43 + } +} diff --git a/autofill/autofill-impl/src/test/resources/json/storeFormData_passwordMissing_partialSave.json b/autofill/autofill-impl/src/test/resources/json/storeFormData_passwordMissing_partialSave.json new file mode 100644 index 000000000000..7b1d6981efae --- /dev/null +++ b/autofill/autofill-impl/src/test/resources/json/storeFormData_passwordMissing_partialSave.json @@ -0,0 +1,6 @@ +{ + "credentials": { + "username": "dax@duck.com" + }, + "trigger": "partialSave" +} \ No newline at end of file diff --git a/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_formSubmission.json b/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_formSubmission.json new file mode 100644 index 000000000000..1ea6a677efa9 --- /dev/null +++ b/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_formSubmission.json @@ -0,0 +1,7 @@ +{ + "credentials": { + "username": "dax@duck.com", + "password": "123456" + }, + "trigger": "formSubmission" +} \ No newline at end of file diff --git a/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_missing.json b/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_missing.json new file mode 100644 index 000000000000..68b344e9a307 --- /dev/null +++ b/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_missing.json @@ -0,0 +1,6 @@ +{ + "credentials": { + "username": "dax@duck.com", + "password": "123456" + } +} \ No newline at end of file diff --git a/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_partialSave.json b/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_partialSave.json new file mode 100644 index 000000000000..85daf42d98c4 --- /dev/null +++ b/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_partialSave.json @@ -0,0 +1,7 @@ +{ + "credentials": { + "username": "dax@duck.com", + "password": "123456" + }, + "trigger": "partialSave" +} \ No newline at end of file diff --git a/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_unknown.json b/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_unknown.json new file mode 100644 index 000000000000..c7a4d82bc9b6 --- /dev/null +++ b/autofill/autofill-impl/src/test/resources/json/storeFormData_trigger_unknown.json @@ -0,0 +1,7 @@ +{ + "credentials": { + "username": "dax@duck.com", + "password": "123456" + }, + "trigger": "unknown trigger" +} \ No newline at end of file diff --git a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js index b02ab092e3a5..042e81694d71 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js +++ b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js @@ -138,6 +138,11 @@ class ZodError extends Error { processError(this); return fieldErrors; } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } toString() { return this.message; } @@ -267,6 +272,13 @@ const makeIssue = params => { ...issueData, path: fullPath }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message + }; + } let errorMessage = ""; const maps = errorMaps.filter(m => !!m).slice().reverse(); for (const map of maps) { @@ -278,17 +290,18 @@ const makeIssue = params => { return { ...issueData, path: fullPath, - message: issueData.message || errorMessage + message: errorMessage }; }; exports.makeIssue = makeIssue; exports.EMPTY_PATH = []; function addIssueToContext(ctx, issueData) { + const overrideMap = (0, errors_1.getErrorMap)(); const issue = (0, exports.makeIssue)({ issueData: issueData, data: ctx.data, path: ctx.path, - errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, (0, errors_1.getErrorMap)(), en_1.default // then global default map + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, overrideMap, overrideMap === en_1.default ? undefined : en_1.default // then global default map ].filter(x => !!x) }); ctx.common.issues.push(issue); @@ -319,9 +332,11 @@ class ParseStatus { static async mergeObjectAsync(status, pairs) { const syncPairs = []; for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; syncPairs.push({ - key: await pair.key, - value: await pair.value + key, + value }); } return ParseStatus.mergeObjectSync(status, syncPairs); @@ -632,11 +647,23 @@ exports.default = errorMap; },{"../ZodError":2,"../helpers/util":8}],11:[function(require,module,exports){ "use strict"; +var __classPrivateFieldGet = void 0 && (void 0).__classPrivateFieldGet || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var __classPrivateFieldSet = void 0 && (void 0).__classPrivateFieldSet || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value), value; +}; +var _ZodEnum_cache, _ZodNativeEnum_cache; Object.defineProperty(exports, "__esModule", { value: true }); -exports.date = exports.boolean = exports.bigint = exports.array = exports.any = exports.coerce = exports.ZodFirstPartyTypeKind = exports.late = exports.ZodSchema = exports.Schema = exports.custom = exports.ZodReadonly = exports.ZodPipeline = exports.ZodBranded = exports.BRAND = exports.ZodNaN = exports.ZodCatch = exports.ZodDefault = exports.ZodNullable = exports.ZodOptional = exports.ZodTransformer = exports.ZodEffects = exports.ZodPromise = exports.ZodNativeEnum = exports.ZodEnum = exports.ZodLiteral = exports.ZodLazy = exports.ZodFunction = exports.ZodSet = exports.ZodMap = exports.ZodRecord = exports.ZodTuple = exports.ZodIntersection = exports.ZodDiscriminatedUnion = exports.ZodUnion = exports.ZodObject = exports.ZodArray = exports.ZodVoid = exports.ZodNever = exports.ZodUnknown = exports.ZodAny = exports.ZodNull = exports.ZodUndefined = exports.ZodSymbol = exports.ZodDate = exports.ZodBoolean = exports.ZodBigInt = exports.ZodNumber = exports.ZodString = exports.ZodType = void 0; -exports.NEVER = exports.void = exports.unknown = exports.union = exports.undefined = exports.tuple = exports.transformer = exports.symbol = exports.string = exports.strictObject = exports.set = exports.record = exports.promise = exports.preprocess = exports.pipeline = exports.ostring = exports.optional = exports.onumber = exports.oboolean = exports.object = exports.number = exports.nullable = exports.null = exports.never = exports.nativeEnum = exports.nan = exports.map = exports.literal = exports.lazy = exports.intersection = exports.instanceof = exports.function = exports.enum = exports.effect = exports.discriminatedUnion = void 0; +exports.boolean = exports.bigint = exports.array = exports.any = exports.coerce = exports.ZodFirstPartyTypeKind = exports.late = exports.ZodSchema = exports.Schema = exports.custom = exports.ZodReadonly = exports.ZodPipeline = exports.ZodBranded = exports.BRAND = exports.ZodNaN = exports.ZodCatch = exports.ZodDefault = exports.ZodNullable = exports.ZodOptional = exports.ZodTransformer = exports.ZodEffects = exports.ZodPromise = exports.ZodNativeEnum = exports.ZodEnum = exports.ZodLiteral = exports.ZodLazy = exports.ZodFunction = exports.ZodSet = exports.ZodMap = exports.ZodRecord = exports.ZodTuple = exports.ZodIntersection = exports.ZodDiscriminatedUnion = exports.ZodUnion = exports.ZodObject = exports.ZodArray = exports.ZodVoid = exports.ZodNever = exports.ZodUnknown = exports.ZodAny = exports.ZodNull = exports.ZodUndefined = exports.ZodSymbol = exports.ZodDate = exports.ZodBoolean = exports.ZodBigInt = exports.ZodNumber = exports.ZodString = exports.datetimeRegex = exports.ZodType = void 0; +exports.NEVER = exports.void = exports.unknown = exports.union = exports.undefined = exports.tuple = exports.transformer = exports.symbol = exports.string = exports.strictObject = exports.set = exports.record = exports.promise = exports.preprocess = exports.pipeline = exports.ostring = exports.optional = exports.onumber = exports.oboolean = exports.object = exports.number = exports.nullable = exports.null = exports.never = exports.nativeEnum = exports.nan = exports.map = exports.literal = exports.lazy = exports.intersection = exports.instanceof = exports.function = exports.enum = exports.effect = exports.discriminatedUnion = exports.date = void 0; const errors_1 = require("./errors"); const errorUtil_1 = require("./helpers/errorUtil"); const parseUtil_1 = require("./helpers/parseUtil"); @@ -698,16 +725,25 @@ function processCreateParams(params) { description }; const customMap = (iss, ctx) => { - if (iss.code !== "invalid_type") return { - message: ctx.defaultError - }; + var _a, _b; + const { + message + } = params; + if (iss.code === "invalid_enum_value") { + return { + message: message !== null && message !== void 0 ? message : ctx.defaultError + }; + } if (typeof ctx.data === "undefined") { return { - message: required_error !== null && required_error !== void 0 ? required_error : ctx.defaultError + message: (_a = message !== null && message !== void 0 ? message : required_error) !== null && _a !== void 0 ? _a : ctx.defaultError }; } + if (iss.code !== "invalid_type") return { + message: ctx.defaultError + }; return { - message: invalid_type_error !== null && invalid_type_error !== void 0 ? invalid_type_error : ctx.defaultError + message: (_b = message !== null && message !== void 0 ? message : invalid_type_error) !== null && _b !== void 0 ? _b : ctx.defaultError }; }; return { @@ -977,11 +1013,13 @@ exports.ZodType = ZodType; exports.Schema = ZodType; exports.ZodSchema = ZodType; const cuidRegex = /^c[^\s-]{8,}$/i; -const cuid2Regex = /^[a-z][a-z0-9]*$/; +const cuid2Regex = /^[0-9a-z]+$/; const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/; // const uuidRegex = // /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; // from https://stackoverflow.com/a/46181/1550155 // old version: too slow, didn't support unicode // const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; @@ -994,36 +1032,47 @@ const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA- // /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; // const emailRegex = // /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; -const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; // const emailRegex = // /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; // from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; let emojiRegex; -const ipv4Regex = /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; const ipv6Regex = /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; -// Adapted from https://stackoverflow.com/a/3143231 -const datetimeRegex = args => { +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + // let regex = `\\d{2}:\\d{2}:\\d{2}`; + let regex = `([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d`; if (args.precision) { - if (args.offset) { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${args.precision}}(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } else { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${args.precision}}Z$`); - } - } else if (args.precision === 0) { - if (args.offset) { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } else { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$`); - } - } else { - if (args.offset) { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } else { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`); - } + regex = `${regex}\\.\\d{${args.precision}}`; + } else if (args.precision == null) { + regex = `${regex}(\\.\\d+)?`; } -}; + return regex; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +exports.datetimeRegex = datetimeRegex; function isValidIP(ip, version) { if ((version === "v4" || !version) && ipv4Regex.test(ip)) { return true; @@ -1045,10 +1094,7 @@ class ZodString extends ZodType { code: ZodError_1.ZodIssueCode.invalid_type, expected: util_1.ZodParsedType.string, received: ctx.parsedType - } - // - ); - + }); return parseUtil_1.INVALID; } const status = new parseUtil_1.ParseStatus(); @@ -1139,6 +1185,16 @@ class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + validation: "nanoid", + code: ZodError_1.ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } } else if (check.kind === "cuid") { if (!cuidRegex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1247,6 +1303,38 @@ class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + code: ZodError_1.ZodIssueCode.invalid_string, + validation: "date", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + code: ZodError_1.ZodIssueCode.invalid_string, + validation: "time", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + validation: "duration", + code: ZodError_1.ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } } else if (check.kind === "ip") { if (!isValidIP(input.data, check.version)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1257,6 +1345,16 @@ class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + validation: "base64", + code: ZodError_1.ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } } else { util_1.util.assertNever(check); } @@ -1303,6 +1401,12 @@ class ZodString extends ZodType { ...errorUtil_1.errorUtil.errToObj(message) }); } + nanoid(message) { + return this._addCheck({ + kind: "nanoid", + ...errorUtil_1.errorUtil.errToObj(message) + }); + } cuid(message) { return this._addCheck({ kind: "cuid", @@ -1321,6 +1425,12 @@ class ZodString extends ZodType { ...errorUtil_1.errorUtil.errToObj(message) }); } + base64(message) { + return this._addCheck({ + kind: "base64", + ...errorUtil_1.errorUtil.errToObj(message) + }); + } ip(options) { return this._addCheck({ kind: "ip", @@ -1328,12 +1438,13 @@ class ZodString extends ZodType { }); } datetime(options) { - var _a; + var _a, _b; if (typeof options === "string") { return this._addCheck({ kind: "datetime", precision: null, offset: false, + local: false, message: options }); } @@ -1341,9 +1452,36 @@ class ZodString extends ZodType { kind: "datetime", precision: typeof (options === null || options === void 0 ? void 0 : options.precision) === "undefined" ? null : options === null || options === void 0 ? void 0 : options.precision, offset: (_a = options === null || options === void 0 ? void 0 : options.offset) !== null && _a !== void 0 ? _a : false, + local: (_b = options === null || options === void 0 ? void 0 : options.local) !== null && _b !== void 0 ? _b : false, + ...errorUtil_1.errorUtil.errToObj(options === null || options === void 0 ? void 0 : options.message) + }); + } + date(message) { + return this._addCheck({ + kind: "date", + message + }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options + }); + } + return this._addCheck({ + kind: "time", + precision: typeof (options === null || options === void 0 ? void 0 : options.precision) === "undefined" ? null : options === null || options === void 0 ? void 0 : options.precision, ...errorUtil_1.errorUtil.errToObj(options === null || options === void 0 ? void 0 : options.message) }); } + duration(message) { + return this._addCheck({ + kind: "duration", + ...errorUtil_1.errorUtil.errToObj(message) + }); + } regex(regex, message) { return this._addCheck({ kind: "regex", @@ -1428,6 +1566,15 @@ class ZodString extends ZodType { get isDatetime() { return !!this._def.checks.find(ch => ch.kind === "datetime"); } + get isDate() { + return !!this._def.checks.find(ch => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find(ch => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find(ch => ch.kind === "duration"); + } get isEmail() { return !!this._def.checks.find(ch => ch.kind === "email"); } @@ -1440,6 +1587,9 @@ class ZodString extends ZodType { get isUUID() { return !!this._def.checks.find(ch => ch.kind === "uuid"); } + get isNANOID() { + return !!this._def.checks.find(ch => ch.kind === "nanoid"); + } get isCUID() { return !!this._def.checks.find(ch => ch.kind === "cuid"); } @@ -1452,6 +1602,9 @@ class ZodString extends ZodType { get isIP() { return !!this._def.checks.find(ch => ch.kind === "ip"); } + get isBase64() { + return !!this._def.checks.find(ch => ch.kind === "base64"); + } get minLength() { let min = null; for (const ch of this._def.checks) { @@ -2442,9 +2595,10 @@ class ZodObject extends ZodType { const syncPairs = []; for (const pair of pairs) { const key = await pair.key; + const value = await pair.value; syncPairs.push({ key, - value: await pair.value, + value, alwaysSet: pair.alwaysSet }); } @@ -2814,15 +2968,25 @@ const getDiscriminator = type => { return type.options; } else if (type instanceof ZodNativeEnum) { // eslint-disable-next-line ban/ban - return Object.keys(type.enum); + return util_1.util.objectValues(type.enum); } else if (type instanceof ZodDefault) { return getDiscriminator(type._def.innerType); } else if (type instanceof ZodUndefined) { return [undefined]; } else if (type instanceof ZodNull) { return [null]; + } else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); } else { - return null; + return []; } }; class ZodDiscriminatedUnion extends ZodType { @@ -2886,7 +3050,7 @@ class ZodDiscriminatedUnion extends ZodType { // try { for (const type of options) { const discriminatorValues = getDiscriminator(type.shape[discriminator]); - if (!discriminatorValues) { + if (!discriminatorValues.length) { throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); } for (const value of discriminatorValues) { @@ -3123,7 +3287,8 @@ class ZodRecord extends ZodType { for (const key in ctx.data) { pairs.push({ key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), - value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)) + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data }); } if (ctx.common.async) { @@ -3511,6 +3676,10 @@ function createZodEnum(values, params) { }); } class ZodEnum extends ZodType { + constructor() { + super(...arguments); + _ZodEnum_cache.set(this, void 0); + } _parse(input) { if (typeof input.data !== "string") { const ctx = this._getOrReturnCtx(input); @@ -3522,7 +3691,10 @@ class ZodEnum extends ZodType { }); return parseUtil_1.INVALID; } - if (this._def.values.indexOf(input.data) === -1) { + if (!__classPrivateFieldGet(this, _ZodEnum_cache, "f")) { + __classPrivateFieldSet(this, _ZodEnum_cache, new Set(this._def.values), "f"); + } + if (!__classPrivateFieldGet(this, _ZodEnum_cache, "f").has(input.data)) { const ctx = this._getOrReturnCtx(input); const expectedValues = this._def.values; (0, parseUtil_1.addIssueToContext)(ctx, { @@ -3559,15 +3731,28 @@ class ZodEnum extends ZodType { return enumValues; } extract(values) { - return ZodEnum.create(values); + let newDef = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._def; + return ZodEnum.create(values, { + ...this._def, + ...newDef + }); } exclude(values) { - return ZodEnum.create(this.options.filter(opt => !values.includes(opt))); + let newDef = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._def; + return ZodEnum.create(this.options.filter(opt => !values.includes(opt)), { + ...this._def, + ...newDef + }); } } exports.ZodEnum = ZodEnum; +_ZodEnum_cache = new WeakMap(); ZodEnum.create = createZodEnum; class ZodNativeEnum extends ZodType { + constructor() { + super(...arguments); + _ZodNativeEnum_cache.set(this, void 0); + } _parse(input) { const nativeEnumValues = util_1.util.getValidEnumValues(this._def.values); const ctx = this._getOrReturnCtx(input); @@ -3580,7 +3765,10 @@ class ZodNativeEnum extends ZodType { }); return parseUtil_1.INVALID; } - if (nativeEnumValues.indexOf(input.data) === -1) { + if (!__classPrivateFieldGet(this, _ZodNativeEnum_cache, "f")) { + __classPrivateFieldSet(this, _ZodNativeEnum_cache, new Set(util_1.util.getValidEnumValues(this._def.values)), "f"); + } + if (!__classPrivateFieldGet(this, _ZodNativeEnum_cache, "f").has(input.data)) { const expectedValues = util_1.util.objectValues(nativeEnumValues); (0, parseUtil_1.addIssueToContext)(ctx, { received: ctx.data, @@ -3596,6 +3784,7 @@ class ZodNativeEnum extends ZodType { } } exports.ZodNativeEnum = ZodNativeEnum; +_ZodNativeEnum_cache = new WeakMap(); ZodNativeEnum.create = (values, params) => { return new ZodNativeEnum({ values: values, @@ -3665,32 +3854,34 @@ class ZodEffects extends ZodType { checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); if (effect.type === "preprocess") { const processed = effect.transform(ctx.data, checkCtx); - if (ctx.common.issues.length) { - return { - status: "dirty", - value: ctx.data - }; - } if (ctx.common.async) { - return Promise.resolve(processed).then(processed => { - return this._def.schema._parseAsync({ + return Promise.resolve(processed).then(async processed => { + if (status.value === "aborted") return parseUtil_1.INVALID; + const result = await this._def.schema._parseAsync({ data: processed, path: ctx.path, parent: ctx }); + if (result.status === "aborted") return parseUtil_1.INVALID; + if (result.status === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + if (status.value === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + return result; }); } else { - return this._def.schema._parseSync({ + if (status.value === "aborted") return parseUtil_1.INVALID; + const result = this._def.schema._parseSync({ data: processed, path: ctx.path, parent: ctx }); + if (result.status === "aborted") return parseUtil_1.INVALID; + if (result.status === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + if (status.value === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + return result; } } if (effect.type === "refinement") { - const executeRefinement = (acc - // effect: RefinementEffect - ) => { + const executeRefinement = acc => { const result = effect.refinement(acc, checkCtx); if (ctx.common.async) { return Promise.resolve(result); @@ -4013,10 +4204,16 @@ exports.ZodPipeline = ZodPipeline; class ZodReadonly extends ZodType { _parse(input) { const result = this._def.innerType._parse(input); - if ((0, parseUtil_1.isValid)(result)) { - result.value = Object.freeze(result.value); - } - return result; + const freeze = data => { + if ((0, parseUtil_1.isValid)(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return (0, parseUtil_1.isAsync)(result) ? result.then(data => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; } } exports.ZodReadonly = ZodReadonly; @@ -4027,7 +4224,7 @@ ZodReadonly.create = (type, params) => { ...processCreateParams(params) }); }; -const custom = function (check) { +function custom(check) { let params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; let /** @@ -4059,7 +4256,7 @@ const custom = function (check) { } }); return ZodAny.create(); -}; +} exports.custom = custom; exports.late = { object: ZodObject.lazycreate @@ -4113,7 +4310,7 @@ cls) { let params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { message: `Input not instance of ${cls.name}` }; - return (0, exports.custom)(data => data instanceof cls, params); + return custom(data => data instanceof cls, params); }; exports.instanceof = instanceOfType; const stringType = ZodString.create; @@ -4435,8 +4632,8 @@ class SchemaValidationError extends Error { } case 'invalid_union': { - for (let unionError of issue.unionErrors) { - for (let issue1 of unionError.issues) { + for (const unionError of issue.unionErrors) { + for (const issue1 of unionError.issues) { log(issue1); } } @@ -4448,7 +4645,7 @@ class SchemaValidationError extends Error { } } } - for (let error of errors) { + for (const error of errors) { log(error); } const message = [heading, 'please see the details above'].join('\n '); @@ -4571,8 +4768,8 @@ class DeviceApi { */ async request(deviceApiCall, options) { deviceApiCall.validateParams(); - let result = await this.transport.send(deviceApiCall, options); - let processed = deviceApiCall.preResultValidation(result); + const result = await this.transport.send(deviceApiCall, options); + const processed = deviceApiCall.preResultValidation(result); return deviceApiCall.validateResult(processed); } /** @@ -4660,44 +4857,44 @@ var _webkit = require("./webkit.js"); */ class Messaging { /** - * @param {WebkitMessagingConfig} config - */ + * @param {WebkitMessagingConfig} config + */ constructor(config) { this.transport = getTransport(config); } /** - * Send a 'fire-and-forget' message. - * @throws {Error} - * {@link MissingHandler} - * - * @example - * - * ``` - * const messaging = new Messaging(config) - * messaging.notify("foo", {bar: "baz"}) - * ``` - * @param {string} name - * @param {Record} [data] - */ + * Send a 'fire-and-forget' message. + * @throws {Error} + * {@link MissingHandler} + * + * @example + * + * ``` + * const messaging = new Messaging(config) + * messaging.notify("foo", {bar: "baz"}) + * ``` + * @param {string} name + * @param {Record} [data] + */ notify(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; this.transport.notify(name, data); } /** - * Send a request, and wait for a response - * @throws {Error} - * {@link MissingHandler} - * - * @example - * ``` - * const messaging = new Messaging(config) - * const response = await messaging.request("foo", {bar: "baz"}) - * ``` - * - * @param {string} name - * @param {Record} [data] - * @return {Promise} - */ + * Send a request, and wait for a response + * @throws {Error} + * {@link MissingHandler} + * + * @example + * ``` + * const messaging = new Messaging(config) + * const response = await messaging.request("foo", {bar: "baz"}) + * ``` + * + * @param {string} name + * @param {Record} [data] + * @return {Promise} + */ request(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return this.transport.request(name, data); @@ -4710,20 +4907,20 @@ class Messaging { exports.Messaging = Messaging; class MessagingTransport { /** - * @param {string} name - * @param {Record} [data] - * @returns {void} - */ + * @param {string} name + * @param {Record} [data] + * @returns {void} + */ // @ts-ignore - ignoring a no-unused ts error, this is only an interface. notify(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; throw new Error("must implement 'notify'"); } /** - * @param {string} name - * @param {Record} [data] - * @return {Promise} - */ + * @param {string} name + * @param {Record} [data] + * @return {Promise} + */ // @ts-ignore - ignoring a no-unused ts error, this is only an interface. request(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; @@ -4748,9 +4945,9 @@ function getTransport(config) { */ class MissingHandler extends Error { /** - * @param {string} message - * @param {string} handlerName - */ + * @param {string} message + * @param {string} handlerName + */ constructor(message, handlerName) { super(message); this.handlerName = handlerName; @@ -4856,8 +5053,8 @@ class WebkitMessagingTransport { config; globals; /** - * @param {WebkitMessagingConfig} config - */ + * @param {WebkitMessagingConfig} config + */ constructor(config) { this.config = config; this.globals = captureGlobals(); @@ -4866,11 +5063,11 @@ class WebkitMessagingTransport { } } /** - * Sends message to the webkit layer (fire and forget) - * @param {String} handler - * @param {*} data - * @internal - */ + * Sends message to the webkit layer (fire and forget) + * @param {String} handler + * @param {*} data + * @internal + */ wkSend(handler) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (!(handler in this.globals.window.webkit.messageHandlers)) { @@ -4894,12 +5091,12 @@ class WebkitMessagingTransport { } /** - * Sends message to the webkit layer and waits for the specified response - * @param {String} handler - * @param {*} data - * @returns {Promise<*>} - * @internal - */ + * Sends message to the webkit layer and waits for the specified response + * @param {String} handler + * @param {*} data + * @returns {Promise<*>} + * @internal + */ async wkSendAndWait(handler) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (this.config.hasModernWebkitAPI) { @@ -4940,27 +5137,27 @@ class WebkitMessagingTransport { } } /** - * @param {string} name - * @param {Record} [data] - */ + * @param {string} name + * @param {Record} [data] + */ notify(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; this.wkSend(name, data); } /** - * @param {string} name - * @param {Record} [data] - */ + * @param {string} name + * @param {Record} [data] + */ request(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return this.wkSendAndWait(name, data); } /** - * Generate a random method name and adds it to the global scope - * The native layer will use this method to send the response - * @param {string | number} randomMethodName - * @param {Function} callback - */ + * Generate a random method name and adds it to the global scope + * The native layer will use this method to send the response + * @param {string | number} randomMethodName + * @param {Function} callback + */ generateRandomMethod(randomMethodName, callback) { var _this = this; this.globals.ObjectDefineProperty(this.globals.window, randomMethodName, { @@ -4969,8 +5166,8 @@ class WebkitMessagingTransport { configurable: true, writable: false, /** - * @param {any[]} args - */ + * @param {any[]} args + */ value: function () { callback(...arguments); // @ts-ignore - we want this to throw if it fails as it would indicate a fatal error. @@ -4986,16 +5183,16 @@ class WebkitMessagingTransport { } /** - * @type {{name: string, length: number}} - */ + * @type {{name: string, length: number}} + */ algoObj = { name: 'AES-GCM', length: 256 }; /** - * @returns {Promise} - */ + * @returns {Promise} + */ async createRandKey() { const key = await this.globals.generateKey(this.algoObj, true, ['encrypt', 'decrypt']); const exportedKey = await this.globals.exportKey('raw', key); @@ -5003,44 +5200,44 @@ class WebkitMessagingTransport { } /** - * @returns {Uint8Array} - */ + * @returns {Uint8Array} + */ createRandIv() { return this.globals.getRandomValues(new this.globals.Uint8Array(12)); } /** - * @param {BufferSource} ciphertext - * @param {BufferSource} key - * @param {Uint8Array} iv - * @returns {Promise} - */ + * @param {BufferSource} ciphertext + * @param {BufferSource} key + * @param {Uint8Array} iv + * @returns {Promise} + */ async decrypt(ciphertext, key, iv) { const cryptoKey = await this.globals.importKey('raw', key, 'AES-GCM', false, ['decrypt']); const algo = { name: 'AES-GCM', iv }; - let decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext); - let dec = new this.globals.TextDecoder(); + const decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext); + const dec = new this.globals.TextDecoder(); return dec.decode(decrypted); } /** - * When required (such as on macos 10.x), capture the `postMessage` method on - * each webkit messageHandler - * - * @param {string[]} handlerNames - */ + * When required (such as on macos 10.x), capture the `postMessage` method on + * each webkit messageHandler + * + * @param {string[]} handlerNames + */ captureWebkitHandlers(handlerNames) { const handlers = window.webkit.messageHandlers; if (!handlers) throw new _messaging.MissingHandler('window.webkit.messageHandlers was absent', 'all'); - for (let webkitMessageHandlerName of handlerNames) { + for (const webkitMessageHandlerName of handlerNames) { if (typeof handlers[webkitMessageHandlerName]?.postMessage === 'function') { /** - * `bind` is used here to ensure future calls to the captured - * `postMessage` have the correct `this` context - */ + * `bind` is used here to ensure future calls to the captured + * `postMessage` have the correct `this` context + */ const original = handlers[webkitMessageHandlerName]; const bound = handlers[webkitMessageHandlerName].postMessage?.bind(original); this.globals.capturedWebkitHandlers[webkitMessageHandlerName] = bound; @@ -5069,11 +5266,11 @@ class WebkitMessagingTransport { exports.WebkitMessagingTransport = WebkitMessagingTransport; class WebkitMessagingConfig { /** - * @param {object} params - * @param {boolean} params.hasModernWebkitAPI - * @param {string[]} params.webkitMessageHandlerNames - * @param {string} params.secret - */ + * @param {object} params + * @param {boolean} params.hasModernWebkitAPI + * @param {string[]} params.webkitMessageHandlerNames + * @param {string} params.secret + */ constructor(params) { /** * Whether or not the current WebKit Platform supports secure messaging @@ -5081,13 +5278,13 @@ class WebkitMessagingConfig { */ this.hasModernWebkitAPI = params.hasModernWebkitAPI; /** - * A list of WebKit message handler names that a user script can send - */ + * A list of WebKit message handler names that a user script can send + */ this.webkitMessageHandlerNames = params.webkitMessageHandlerNames; /** - * A string provided by native platforms to be sent with future outgoing - * messages - */ + * A string provided by native platforms to be sent with future outgoing + * messages + */ this.secret = params.secret; } } @@ -5099,28 +5296,28 @@ class WebkitMessagingConfig { exports.WebkitMessagingConfig = WebkitMessagingConfig; class SecureMessagingParams { /** - * @param {object} params - * @param {string} params.methodName - * @param {string} params.secret - * @param {number[]} params.key - * @param {number[]} params.iv - */ + * @param {object} params + * @param {string} params.methodName + * @param {string} params.secret + * @param {number[]} params.key + * @param {number[]} params.iv + */ constructor(params) { /** * The method that's been appended to `window` to be called later */ this.methodName = params.methodName; /** - * The secret used to ensure message sender validity - */ + * The secret used to ensure message sender validity + */ this.secret = params.secret; /** - * The CipherKey as number[] - */ + * The CipherKey as number[] + */ this.key = params.key; /** - * The Initial Vector as number[] - */ + * The Initial Vector as number[] + */ this.iv = params.iv; } } @@ -5331,7 +5528,7 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && * }} PasswordParameters */ const defaults = Object.freeze({ - SCAN_SET_ORDER: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-~!@#$%^&*_+=`|(){}[:;\\\"'<>,.?/ ]", + SCAN_SET_ORDER: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-~!@#$%^&*_+=`|(){}[:;\\"\'<>,.?/ ]', defaultUnambiguousCharacters: 'abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ0123456789', defaultPasswordLength: _constants.constants.DEFAULT_MIN_LENGTH, defaultPasswordRules: _constants.constants.DEFAULT_PASSWORD_RULES, @@ -5458,7 +5655,7 @@ class Password { _requirementsFromRules(passwordRules) { /** @type {Requirements} */ const requirements = {}; - for (let rule of passwordRules) { + for (const rule of passwordRules) { if (rule.name === parser.RuleName.ALLOWED) { console.assert(!('PasswordAllowedCharacters' in requirements)); const chars = this._charactersFromCharactersClasses(rule.value); @@ -5789,7 +5986,7 @@ class Password { */ _charactersFromCharactersClasses(characterClasses) { const output = []; - for (let characterClass of characterClasses) { + for (const characterClass of characterClasses) { output.push(...this._scanSetFromCharacterClass(characterClass)); } return output; @@ -5803,9 +6000,9 @@ class Password { if (!characters.length) { return ''; } - let shadowCharacters = Array.prototype.slice.call(characters); + const shadowCharacters = Array.prototype.slice.call(characters); shadowCharacters.sort((a, b) => this.options.SCAN_SET_ORDER.indexOf(a) - this.options.SCAN_SET_ORDER.indexOf(b)); - let uniqueCharacters = [shadowCharacters[0]]; + const uniqueCharacters = [shadowCharacters[0]]; for (let i = 1, length = shadowCharacters.length; i < length; ++i) { if (shadowCharacters[i] === shadowCharacters[i - 1]) { continue; @@ -5845,6 +6042,7 @@ Object.defineProperty(exports, "__esModule", { }); exports.SHOULD_NOT_BE_REACHED = exports.RuleName = exports.Rule = exports.ParserError = exports.NamedCharacterClass = exports.Identifier = exports.CustomCharacterClass = void 0; exports.parsePasswordRules = parsePasswordRules; +/* eslint-disable no-var */ // Copyright (c) 2019 - 2020 Apple Inc. Licensed under MIT License. /* @@ -5897,7 +6095,6 @@ class Rule { } } exports.Rule = Rule; -; class NamedCharacterClass { constructor(name) { console.assert(_isValidRequiredOrAllowedPropertyValueIdentifier(name)); @@ -5914,10 +6111,8 @@ class NamedCharacterClass { } } exports.NamedCharacterClass = NamedCharacterClass; -; class ParserError extends Error {} exports.ParserError = ParserError; -; class CustomCharacterClass { constructor(characters) { console.assert(characters instanceof Array); @@ -5933,14 +6128,11 @@ class CustomCharacterClass { return `[${this._characters.join('').replace('"', '"')}]`; } } -exports.CustomCharacterClass = CustomCharacterClass; -; // MARK: Lexer functions - +exports.CustomCharacterClass = CustomCharacterClass; function _isIdentifierCharacter(c) { console.assert(c.length === 1); - // eslint-disable-next-line no-mixed-operators return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c === '-'; } function _isASCIIDigit(c) { @@ -5986,14 +6178,14 @@ function _markBitsForNamedCharacterClass(bitSet, namedCharacterClass) { } } function _markBitsForCustomCharacterClass(bitSet, customCharacterClass) { - for (let character of customCharacterClass.characters) { + for (const character of customCharacterClass.characters) { bitSet[_bitSetIndexForCharacter(character)] = true; } } function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFormatCompliant) { // @ts-ignore - let asciiPrintableBitSet = new Array('~'.codePointAt(0) - ' '.codePointAt(0) + 1); - for (let propertyValue of propertyValues) { + const asciiPrintableBitSet = new Array('~'.codePointAt(0) - ' '.codePointAt(0) + 1); + for (const propertyValue of propertyValues) { if (propertyValue instanceof NamedCharacterClass) { if (propertyValue.name === Identifier.UNICODE) { return [new NamedCharacterClass(Identifier.UNICODE)]; @@ -6008,32 +6200,32 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo } let charactersSeen = []; function checkRange(start, end) { - let temp = []; + const temp = []; for (let i = _bitSetIndexForCharacter(start); i <= _bitSetIndexForCharacter(end); ++i) { if (asciiPrintableBitSet[i]) { temp.push(_characterAtBitSetIndex(i)); } } - let result = temp.length === _bitSetIndexForCharacter(end) - _bitSetIndexForCharacter(start) + 1; + const result = temp.length === _bitSetIndexForCharacter(end) - _bitSetIndexForCharacter(start) + 1; if (!result) { charactersSeen = charactersSeen.concat(temp); } return result; } - let hasAllUpper = checkRange('A', 'Z'); - let hasAllLower = checkRange('a', 'z'); - let hasAllDigits = checkRange('0', '9'); + const hasAllUpper = checkRange('A', 'Z'); + const hasAllLower = checkRange('a', 'z'); + const hasAllDigits = checkRange('0', '9'); // Check for special characters, accounting for characters that are given special treatment (i.e. '-' and ']') let hasAllSpecial = false; let hasDash = false; let hasRightSquareBracket = false; - let temp = []; + const temp = []; for (let i = _bitSetIndexForCharacter(' '); i <= _bitSetIndexForCharacter('/'); ++i) { if (!asciiPrintableBitSet[i]) { continue; } - let character = _characterAtBitSetIndex(i); + const character = _characterAtBitSetIndex(i); if (keepCustomCharacterClassFormatCompliant && character === '-') { hasDash = true; } else { @@ -6049,7 +6241,7 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo if (!asciiPrintableBitSet[i]) { continue; } - let character = _characterAtBitSetIndex(i); + const character = _characterAtBitSetIndex(i); if (keepCustomCharacterClassFormatCompliant && character === ']') { hasRightSquareBracket = true; } else { @@ -6067,12 +6259,12 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo if (hasRightSquareBracket) { temp.push(']'); } - let numberOfSpecialCharacters = _bitSetIndexForCharacter('/') - _bitSetIndexForCharacter(' ') + 1 + (_bitSetIndexForCharacter('@') - _bitSetIndexForCharacter(':') + 1) + (_bitSetIndexForCharacter('`') - _bitSetIndexForCharacter('[') + 1) + (_bitSetIndexForCharacter('~') - _bitSetIndexForCharacter('{') + 1); + const numberOfSpecialCharacters = _bitSetIndexForCharacter('/') - _bitSetIndexForCharacter(' ') + 1 + (_bitSetIndexForCharacter('@') - _bitSetIndexForCharacter(':') + 1) + (_bitSetIndexForCharacter('`') - _bitSetIndexForCharacter('[') + 1) + (_bitSetIndexForCharacter('~') - _bitSetIndexForCharacter('{') + 1); hasAllSpecial = temp.length === numberOfSpecialCharacters; if (!hasAllSpecial) { charactersSeen = charactersSeen.concat(temp); } - let result = []; + const result = []; if (hasAllUpper && hasAllLower && hasAllDigits && hasAllSpecial) { return [new NamedCharacterClass(Identifier.ASCII_PRINTABLE)]; } @@ -6100,7 +6292,7 @@ function _indexOfNonWhitespaceCharacter(input) { let position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; console.assert(position >= 0); console.assert(position <= input.length); - let length = input.length; + const length = input.length; while (position < length && _isASCIIWhitespace(input[position])) { ++position; } @@ -6110,10 +6302,10 @@ function _parseIdentifier(input, position) { console.assert(position >= 0); console.assert(position < input.length); console.assert(_isIdentifierCharacter(input[position])); - let length = input.length; - let seenIdentifiers = []; + const length = input.length; + const seenIdentifiers = []; do { - let c = input[position]; + const c = input[position]; if (!_isIdentifierCharacter(c)) { break; } @@ -6129,16 +6321,16 @@ function _parseCustomCharacterClass(input, position) { console.assert(position >= 0); console.assert(position < input.length); console.assert(input[position] === CHARACTER_CLASS_START_SENTINEL); - let length = input.length; + const length = input.length; ++position; if (position >= length) { // console.error('Found end-of-line instead of character class character') return [null, position]; } - let initialPosition = position; - let result = []; + const initialPosition = position; + const result = []; do { - let c = input[position]; + const c = input[position]; if (!_isASCIIPrintableCharacter(c)) { ++position; continue; @@ -6174,11 +6366,11 @@ function _parseCustomCharacterClass(input, position) { function _parsePasswordRequiredOrAllowedPropertyValue(input, position) { console.assert(position >= 0); console.assert(position < input.length); - let length = input.length; - let propertyValues = []; + const length = input.length; + const propertyValues = []; while (true) { if (_isIdentifierCharacter(input[position])) { - let identifierStartPosition = position; + const identifierStartPosition = position; // eslint-disable-next-line no-redeclare var [propertyValue, position] = _parseIdentifier(input, position); if (!_isValidRequiredOrAllowedPropertyValueIdentifier(propertyValue)) { @@ -6225,8 +6417,8 @@ function _parsePasswordRule(input, position) { console.assert(position >= 0); console.assert(position < input.length); console.assert(_isIdentifierCharacter(input[position])); - let length = input.length; - var mayBeIdentifierStartPosition = position; + const length = input.length; + const mayBeIdentifierStartPosition = position; // eslint-disable-next-line no-redeclare var [identifier, position] = _parseIdentifier(input, position); if (!Object.values(RuleName).includes(identifier)) { @@ -6241,7 +6433,7 @@ function _parsePasswordRule(input, position) { // console.error('Failed to find start of property value: ' + input.substr(position)) return [null, position, undefined]; } - let property = { + const property = { name: identifier, value: null }; @@ -6297,7 +6489,7 @@ function _parseInteger(input, position) { // console.error('Failed to parse value of type integer; not a number: ' + input.substr(position)) return [null, position]; } - let length = input.length; + const length = input.length; // let initialPosition = position let result = 0; do { @@ -6318,8 +6510,8 @@ function _parseInteger(input, position) { * @private */ function _parsePasswordRulesInternal(input) { - let parsedProperties = []; - let length = input.length; + const parsedProperties = []; + const length = input.length; var position = _indexOfNonWhitespaceCharacter(input); while (position < length) { if (!_isIdentifierCharacter(input[position])) { @@ -6356,7 +6548,7 @@ function _parsePasswordRulesInternal(input) { * @returns {Rule[]} */ function parsePasswordRules(input, formatRulesForMinifiedVersion) { - let [passwordRules, maybeMessage] = _parsePasswordRulesInternal(input); + const [passwordRules, maybeMessage] = _parsePasswordRulesInternal(input); if (!passwordRules) { throw new ParserError(maybeMessage); } @@ -6366,13 +6558,13 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { // When formatting rules for minified version, we should keep the formatted rules // as similar to the input as possible. Avoid copying required rules to allowed rules. - let suppressCopyingRequiredToAllowed = formatRulesForMinifiedVersion; - let requiredRules = []; + const suppressCopyingRequiredToAllowed = formatRulesForMinifiedVersion; + const requiredRules = []; let newAllowedValues = []; let minimumMaximumConsecutiveCharacters = null; let maximumMinLength = 0; let minimumMaxLength = null; - for (let rule of passwordRules) { + for (const rule of passwordRules) { switch (rule.name) { case RuleName.MAX_CONSECUTIVE: minimumMaximumConsecutiveCharacters = minimumMaximumConsecutiveCharacters ? Math.min(rule.value, minimumMaximumConsecutiveCharacters) : rule.value; @@ -6405,10 +6597,10 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { if (minimumMaximumConsecutiveCharacters !== null) { newPasswordRules.push(new Rule(RuleName.MAX_CONSECUTIVE, minimumMaximumConsecutiveCharacters)); } - let sortedRequiredRules = requiredRules.sort(function (a, b) { + const sortedRequiredRules = requiredRules.sort(function (a, b) { const namedCharacterClassOrder = [Identifier.LOWER, Identifier.UPPER, Identifier.DIGIT, Identifier.SPECIAL, Identifier.ASCII_PRINTABLE, Identifier.UNICODE]; - let aIsJustOneNamedCharacterClass = a.value.length === 1 && a.value[0] instanceof NamedCharacterClass; - let bIsJustOneNamedCharacterClass = b.value.length === 1 && b.value[0] instanceof NamedCharacterClass; + const aIsJustOneNamedCharacterClass = a.value.length === 1 && a.value[0] instanceof NamedCharacterClass; + const bIsJustOneNamedCharacterClass = b.value.length === 1 && b.value[0] instanceof NamedCharacterClass; if (aIsJustOneNamedCharacterClass && !bIsJustOneNamedCharacterClass) { return -1; } @@ -6416,8 +6608,8 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { return 1; } if (aIsJustOneNamedCharacterClass && bIsJustOneNamedCharacterClass) { - let aIndex = namedCharacterClassOrder.indexOf(a.value[0].name); - let bIndex = namedCharacterClassOrder.indexOf(b.value[0].name); + const aIndex = namedCharacterClassOrder.indexOf(a.value[0].name); + const bIndex = namedCharacterClassOrder.indexOf(b.value[0].name); return aIndex - bIndex; } return 0; @@ -7062,6 +7254,9 @@ module.exports={ "keldoc.com": { "password-rules": "minlength: 12; required: lower; required: upper; required: digit; required: [!@#$%^&*];" }, + "kennedy-center.org": { + "password-rules": "minlength: 8; required: lower; required: upper; required: digit; required: [!#$%&*?@];" + }, "key.harvard.edu": { "password-rules": "minlength: 10; maxlength: 100; required: lower; required: upper; required: digit; allowed: [-@_#!&$`%*+()./,;~:{}|?>=<^[']];" }, @@ -7619,7 +7814,7 @@ function createDevice() { }; // Create the DeviceAPI + Setting - let deviceApi = new _index.DeviceApi(globalConfig.isDDGTestMode ? loggingTransport : transport); + const deviceApi = new _index.DeviceApi(globalConfig.isDDGTestMode ? loggingTransport : transport); const settings = new _Settings.Settings(globalConfig, deviceApi); if (globalConfig.isWindows) { if (globalConfig.isTopFrame) { @@ -7753,9 +7948,9 @@ class AndroidInterface extends _InterfacePrototype.default { } /** - * Used by the email web app - * Provides functionality to log the user out - */ + * Used by the email web app + * Provides functionality to log the user out + */ removeUserData() { try { return window.EmailInterface.removeCredentials(); @@ -9024,14 +9219,16 @@ class InterfacePrototype { }); break; default: - // Also fire pixel when filling an identity with the personal duck address from an email field - const checks = [subtype === 'emailAddress', this.hasLocalAddresses, data?.emailAddress === (0, _autofillUtils.formatDuckAddress)(this.#addresses.personalAddress)]; - if (checks.every(Boolean)) { - this.firePixel({ - pixelName: 'autofill_personal_address' - }); + { + // Also fire pixel when filling an identity with the personal duck address from an email field + const checks = [subtype === 'emailAddress', this.hasLocalAddresses, data?.emailAddress === (0, _autofillUtils.formatDuckAddress)(this.#addresses.personalAddress)]; + if (checks.every(Boolean)) { + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + } + break; } - break; } } // some platforms do not include a `success` object, why? @@ -9279,13 +9476,17 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials || {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, username: this.emailProtection.lastGenerated }); - this.storeFormData(formData, 'formSubmission'); + + // If credentials has only username field, and no password field, then trigger is a partialSave + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; + this.storeFormData(formData, trigger); } } @@ -9706,6 +9907,7 @@ function initFormSubmissionsApi(forms, matching) { // @ts-ignore if (btns.find(btn => btn.contains(realTarget))) return true; + return false; }); matchingForm?.submitHandler('global pointerdown event + matching form'); if (!matchingForm) { @@ -9801,7 +10003,7 @@ function overlayApi(device) { * @returns {Promise} */ async selectedDetail(data, type) { - let detailsEntries = Object.entries(data).map(_ref => { + const detailsEntries = Object.entries(data).map(_ref => { let [key, value] = _ref; return [key, String(value)]; }); @@ -10095,7 +10297,7 @@ class Form { if (!input.classList.contains('ddg-autofilled')) return; (0, _autofillUtils.removeInlineStyles)(input, (0, _inputStyles.getIconStylesAutofilled)(input, this)); (0, _autofillUtils.removeInlineStyles)(input, { - 'cursor': 'pointer' + cursor: 'pointer' }); input.classList.remove('ddg-autofilled'); this.addAutofillStyles(input); @@ -10216,20 +10418,10 @@ class Form { if (this.form.matches(selector)) { this.addInput(this.form); } else { - /** @type {Element[] | NodeList} */ - let foundInputs = []; - // Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable. - if (this.form instanceof HTMLFormElement && this.form.elements != null && Symbol.iterator in Object(this.form.elements)) { - // For form elements we use .elements to catch fields outside the form itself using the form attribute. - // It also catches all elements when the markup is broken. - // We use .filter to avoid fieldset, button, textarea etc. - const formElements = [...this.form.elements].filter(el => el.matches(selector)); - // If there are no form elements, we try to look for all - // enclosed elements within the form. - foundInputs = formElements.length > 0 ? formElements : (0, _autofillUtils.findEnclosedElements)(this.form, selector); - } else { - foundInputs = this.form.querySelectorAll(selector); - } + // Attempt to get form's control elements first as it can catch elements when markup is broke, or if the fields are outside the form. + // Other wise use queryElementsWithShadow, that can scan for shadow tree. + const formControlElements = (0, _autofillUtils.getFormControlElements)(this.form, selector); + const foundInputs = formControlElements != null ? [...formControlElements, ...(0, _autofillUtils.findElementsInShadowTree)(this.form, selector)] : (0, _autofillUtils.queryElementsWithShadow)(this.form, selector, true); if (foundInputs.length < MAX_INPUTS_PER_FORM) { foundInputs.forEach(input => this.addInput(input)); } else { @@ -10290,7 +10482,7 @@ class Form { } get submitButtons() { const selector = this.matching.cssSelector('submitButtonSelector'); - const allButtons = /** @type {HTMLElement[]} */(0, _autofillUtils.findEnclosedElements)(this.form, selector); + const allButtons = /** @type {HTMLElement[]} */(0, _autofillUtils.queryElementsWithShadow)(this.form, selector); return allButtons.filter(btn => (0, _autofillUtils.isPotentiallyViewable)(btn) && (0, _autofillUtils.isLikelyASubmitButton)(btn, this.matching) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); } attemptSubmissionIfNeeded() { @@ -10413,12 +10605,12 @@ class Form { if ((0, _autofillUtils.wasAutofilledByChrome)(input)) return; if ((0, _autofillUtils.isEventWithinDax)(e, e.target)) { (0, _autofillUtils.addInlineStyles)(e.target, { - 'cursor': 'pointer', + cursor: 'pointer', ...onMouseMove }); } else { (0, _autofillUtils.removeInlineStyles)(e.target, { - 'cursor': 'pointer' + cursor: 'pointer' }); // Only overwrite active icon styles if tooltip is closed if (!this.device.isTooltipActive()) { @@ -10430,7 +10622,7 @@ class Form { }); this.addListener(input, 'mouseleave', e => { (0, _autofillUtils.removeInlineStyles)(e.target, { - 'cursor': 'pointer' + cursor: 'pointer' }); // Only overwrite active icon styles if tooltip is closed if (!this.device.isTooltipActive()) { @@ -10528,7 +10720,7 @@ class Form { this.touched.add(input); this.device.attachTooltip({ form: this, - input: input, + input, click: clickCoords, trigger: 'userInitiated', triggerMetaData: { @@ -10757,7 +10949,7 @@ class Form { }, 'credentials'); this.device.attachTooltip({ form: this, - input: input, + input, click: null, trigger: 'autoprompt', triggerMetaData: { @@ -10992,6 +11184,23 @@ class FormAnalyzer { } }); } + + /** + * Function that checks if the element is an external link or a custom web element that + * encapsulates a link. + * @param {any} el + * @returns {boolean} + */ + isElementExternalLink(el) { + // Checks if the element is present in the cusotm elements registry and ends with a '-link' suffix. + // If it does, it checks if it contains an anchor element inside. + const tagName = el.nodeName.toLowerCase(); + const isCustomWebElementLink = customElements?.get(tagName) != null && /-link$/.test(tagName) && (0, _autofillUtils.findElementsInShadowTree)(el, 'a').length > 0; + + // if an external link matches one of the regexes, we assume the match is not pertinent to the current form + const isElementLink = el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]'); + return isCustomWebElementLink || isElementLink; + } evaluateElement(el) { const string = (0, _autofillUtils.getTextShallow)(el); if (el.matches(this.matching.cssSelector('password'))) { @@ -11012,7 +11221,7 @@ class FormAnalyzer { if (likelyASubmit) { this.form.querySelectorAll('input[type=submit], button[type=submit]').forEach(submit => { // If there is another element marked as submit and this is not, flip back to false - if (el.type !== 'submit' && el !== submit) { + if (el.getAttribute('type') !== 'submit' && el !== submit) { likelyASubmit = false; } }); @@ -11031,8 +11240,7 @@ class FormAnalyzer { }); return; } - // if an external link matches one of the regexes, we assume the match is not pertinent to the current form - if (el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]')) { + if (this.isElementExternalLink(el)) { let shouldFlip = true; let strength = 1; // Don't flip forgotten password links @@ -11051,9 +11259,10 @@ class FormAnalyzer { }); } else { // any other case + const isH1Element = el.tagName === 'H1'; this.updateSignal({ string, - strength: 1, + strength: isH1Element ? 3 : 1, signalType: `generic: ${string}`, shouldCheckUnifiedForm: true }); @@ -11071,7 +11280,7 @@ class FormAnalyzer { // Check form contents (noisy elements are skipped with the safeUniversalSelector) const selector = this.matching.cssSelector('safeUniversalSelector'); - const formElements = (0, _autofillUtils.findEnclosedElements)(this.form, selector); + const formElements = (0, _autofillUtils.queryElementsWithShadow)(this.form, selector); for (let i = 0; i < formElements.length; i++) { // Safety cutoff to avoid huge DOMs freezing the browser if (i >= 200) break; @@ -11131,7 +11340,7 @@ class FormAnalyzer { } // Match form textContent against common cc fields (includes hidden labels) - const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); + const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/gi); // De-dupe matches to avoid counting the same element more than once const deDupedMatches = new Set(textMatches?.map(match => match.toLowerCase())); @@ -11450,7 +11659,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { Anguilla: 'AI', Albania: 'AL', Armenia: 'AM', - 'Curaçao': 'CW', + Curaçao: 'CW', Angola: 'AO', Antarctica: 'AQ', Argentina: 'AR', @@ -11639,7 +11848,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { Paraguay: 'PY', Qatar: 'QA', 'Outlying Oceania': 'QO', - 'Réunion': 'RE', + Réunion: 'RE', Zimbabwe: 'ZW', Romania: 'RO', Russia: 'SU', @@ -11716,6 +11925,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -11888,21 +12098,10 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = _ref3 => { - let { - credentials - } = _ref3; - return Boolean(credentials.password); -}; - -/** - * @param {InternalDataStorageObject} credentials - * @return {boolean} - */ -const shouldStoreIdentities = _ref4 => { +const shouldStoreIdentities = _ref3 => { let { identities - } = _ref4; + } = _ref3; return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; @@ -11910,10 +12109,10 @@ const shouldStoreIdentities = _ref4 => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreCreditCards = _ref5 => { +const shouldStoreCreditCards = _ref4 => { let { creditCards - } = _ref5; + } = _ref4; if (!creditCards.cardNumber) return false; if (creditCards.cardSecurityCode) return true; // Some forms (Amazon) don't have the cvv, so we still save if there's the expiration @@ -11949,14 +12148,14 @@ const prepareFormValuesForStorage = formValues => { creditCards.cardName = identities?.fullName || formatFullName(identities); } - /** Fixes for credentials **/ - // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues)) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + /** Fixes for credentials */ + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress || identities.phone; + } + + // If we still don't have any credentials, we discard the object + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -12012,7 +12211,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ +},{"../autofill-utils.js":64,"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12055,7 +12254,7 @@ const getBasicStyles = (input, icon) => ({ 'background-repeat': 'no-repeat', 'background-origin': 'content-box', 'background-image': `url(${icon})`, - 'transition': 'background 0s' + transition: 'background 0s' }); /** @@ -12098,7 +12297,7 @@ const getIconStylesAutofilled = (input, form) => { return { ...iconStyle, 'background-color': '#F8F498', - 'color': '#333333' + color: '#333333' }; }; exports.getIconStylesAutofilled = getIconStylesAutofilled; @@ -12386,14 +12585,14 @@ const extractElementStrings = element => { // only take the string when it's an explicit text node if (el.nodeType === el.TEXT_NODE || !el.childNodes.length) { - let trimmedText = (0, _matching.removeExcessWhitespace)(el.textContent); + const trimmedText = (0, _matching.removeExcessWhitespace)(el.textContent); if (trimmedText) { strings.add(trimmedText); } return; } - for (let node of el.childNodes) { - let nodeType = node.nodeType; + for (const node of el.childNodes) { + const nodeType = node.nodeType; if (nodeType !== node.ELEMENT_NODE && nodeType !== node.TEXT_NODE) { continue; } @@ -12795,7 +12994,7 @@ const matchingConfiguration = exports.matchingConfiguration = { match: /sign.?up|join|register|enroll|(create|new).+account|newsletter|subscri(be|ption)|settings|preferences|profile|update|iscri(viti|zione)|registra(ti|zione)|(?:nuovo|crea(?:zione)?) account|contatt(?:ac)?i|sottoscriv|sottoscrizione|impostazioni|preferenze|aggiorna|anmeld(en|ung)|registrier(en|ung)|neukunde|neuer (kunde|benutzer|nutzer)|registreren|eigenschappen|profiel|bijwerken|s.inscrire|inscription|s.abonner|abonnement|préférences|profil|créer un compte|regis(trarse|tro)|regístrate|inscr(ibirse|ipción|íbete)|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo|registrer(a|ing)|(nytt|öppna) konto|nyhetsbrev|prenumer(era|ation)|kontakt|skapa|starta|inställningar|min (sida|kundvagn)|uppdatera/iu }, resetPasswordLink: { - match: /(forgot(ten)?|reset|don't remember) (your )?password|password forgotten|password dimenticata|reset(?:ta) password|recuper[ao] password|(vergessen|verloren|verlegt|wiederherstellen) passwort|wachtwoord (vergeten|reset)|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)|glömt lösenord|återställ lösenord/iu + match: /(forgot(ten)?|reset|don't remember).?(your )?password|password forgotten|password dimenticata|reset(?:ta) password|recuper[ao] password|(vergessen|verloren|verlegt|wiederherstellen) passwort|wachtwoord (vergeten|reset)|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)|glömt lösenord|återställ lösenord/iu }, loginProvidersRegex: { match: / with | con | mit | met | avec /iu @@ -13060,8 +13259,8 @@ class Matching { * * `email: [{type: "email", strategies: {cssSelector: "email", ... etc}]` */ - for (let [listName, matcherNames] of Object.entries(this.#config.matchers.lists)) { - for (let fieldName of matcherNames) { + for (const [listName, matcherNames] of Object.entries(this.#config.matchers.lists)) { + for (const fieldName of matcherNames) { if (!this.#matcherLists[listName]) { this.#matcherLists[listName] = []; } @@ -13178,7 +13377,7 @@ class Matching { * @type {string[]} */ const selectors = []; - for (let matcher of matcherList) { + for (const matcher of matcherList) { if (matcher.strategies.cssSelector) { const css = this.cssSelector(matcher.strategies.cssSelector); if (css) { @@ -13315,12 +13514,12 @@ class Matching { /** * Loop through each strategy in order */ - for (let strategyName of this.#defaultStrategyOrder) { + for (const strategyName of this.#defaultStrategyOrder) { let result; /** * Now loop through each matcher in the list. */ - for (let matcher of matchers) { + for (const matcher of matchers) { /** * for each `strategyName` (such as cssSelector), check * if the current matcher implements it. @@ -13445,16 +13644,16 @@ class Matching { if (!ddgMatcher || !ddgMatcher.match) { return defaultResult; } - let matchRexExp = this.getDDGMatcherRegex(lookup); + const matchRexExp = this.getDDGMatcherRegex(lookup); if (!matchRexExp) { return defaultResult; } - let requiredScore = ['match', 'forceUnknown', 'maxDigits'].filter(ddgMatcherProp => ddgMatcherProp in ddgMatcher).length; + const requiredScore = ['match', 'forceUnknown', 'maxDigits'].filter(ddgMatcherProp => ddgMatcherProp in ddgMatcher).length; /** @type {MatchableStrings[]} */ const matchableStrings = ddgMatcher.matchableStrings || ['labelText', 'placeholderAttr', 'relatedText']; - for (let stringName of matchableStrings) { - let elementString = this.activeElementStrings[stringName]; + for (const stringName of matchableStrings) { + const elementString = this.activeElementStrings[stringName]; if (!elementString) continue; // Scoring to ensure all DDG tests are valid @@ -13470,7 +13669,7 @@ class Matching { // If a negated regex was provided, ensure it does not match // If it DOES match - then we need to prevent any future strategies from continuing if (ddgMatcher.forceUnknown) { - let notRegex = ddgMatcher.forceUnknown; + const notRegex = ddgMatcher.forceUnknown; if (!notRegex) { return { ...result, @@ -13489,7 +13688,7 @@ class Matching { } } if (ddgMatcher.skip) { - let skipRegex = ddgMatcher.skip; + const skipRegex = ddgMatcher.skip; if (!skipRegex) { return { ...result, @@ -13554,8 +13753,8 @@ class Matching { } /** @type {MatchableStrings[]} */ const stringsToMatch = ['placeholderAttr', 'nameAttr', 'labelText', 'id', 'relatedText']; - for (let stringName of stringsToMatch) { - let elementString = this.activeElementStrings[stringName]; + for (const stringName of stringsToMatch) { + const elementString = this.activeElementStrings[stringName]; if (!elementString) continue; if ((0, _autofillUtils.safeRegexTest)(regex, elementString)) { return { @@ -13629,14 +13828,14 @@ class Matching { fields: {} }, strategies: { - 'vendorRegex': { + vendorRegex: { rules: {}, ruleSets: [] }, - 'ddgMatcher': { + ddgMatcher: { matchers: {} }, - 'cssSelector': { + cssSelector: { selectors: {} } } @@ -13807,7 +14006,7 @@ const removeExcessWhitespace = function () { exports.removeExcessWhitespace = removeExcessWhitespace; const getExplicitLabelsText = el => { const labelTextCandidates = []; - for (let label of el.labels || []) { + for (const label of el.labels || []) { labelTextCandidates.push(...(0, _labelUtil.extractElementStrings)(label)); } if (el.hasAttribute('aria-label')) { @@ -13862,7 +14061,7 @@ const getRelatedText = (el, form, cssSelector) => { // If we didn't find a container, try looking for an adjacent label if (scope === el) { - let previousEl = recursiveGetPreviousElSibling(el); + const previousEl = recursiveGetPreviousElSibling(el); if (previousEl instanceof HTMLElement) { scope = previousEl; } @@ -14521,19 +14720,18 @@ class DefaultScanner { if (this.device.globalConfig.isDDGDomain) { return this; } - if ('matches' in context && context.matches?.(this.matching.cssSelector('formInputsSelectorWithoutSelect'))) { + const formInputsSelectorWithoutSelect = this.matching.cssSelector('formInputsSelectorWithoutSelect'); + if ('matches' in context && context.matches?.(formInputsSelectorWithoutSelect)) { this.addInput(context); } else { - const selector = this.matching.cssSelector('formInputsSelectorWithoutSelect'); - const inputs = context.querySelectorAll(selector); + const inputs = context.querySelectorAll(formInputsSelectorWithoutSelect); if (inputs.length > this.options.maxInputsPerPage) { this.setMode('stopped', `Too many input fields in the given context (${inputs.length}), stop scanning`, context); return this; } inputs.forEach(input => this.addInput(input)); if (context instanceof HTMLFormElement && this.forms.get(context)?.hasShadowTree) { - const selector = this.matching.cssSelector('formInputsSelectorWithoutSelect'); - (0, _autofillUtils.findEnclosedElements)(context, selector).forEach(input => { + (0, _autofillUtils.findElementsInShadowTree)(context, formInputsSelectorWithoutSelect).forEach(input => { if (input instanceof HTMLInputElement) { this.addInput(input, context); } @@ -14619,12 +14817,16 @@ class DefaultScanner { } if (element.parentElement) { element = element.parentElement; - const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector')); - const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector')); - // If we find a button or another input, we assume that's our form - if (inputs.length > 1 || buttons.length) { - // found related input, return common ancestor - return element; + // If the parent is a redundant component (only contains a single element or is a shadowRoot) do not increase the traversal count. + if (element.childElementCount > 1) { + const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector')); + const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector')); + // If we find a button or another input, we assume that's our form + if (inputs.length > 1 || buttons.length) { + // found related input, return common ancestor + return element; + } + traversalLayerCount++; } } else { // possibly a shadow boundary, so traverse through the shadow root and find the form @@ -14632,9 +14834,11 @@ class DefaultScanner { if (root instanceof ShadowRoot && root.host) { // @ts-ignore element = root.host; + } else { + // We're in a strange state (no parent or shadow root), just break out of the loop for safety + break; } } - traversalLayerCount++; } return input; } @@ -14717,7 +14921,7 @@ class DefaultScanner { this.changedElements.clear(); } else if (!this.rescanAll) { // otherwise keep adding each element to the queue - for (let element of htmlElements) { + for (const element of htmlElements) { this.changedElements.add(element); } } @@ -14741,7 +14945,7 @@ class DefaultScanner { this.findEligibleInputs(document); return; } - for (let element of this.changedElements) { + for (const element of this.changedElements) { if (element.isConnected) { this.findEligibleInputs(element); } @@ -14762,7 +14966,7 @@ class DefaultScanner { const outgoing = []; for (const mutationRecord of mutationList) { if (mutationRecord.type === 'childList') { - for (let addedNode of mutationRecord.addedNodes) { + for (const addedNode of mutationRecord.addedNodes) { if (!(addedNode instanceof HTMLElement)) continue; if (addedNode.nodeName === 'DDG-AUTOFILL') continue; outgoing.push(addedNode); @@ -14796,12 +15000,13 @@ class DefaultScanner { // find the enclosing parent form, and scan it. if (realTarget instanceof HTMLInputElement && !realTarget.hasAttribute(ATTR_INPUT_TYPE)) { const parentForm = this.getParentForm(realTarget); - if (parentForm && parentForm instanceof HTMLFormElement) { - const hasShadowTree = event.target?.shadowRoot != null; - const form = new _Form.Form(parentForm, realTarget, this.device, this.matching, this.shouldAutoprompt, hasShadowTree); - this.forms.set(parentForm, form); - this.findEligibleInputs(parentForm); - } + + // If the parent form is an input element we bail. + if (parentForm instanceof HTMLInputElement) return; + const hasShadowTree = event.target?.shadowRoot != null; + const form = new _Form.Form(parentForm, realTarget, this.device, this.matching, this.shouldAutoprompt, hasShadowTree); + this.forms.set(parentForm, form); + this.findEligibleInputs(parentForm); } window.performance?.mark?.('scan_shadow:init:end'); (0, _autofillUtils.logPerformance)('scan_shadow'); @@ -15500,7 +15705,7 @@ ${css} if (btn.matches('.wrapper:not(.top-autofill) button:hover, .currentFocus')) { callbacks.onSelect(btn.id); } else { - console.warn('The button doesn\'t seem to be hovered. Please check.'); + console.warn("The button doesn't seem to be hovered. Please check."); } }); }); @@ -15704,7 +15909,9 @@ const defaultOptions = exports.defaultOptions = { }`, css: ``, setSize: undefined, - remove: () => {/** noop */}, + remove: () => { + /** noop */ + }, testMode: false, checkVisibility: true, hasCaret: false, @@ -15732,9 +15939,9 @@ class HTMLTooltip { this.tooltip = null; this.getPosition = getPosition; const forcedVisibilityStyles = { - 'display': 'block', - 'visibility': 'visible', - 'opacity': '1' + display: 'block', + visibility: 'visible', + opacity: '1' }; // @ts-ignore how to narrow this.host to HTMLElement? (0, _autofillUtils.addInlineStyles)(this.host, forcedVisibilityStyles); @@ -15992,7 +16199,7 @@ class HTMLTooltip { checkVisibility: this.options.checkVisibility }); } else { - console.warn('The button doesn\'t seem to be hovered. Please check.'); + console.warn("The button doesn't seem to be hovered. Please check."); } } } @@ -16936,10 +17143,14 @@ Object.defineProperty(exports, "__esModule", { }); exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; exports.escapeXML = escapeXML; -exports.findEnclosedElements = findEnclosedElements; +exports.findElementsInShadowTree = findElementsInShadowTree; exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = exports.getDaxBoundingBox = void 0; +exports.getDaxBoundingBox = void 0; +exports.getFormControlElements = getFormControlElements; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -16948,6 +17159,7 @@ exports.isValidTLD = isValidTLD; exports.logPerformance = logPerformance; exports.notifyWebApp = void 0; exports.pierceShadowTree = pierceShadowTree; +exports.queryElementsWithShadow = queryElementsWithShadow; exports.safeExecute = exports.removeInlineStyles = void 0; exports.safeRegexTest = safeRegexTest; exports.setValue = exports.sendAndWaitForAnswer = void 0; @@ -17320,8 +17532,9 @@ const isLikelyASubmitButton = (el, matching) => { // has high-signal submit classes safeRegexTest(/submit/i, dataTestId) || safeRegexTest(matching.getDDGMatcherRegex('submitButtonRegex'), text) || // has high-signal text - el.offsetHeight * el.offsetWidth >= 10000 && !safeRegexTest(/secondary/i, el.className) // it's a large element 250x40px - ) && el.offsetHeight * el.offsetWidth >= 2000 && + el.offsetHeight * el.offsetWidth >= 10000 && !safeRegexTest(/secondary/i, el.className)) && + // it's a large element 250x40px + el.offsetHeight * el.offsetWidth >= 2000 && // it's not a very small button like inline links and such !safeRegexTest(matching.getDDGMatcherRegex('submitButtonUnlikelyRegex'), text + ' ' + ariaLabel); }; @@ -17549,22 +17762,16 @@ function getActiveElement() { } /** - * Takes a root element and tries to find visible elements first, and if it fails, it tries to find shadow elements + * Takes a root element and tries to find elements in shadow DOMs that match the selector * @param {HTMLElement|HTMLFormElement} root * @param {string} selector * @returns {Element[]} */ -function findEnclosedElements(root, selector) { - // Check if there are any normal elements that match the selector - const elements = root.querySelectorAll(selector); - if (elements.length > 0) { - return Array.from(elements); - } - - // Check if there are any shadow elements that match the selector +function findElementsInShadowTree(root, selector) { const shadowElements = []; const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); - let node = walker.nextNode(); + /** @type {Node|null} */ + let node = walker.currentNode; while (node) { if (node instanceof HTMLElement && node.shadowRoot) { shadowElements.push(...node.shadowRoot.querySelectorAll(selector)); @@ -17574,6 +17781,52 @@ function findEnclosedElements(root, selector) { return shadowElements; } +/** + * The function looks for form's control elements, and returns them if they're iterable. + * @param {HTMLElement} form + * @param {string} selector + * @returns {Element[]|null} + */ +function getFormControlElements(form, selector) { + // Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable. + if (form instanceof HTMLFormElement && form.elements != null && Symbol.iterator in Object(form.elements)) { + // For form elements we use .elements to catch fields outside the form itself using the form attribute. + // It also catches all elements when the markup is broken. + // We use .filter to avoid specific types of elements. + const formControls = [...form.elements].filter(el => el.matches(selector)); + return [...formControls]; + } else { + return null; + } +} + +/** + * Default operation: finds elements using querySelectorAll. + * Optionally, can be forced to scan the shadow tree. + * @param {HTMLElement} element + * @param {string} selector + * @param {boolean} forceScanShadowTree + * @returns {Element[]} + */ +function queryElementsWithShadow(element, selector) { + let forceScanShadowTree = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + /** @type {Element[]|NodeListOf} element */ + const elements = element.querySelectorAll(selector); + if (forceScanShadowTree || elements.length === 0) { + return [...elements, ...findElementsInShadowTree(element, selector)]; + } + return [...elements]; +} + +/** + * Checks if there is a single username-like identity, i.e. email or phone + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":44,"./constants.js":67,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],65:[function(require,module,exports){ "use strict"; @@ -17613,7 +17866,8 @@ Object.defineProperty(exports, "__esModule", { }); exports.DDG_DOMAIN_REGEX = void 0; exports.createGlobalConfig = createGlobalConfig; -const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = new RegExp(/^https:\/\/(([a-z0-9-_]+?)\.)?duckduckgo\.com\/email/); +/* eslint-disable prefer-const */ +const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = /^https:\/\/(([a-z0-9-_]+?)\.)?duckduckgo\.com\/email/; /** * This is a centralised place to contain all string/variable replacements @@ -18124,6 +18378,26 @@ const availableInputTypesSchema = exports.availableInputTypesSchema = _zod.z.obj credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); +const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({ + type: _zod.z.literal("getAutofillInitDataResponse").optional(), + success: _zod.z.object({ + credentials: _zod.z.array(credentialsSchema), + identities: _zod.z.array(_zod.z.record(_zod.z.unknown())), + creditCards: _zod.z.array(_zod.z.record(_zod.z.unknown())), + serializedInputContext: _zod.z.string() + }).optional(), + error: genericErrorSchema.optional() +}); +const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = _zod.z.object({ + type: _zod.z.literal("getAutofillCredentialsResponse").optional(), + success: _zod.z.object({ + id: _zod.z.string().optional(), + autogenerated: _zod.z.boolean().optional(), + username: _zod.z.string(), + password: _zod.z.string().optional() + }).optional(), + error: genericErrorSchema.optional() +}); const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.object({ credentials: _zod.z.object({ username: _zod.z.boolean().optional(), @@ -18156,6 +18430,11 @@ const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.o credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); +const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({ + status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]), + credentials: _zod.z.array(credentialsSchema), + availableInputTypes: availableInputTypes1Schema +}); const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod.z.object({ inputType_credentials: _zod.z.boolean().optional(), inputType_identities: _zod.z.boolean().optional(), @@ -18168,55 +18447,6 @@ const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod third_party_credentials_provider: _zod.z.boolean().optional(), unknown_username_categorization: _zod.z.boolean().optional() }); -const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = _zod.z.object({ - generatedPassword: generatedPasswordSchema.optional(), - inputType: _zod.z.string(), - mainType: _zod.z.union([_zod.z.literal("credentials"), _zod.z.literal("identities"), _zod.z.literal("creditCards")]), - subType: _zod.z.string(), - trigger: _zod.z.union([_zod.z.literal("userInitiated"), _zod.z.literal("autoprompt"), _zod.z.literal("postSignup")]).optional(), - serializedInputContext: _zod.z.string().optional(), - triggerContext: triggerContextSchema.optional() -}); -const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _zod.z.object({ - type: _zod.z.literal("getAutofillDataResponse").optional(), - success: _zod.z.object({ - credentials: credentialsSchema.optional(), - action: _zod.z.union([_zod.z.literal("fill"), _zod.z.literal("focus"), _zod.z.literal("none"), _zod.z.literal("refreshAvailableInputTypes"), _zod.z.literal("acceptGeneratedPassword"), _zod.z.literal("rejectGeneratedPassword")]) - }).optional(), - error: genericErrorSchema.optional() -}); -const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ - credentials: outgoingCredentialsSchema.optional(), - trigger: _zod.z.union([_zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() -}); -const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ - type: _zod.z.literal("getAvailableInputTypesResponse").optional(), - success: availableInputTypesSchema, - error: genericErrorSchema.optional() -}); -const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({ - type: _zod.z.literal("getAutofillInitDataResponse").optional(), - success: _zod.z.object({ - credentials: _zod.z.array(credentialsSchema), - identities: _zod.z.array(_zod.z.record(_zod.z.unknown())), - creditCards: _zod.z.array(_zod.z.record(_zod.z.unknown())), - serializedInputContext: _zod.z.string() - }).optional(), - error: genericErrorSchema.optional() -}); -const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = _zod.z.object({ - type: _zod.z.literal("getAutofillCredentialsResponse").optional(), - success: _zod.z.object({ - id: _zod.z.string().optional(), - autogenerated: _zod.z.boolean().optional(), - username: _zod.z.string(), - password: _zod.z.string().optional() - }).optional(), - error: genericErrorSchema.optional() -}); -const autofillSettingsSchema = exports.autofillSettingsSchema = _zod.z.object({ - featureToggles: autofillFeatureTogglesSchema -}); const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = _zod.z.object({ success: _zod.z.boolean().optional(), error: genericErrorSchema.optional() @@ -18251,19 +18481,30 @@ const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtection }).optional(), error: genericErrorSchema.optional() }); -const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({ - contentScope: contentScopeSchema, - userUnprotectedDomains: _zod.z.array(_zod.z.string()), - userPreferences: userPreferencesSchema +const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = _zod.z.object({ + generatedPassword: generatedPasswordSchema.optional(), + inputType: _zod.z.string(), + mainType: _zod.z.union([_zod.z.literal("credentials"), _zod.z.literal("identities"), _zod.z.literal("creditCards")]), + subType: _zod.z.string(), + trigger: _zod.z.union([_zod.z.literal("userInitiated"), _zod.z.literal("autoprompt"), _zod.z.literal("postSignup")]).optional(), + serializedInputContext: _zod.z.string().optional(), + triggerContext: triggerContextSchema.optional() }); -const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({ - status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]), - credentials: _zod.z.array(credentialsSchema), - availableInputTypes: availableInputTypes1Schema +const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _zod.z.object({ + type: _zod.z.literal("getAutofillDataResponse").optional(), + success: _zod.z.object({ + credentials: credentialsSchema.optional(), + action: _zod.z.union([_zod.z.literal("fill"), _zod.z.literal("focus"), _zod.z.literal("none"), _zod.z.literal("refreshAvailableInputTypes"), _zod.z.literal("acceptGeneratedPassword"), _zod.z.literal("rejectGeneratedPassword")]) + }).optional(), + error: genericErrorSchema.optional() }); -const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = _zod.z.object({ - type: _zod.z.literal("getRuntimeConfigurationResponse").optional(), - success: runtimeConfigurationSchema.optional(), +const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ + credentials: outgoingCredentialsSchema.optional(), + trigger: _zod.z.union([_zod.z.literal("partialSave"), _zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() +}); +const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ + type: _zod.z.literal("getAvailableInputTypesResponse").optional(), + success: availableInputTypesSchema, error: genericErrorSchema.optional() }); const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = _zod.z.object({ @@ -18276,6 +18517,19 @@ const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProvi success: providerStatusUpdatedSchema, error: genericErrorSchema.optional() }); +const autofillSettingsSchema = exports.autofillSettingsSchema = _zod.z.object({ + featureToggles: autofillFeatureTogglesSchema +}); +const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({ + contentScope: contentScopeSchema, + userUnprotectedDomains: _zod.z.array(_zod.z.string()), + userPreferences: userPreferencesSchema +}); +const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = _zod.z.object({ + type: _zod.z.literal("getRuntimeConfigurationResponse").optional(), + success: runtimeConfigurationSchema.optional(), + error: genericErrorSchema.optional() +}); const apiSchema = exports.apiSchema = _zod.z.object({ addDebugFlag: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ paramsValidator: addDebugFlagParamsSchema.optional() @@ -18492,7 +18746,7 @@ function waitForResponse(expectedResponse, config) { return; } try { - let data = JSON.parse(e.data); + const data = JSON.parse(e.data); if (data.type === expectedResponse) { window.removeEventListener('message', handler); return resolve(data); @@ -18647,7 +18901,7 @@ async function extensionSpecificRuntimeConfiguration(deviceApi) { return { success: { // @ts-ignore - contentScope: contentScope, + contentScope, // @ts-ignore userPreferences: { // Copy locale to user preferences as 'language' to match expected payload @@ -18841,6 +19095,7 @@ function waitForWindowsResponse(responseId, options) { if (options?.signal?.aborted) { return reject(new DOMException('Aborted', 'AbortError')); } + // eslint-disable-next-line prefer-const let teardown; // The event handler @@ -21727,7 +21982,6 @@ exports.default = void 0; window.requestIdleCallback = window.requestIdleCallback || function (cb) { return setTimeout(function () { const start = Date.now(); - // eslint-disable-next-line standard/no-callback-literal cb({ didTimeout: false, timeRemaining: function () { diff --git a/node_modules/@duckduckgo/autofill/dist/autofill.js b/node_modules/@duckduckgo/autofill/dist/autofill.js index 6a97e06c86fa..fd485bd7953e 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill.js +++ b/node_modules/@duckduckgo/autofill/dist/autofill.js @@ -269,8 +269,8 @@ class SchemaValidationError extends Error { } case 'invalid_union': { - for (let unionError of issue.unionErrors) { - for (let issue1 of unionError.issues) { + for (const unionError of issue.unionErrors) { + for (const issue1 of unionError.issues) { log(issue1); } } @@ -282,7 +282,7 @@ class SchemaValidationError extends Error { } } } - for (let error of errors) { + for (const error of errors) { log(error); } const message = [heading, 'please see the details above'].join('\n '); @@ -405,8 +405,8 @@ class DeviceApi { */ async request(deviceApiCall, options) { deviceApiCall.validateParams(); - let result = await this.transport.send(deviceApiCall, options); - let processed = deviceApiCall.preResultValidation(result); + const result = await this.transport.send(deviceApiCall, options); + const processed = deviceApiCall.preResultValidation(result); return deviceApiCall.validateResult(processed); } /** @@ -494,44 +494,44 @@ var _webkit = require("./webkit.js"); */ class Messaging { /** - * @param {WebkitMessagingConfig} config - */ + * @param {WebkitMessagingConfig} config + */ constructor(config) { this.transport = getTransport(config); } /** - * Send a 'fire-and-forget' message. - * @throws {Error} - * {@link MissingHandler} - * - * @example - * - * ``` - * const messaging = new Messaging(config) - * messaging.notify("foo", {bar: "baz"}) - * ``` - * @param {string} name - * @param {Record} [data] - */ + * Send a 'fire-and-forget' message. + * @throws {Error} + * {@link MissingHandler} + * + * @example + * + * ``` + * const messaging = new Messaging(config) + * messaging.notify("foo", {bar: "baz"}) + * ``` + * @param {string} name + * @param {Record} [data] + */ notify(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; this.transport.notify(name, data); } /** - * Send a request, and wait for a response - * @throws {Error} - * {@link MissingHandler} - * - * @example - * ``` - * const messaging = new Messaging(config) - * const response = await messaging.request("foo", {bar: "baz"}) - * ``` - * - * @param {string} name - * @param {Record} [data] - * @return {Promise} - */ + * Send a request, and wait for a response + * @throws {Error} + * {@link MissingHandler} + * + * @example + * ``` + * const messaging = new Messaging(config) + * const response = await messaging.request("foo", {bar: "baz"}) + * ``` + * + * @param {string} name + * @param {Record} [data] + * @return {Promise} + */ request(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return this.transport.request(name, data); @@ -544,20 +544,20 @@ class Messaging { exports.Messaging = Messaging; class MessagingTransport { /** - * @param {string} name - * @param {Record} [data] - * @returns {void} - */ + * @param {string} name + * @param {Record} [data] + * @returns {void} + */ // @ts-ignore - ignoring a no-unused ts error, this is only an interface. notify(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; throw new Error("must implement 'notify'"); } /** - * @param {string} name - * @param {Record} [data] - * @return {Promise} - */ + * @param {string} name + * @param {Record} [data] + * @return {Promise} + */ // @ts-ignore - ignoring a no-unused ts error, this is only an interface. request(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; @@ -582,9 +582,9 @@ function getTransport(config) { */ class MissingHandler extends Error { /** - * @param {string} message - * @param {string} handlerName - */ + * @param {string} message + * @param {string} handlerName + */ constructor(message, handlerName) { super(message); this.handlerName = handlerName; @@ -690,8 +690,8 @@ class WebkitMessagingTransport { config; globals; /** - * @param {WebkitMessagingConfig} config - */ + * @param {WebkitMessagingConfig} config + */ constructor(config) { this.config = config; this.globals = captureGlobals(); @@ -700,11 +700,11 @@ class WebkitMessagingTransport { } } /** - * Sends message to the webkit layer (fire and forget) - * @param {String} handler - * @param {*} data - * @internal - */ + * Sends message to the webkit layer (fire and forget) + * @param {String} handler + * @param {*} data + * @internal + */ wkSend(handler) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (!(handler in this.globals.window.webkit.messageHandlers)) { @@ -728,12 +728,12 @@ class WebkitMessagingTransport { } /** - * Sends message to the webkit layer and waits for the specified response - * @param {String} handler - * @param {*} data - * @returns {Promise<*>} - * @internal - */ + * Sends message to the webkit layer and waits for the specified response + * @param {String} handler + * @param {*} data + * @returns {Promise<*>} + * @internal + */ async wkSendAndWait(handler) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (this.config.hasModernWebkitAPI) { @@ -774,27 +774,27 @@ class WebkitMessagingTransport { } } /** - * @param {string} name - * @param {Record} [data] - */ + * @param {string} name + * @param {Record} [data] + */ notify(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; this.wkSend(name, data); } /** - * @param {string} name - * @param {Record} [data] - */ + * @param {string} name + * @param {Record} [data] + */ request(name) { let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return this.wkSendAndWait(name, data); } /** - * Generate a random method name and adds it to the global scope - * The native layer will use this method to send the response - * @param {string | number} randomMethodName - * @param {Function} callback - */ + * Generate a random method name and adds it to the global scope + * The native layer will use this method to send the response + * @param {string | number} randomMethodName + * @param {Function} callback + */ generateRandomMethod(randomMethodName, callback) { var _this = this; this.globals.ObjectDefineProperty(this.globals.window, randomMethodName, { @@ -803,8 +803,8 @@ class WebkitMessagingTransport { configurable: true, writable: false, /** - * @param {any[]} args - */ + * @param {any[]} args + */ value: function () { callback(...arguments); // @ts-ignore - we want this to throw if it fails as it would indicate a fatal error. @@ -820,16 +820,16 @@ class WebkitMessagingTransport { } /** - * @type {{name: string, length: number}} - */ + * @type {{name: string, length: number}} + */ algoObj = { name: 'AES-GCM', length: 256 }; /** - * @returns {Promise} - */ + * @returns {Promise} + */ async createRandKey() { const key = await this.globals.generateKey(this.algoObj, true, ['encrypt', 'decrypt']); const exportedKey = await this.globals.exportKey('raw', key); @@ -837,44 +837,44 @@ class WebkitMessagingTransport { } /** - * @returns {Uint8Array} - */ + * @returns {Uint8Array} + */ createRandIv() { return this.globals.getRandomValues(new this.globals.Uint8Array(12)); } /** - * @param {BufferSource} ciphertext - * @param {BufferSource} key - * @param {Uint8Array} iv - * @returns {Promise} - */ + * @param {BufferSource} ciphertext + * @param {BufferSource} key + * @param {Uint8Array} iv + * @returns {Promise} + */ async decrypt(ciphertext, key, iv) { const cryptoKey = await this.globals.importKey('raw', key, 'AES-GCM', false, ['decrypt']); const algo = { name: 'AES-GCM', iv }; - let decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext); - let dec = new this.globals.TextDecoder(); + const decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext); + const dec = new this.globals.TextDecoder(); return dec.decode(decrypted); } /** - * When required (such as on macos 10.x), capture the `postMessage` method on - * each webkit messageHandler - * - * @param {string[]} handlerNames - */ + * When required (such as on macos 10.x), capture the `postMessage` method on + * each webkit messageHandler + * + * @param {string[]} handlerNames + */ captureWebkitHandlers(handlerNames) { const handlers = window.webkit.messageHandlers; if (!handlers) throw new _messaging.MissingHandler('window.webkit.messageHandlers was absent', 'all'); - for (let webkitMessageHandlerName of handlerNames) { + for (const webkitMessageHandlerName of handlerNames) { if (typeof handlers[webkitMessageHandlerName]?.postMessage === 'function') { /** - * `bind` is used here to ensure future calls to the captured - * `postMessage` have the correct `this` context - */ + * `bind` is used here to ensure future calls to the captured + * `postMessage` have the correct `this` context + */ const original = handlers[webkitMessageHandlerName]; const bound = handlers[webkitMessageHandlerName].postMessage?.bind(original); this.globals.capturedWebkitHandlers[webkitMessageHandlerName] = bound; @@ -903,11 +903,11 @@ class WebkitMessagingTransport { exports.WebkitMessagingTransport = WebkitMessagingTransport; class WebkitMessagingConfig { /** - * @param {object} params - * @param {boolean} params.hasModernWebkitAPI - * @param {string[]} params.webkitMessageHandlerNames - * @param {string} params.secret - */ + * @param {object} params + * @param {boolean} params.hasModernWebkitAPI + * @param {string[]} params.webkitMessageHandlerNames + * @param {string} params.secret + */ constructor(params) { /** * Whether or not the current WebKit Platform supports secure messaging @@ -915,13 +915,13 @@ class WebkitMessagingConfig { */ this.hasModernWebkitAPI = params.hasModernWebkitAPI; /** - * A list of WebKit message handler names that a user script can send - */ + * A list of WebKit message handler names that a user script can send + */ this.webkitMessageHandlerNames = params.webkitMessageHandlerNames; /** - * A string provided by native platforms to be sent with future outgoing - * messages - */ + * A string provided by native platforms to be sent with future outgoing + * messages + */ this.secret = params.secret; } } @@ -933,28 +933,28 @@ class WebkitMessagingConfig { exports.WebkitMessagingConfig = WebkitMessagingConfig; class SecureMessagingParams { /** - * @param {object} params - * @param {string} params.methodName - * @param {string} params.secret - * @param {number[]} params.key - * @param {number[]} params.iv - */ + * @param {object} params + * @param {string} params.methodName + * @param {string} params.secret + * @param {number[]} params.key + * @param {number[]} params.iv + */ constructor(params) { /** * The method that's been appended to `window` to be called later */ this.methodName = params.methodName; /** - * The secret used to ensure message sender validity - */ + * The secret used to ensure message sender validity + */ this.secret = params.secret; /** - * The CipherKey as number[] - */ + * The CipherKey as number[] + */ this.key = params.key; /** - * The Initial Vector as number[] - */ + * The Initial Vector as number[] + */ this.iv = params.iv; } } @@ -1165,7 +1165,7 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && * }} PasswordParameters */ const defaults = Object.freeze({ - SCAN_SET_ORDER: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-~!@#$%^&*_+=`|(){}[:;\\\"'<>,.?/ ]", + SCAN_SET_ORDER: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-~!@#$%^&*_+=`|(){}[:;\\"\'<>,.?/ ]', defaultUnambiguousCharacters: 'abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ0123456789', defaultPasswordLength: _constants.constants.DEFAULT_MIN_LENGTH, defaultPasswordRules: _constants.constants.DEFAULT_PASSWORD_RULES, @@ -1292,7 +1292,7 @@ class Password { _requirementsFromRules(passwordRules) { /** @type {Requirements} */ const requirements = {}; - for (let rule of passwordRules) { + for (const rule of passwordRules) { if (rule.name === parser.RuleName.ALLOWED) { console.assert(!('PasswordAllowedCharacters' in requirements)); const chars = this._charactersFromCharactersClasses(rule.value); @@ -1623,7 +1623,7 @@ class Password { */ _charactersFromCharactersClasses(characterClasses) { const output = []; - for (let characterClass of characterClasses) { + for (const characterClass of characterClasses) { output.push(...this._scanSetFromCharacterClass(characterClass)); } return output; @@ -1637,9 +1637,9 @@ class Password { if (!characters.length) { return ''; } - let shadowCharacters = Array.prototype.slice.call(characters); + const shadowCharacters = Array.prototype.slice.call(characters); shadowCharacters.sort((a, b) => this.options.SCAN_SET_ORDER.indexOf(a) - this.options.SCAN_SET_ORDER.indexOf(b)); - let uniqueCharacters = [shadowCharacters[0]]; + const uniqueCharacters = [shadowCharacters[0]]; for (let i = 1, length = shadowCharacters.length; i < length; ++i) { if (shadowCharacters[i] === shadowCharacters[i - 1]) { continue; @@ -1679,6 +1679,7 @@ Object.defineProperty(exports, "__esModule", { }); exports.SHOULD_NOT_BE_REACHED = exports.RuleName = exports.Rule = exports.ParserError = exports.NamedCharacterClass = exports.Identifier = exports.CustomCharacterClass = void 0; exports.parsePasswordRules = parsePasswordRules; +/* eslint-disable no-var */ // Copyright (c) 2019 - 2020 Apple Inc. Licensed under MIT License. /* @@ -1731,7 +1732,6 @@ class Rule { } } exports.Rule = Rule; -; class NamedCharacterClass { constructor(name) { console.assert(_isValidRequiredOrAllowedPropertyValueIdentifier(name)); @@ -1748,10 +1748,8 @@ class NamedCharacterClass { } } exports.NamedCharacterClass = NamedCharacterClass; -; class ParserError extends Error {} exports.ParserError = ParserError; -; class CustomCharacterClass { constructor(characters) { console.assert(characters instanceof Array); @@ -1767,14 +1765,11 @@ class CustomCharacterClass { return `[${this._characters.join('').replace('"', '"')}]`; } } -exports.CustomCharacterClass = CustomCharacterClass; -; // MARK: Lexer functions - +exports.CustomCharacterClass = CustomCharacterClass; function _isIdentifierCharacter(c) { console.assert(c.length === 1); - // eslint-disable-next-line no-mixed-operators return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c === '-'; } function _isASCIIDigit(c) { @@ -1820,14 +1815,14 @@ function _markBitsForNamedCharacterClass(bitSet, namedCharacterClass) { } } function _markBitsForCustomCharacterClass(bitSet, customCharacterClass) { - for (let character of customCharacterClass.characters) { + for (const character of customCharacterClass.characters) { bitSet[_bitSetIndexForCharacter(character)] = true; } } function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFormatCompliant) { // @ts-ignore - let asciiPrintableBitSet = new Array('~'.codePointAt(0) - ' '.codePointAt(0) + 1); - for (let propertyValue of propertyValues) { + const asciiPrintableBitSet = new Array('~'.codePointAt(0) - ' '.codePointAt(0) + 1); + for (const propertyValue of propertyValues) { if (propertyValue instanceof NamedCharacterClass) { if (propertyValue.name === Identifier.UNICODE) { return [new NamedCharacterClass(Identifier.UNICODE)]; @@ -1842,32 +1837,32 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo } let charactersSeen = []; function checkRange(start, end) { - let temp = []; + const temp = []; for (let i = _bitSetIndexForCharacter(start); i <= _bitSetIndexForCharacter(end); ++i) { if (asciiPrintableBitSet[i]) { temp.push(_characterAtBitSetIndex(i)); } } - let result = temp.length === _bitSetIndexForCharacter(end) - _bitSetIndexForCharacter(start) + 1; + const result = temp.length === _bitSetIndexForCharacter(end) - _bitSetIndexForCharacter(start) + 1; if (!result) { charactersSeen = charactersSeen.concat(temp); } return result; } - let hasAllUpper = checkRange('A', 'Z'); - let hasAllLower = checkRange('a', 'z'); - let hasAllDigits = checkRange('0', '9'); + const hasAllUpper = checkRange('A', 'Z'); + const hasAllLower = checkRange('a', 'z'); + const hasAllDigits = checkRange('0', '9'); // Check for special characters, accounting for characters that are given special treatment (i.e. '-' and ']') let hasAllSpecial = false; let hasDash = false; let hasRightSquareBracket = false; - let temp = []; + const temp = []; for (let i = _bitSetIndexForCharacter(' '); i <= _bitSetIndexForCharacter('/'); ++i) { if (!asciiPrintableBitSet[i]) { continue; } - let character = _characterAtBitSetIndex(i); + const character = _characterAtBitSetIndex(i); if (keepCustomCharacterClassFormatCompliant && character === '-') { hasDash = true; } else { @@ -1883,7 +1878,7 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo if (!asciiPrintableBitSet[i]) { continue; } - let character = _characterAtBitSetIndex(i); + const character = _characterAtBitSetIndex(i); if (keepCustomCharacterClassFormatCompliant && character === ']') { hasRightSquareBracket = true; } else { @@ -1901,12 +1896,12 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo if (hasRightSquareBracket) { temp.push(']'); } - let numberOfSpecialCharacters = _bitSetIndexForCharacter('/') - _bitSetIndexForCharacter(' ') + 1 + (_bitSetIndexForCharacter('@') - _bitSetIndexForCharacter(':') + 1) + (_bitSetIndexForCharacter('`') - _bitSetIndexForCharacter('[') + 1) + (_bitSetIndexForCharacter('~') - _bitSetIndexForCharacter('{') + 1); + const numberOfSpecialCharacters = _bitSetIndexForCharacter('/') - _bitSetIndexForCharacter(' ') + 1 + (_bitSetIndexForCharacter('@') - _bitSetIndexForCharacter(':') + 1) + (_bitSetIndexForCharacter('`') - _bitSetIndexForCharacter('[') + 1) + (_bitSetIndexForCharacter('~') - _bitSetIndexForCharacter('{') + 1); hasAllSpecial = temp.length === numberOfSpecialCharacters; if (!hasAllSpecial) { charactersSeen = charactersSeen.concat(temp); } - let result = []; + const result = []; if (hasAllUpper && hasAllLower && hasAllDigits && hasAllSpecial) { return [new NamedCharacterClass(Identifier.ASCII_PRINTABLE)]; } @@ -1934,7 +1929,7 @@ function _indexOfNonWhitespaceCharacter(input) { let position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; console.assert(position >= 0); console.assert(position <= input.length); - let length = input.length; + const length = input.length; while (position < length && _isASCIIWhitespace(input[position])) { ++position; } @@ -1944,10 +1939,10 @@ function _parseIdentifier(input, position) { console.assert(position >= 0); console.assert(position < input.length); console.assert(_isIdentifierCharacter(input[position])); - let length = input.length; - let seenIdentifiers = []; + const length = input.length; + const seenIdentifiers = []; do { - let c = input[position]; + const c = input[position]; if (!_isIdentifierCharacter(c)) { break; } @@ -1963,16 +1958,16 @@ function _parseCustomCharacterClass(input, position) { console.assert(position >= 0); console.assert(position < input.length); console.assert(input[position] === CHARACTER_CLASS_START_SENTINEL); - let length = input.length; + const length = input.length; ++position; if (position >= length) { // console.error('Found end-of-line instead of character class character') return [null, position]; } - let initialPosition = position; - let result = []; + const initialPosition = position; + const result = []; do { - let c = input[position]; + const c = input[position]; if (!_isASCIIPrintableCharacter(c)) { ++position; continue; @@ -2008,11 +2003,11 @@ function _parseCustomCharacterClass(input, position) { function _parsePasswordRequiredOrAllowedPropertyValue(input, position) { console.assert(position >= 0); console.assert(position < input.length); - let length = input.length; - let propertyValues = []; + const length = input.length; + const propertyValues = []; while (true) { if (_isIdentifierCharacter(input[position])) { - let identifierStartPosition = position; + const identifierStartPosition = position; // eslint-disable-next-line no-redeclare var [propertyValue, position] = _parseIdentifier(input, position); if (!_isValidRequiredOrAllowedPropertyValueIdentifier(propertyValue)) { @@ -2059,8 +2054,8 @@ function _parsePasswordRule(input, position) { console.assert(position >= 0); console.assert(position < input.length); console.assert(_isIdentifierCharacter(input[position])); - let length = input.length; - var mayBeIdentifierStartPosition = position; + const length = input.length; + const mayBeIdentifierStartPosition = position; // eslint-disable-next-line no-redeclare var [identifier, position] = _parseIdentifier(input, position); if (!Object.values(RuleName).includes(identifier)) { @@ -2075,7 +2070,7 @@ function _parsePasswordRule(input, position) { // console.error('Failed to find start of property value: ' + input.substr(position)) return [null, position, undefined]; } - let property = { + const property = { name: identifier, value: null }; @@ -2131,7 +2126,7 @@ function _parseInteger(input, position) { // console.error('Failed to parse value of type integer; not a number: ' + input.substr(position)) return [null, position]; } - let length = input.length; + const length = input.length; // let initialPosition = position let result = 0; do { @@ -2152,8 +2147,8 @@ function _parseInteger(input, position) { * @private */ function _parsePasswordRulesInternal(input) { - let parsedProperties = []; - let length = input.length; + const parsedProperties = []; + const length = input.length; var position = _indexOfNonWhitespaceCharacter(input); while (position < length) { if (!_isIdentifierCharacter(input[position])) { @@ -2190,7 +2185,7 @@ function _parsePasswordRulesInternal(input) { * @returns {Rule[]} */ function parsePasswordRules(input, formatRulesForMinifiedVersion) { - let [passwordRules, maybeMessage] = _parsePasswordRulesInternal(input); + const [passwordRules, maybeMessage] = _parsePasswordRulesInternal(input); if (!passwordRules) { throw new ParserError(maybeMessage); } @@ -2200,13 +2195,13 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { // When formatting rules for minified version, we should keep the formatted rules // as similar to the input as possible. Avoid copying required rules to allowed rules. - let suppressCopyingRequiredToAllowed = formatRulesForMinifiedVersion; - let requiredRules = []; + const suppressCopyingRequiredToAllowed = formatRulesForMinifiedVersion; + const requiredRules = []; let newAllowedValues = []; let minimumMaximumConsecutiveCharacters = null; let maximumMinLength = 0; let minimumMaxLength = null; - for (let rule of passwordRules) { + for (const rule of passwordRules) { switch (rule.name) { case RuleName.MAX_CONSECUTIVE: minimumMaximumConsecutiveCharacters = minimumMaximumConsecutiveCharacters ? Math.min(rule.value, minimumMaximumConsecutiveCharacters) : rule.value; @@ -2239,10 +2234,10 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { if (minimumMaximumConsecutiveCharacters !== null) { newPasswordRules.push(new Rule(RuleName.MAX_CONSECUTIVE, minimumMaximumConsecutiveCharacters)); } - let sortedRequiredRules = requiredRules.sort(function (a, b) { + const sortedRequiredRules = requiredRules.sort(function (a, b) { const namedCharacterClassOrder = [Identifier.LOWER, Identifier.UPPER, Identifier.DIGIT, Identifier.SPECIAL, Identifier.ASCII_PRINTABLE, Identifier.UNICODE]; - let aIsJustOneNamedCharacterClass = a.value.length === 1 && a.value[0] instanceof NamedCharacterClass; - let bIsJustOneNamedCharacterClass = b.value.length === 1 && b.value[0] instanceof NamedCharacterClass; + const aIsJustOneNamedCharacterClass = a.value.length === 1 && a.value[0] instanceof NamedCharacterClass; + const bIsJustOneNamedCharacterClass = b.value.length === 1 && b.value[0] instanceof NamedCharacterClass; if (aIsJustOneNamedCharacterClass && !bIsJustOneNamedCharacterClass) { return -1; } @@ -2250,8 +2245,8 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { return 1; } if (aIsJustOneNamedCharacterClass && bIsJustOneNamedCharacterClass) { - let aIndex = namedCharacterClassOrder.indexOf(a.value[0].name); - let bIndex = namedCharacterClassOrder.indexOf(b.value[0].name); + const aIndex = namedCharacterClassOrder.indexOf(a.value[0].name); + const bIndex = namedCharacterClassOrder.indexOf(b.value[0].name); return aIndex - bIndex; } return 0; @@ -2896,6 +2891,9 @@ module.exports={ "keldoc.com": { "password-rules": "minlength: 12; required: lower; required: upper; required: digit; required: [!@#$%^&*];" }, + "kennedy-center.org": { + "password-rules": "minlength: 8; required: lower; required: upper; required: digit; required: [!#$%&*?@];" + }, "key.harvard.edu": { "password-rules": "minlength: 10; maxlength: 100; required: lower; required: upper; required: digit; allowed: [-@_#!&$`%*+()./,;~:{}|?>=<^[']];" }, @@ -3453,7 +3451,7 @@ function createDevice() { }; // Create the DeviceAPI + Setting - let deviceApi = new _index.DeviceApi(globalConfig.isDDGTestMode ? loggingTransport : transport); + const deviceApi = new _index.DeviceApi(globalConfig.isDDGTestMode ? loggingTransport : transport); const settings = new _Settings.Settings(globalConfig, deviceApi); if (globalConfig.isWindows) { if (globalConfig.isTopFrame) { @@ -3587,9 +3585,9 @@ class AndroidInterface extends _InterfacePrototype.default { } /** - * Used by the email web app - * Provides functionality to log the user out - */ + * Used by the email web app + * Provides functionality to log the user out + */ removeUserData() { try { return window.EmailInterface.removeCredentials(); @@ -4858,14 +4856,16 @@ class InterfacePrototype { }); break; default: - // Also fire pixel when filling an identity with the personal duck address from an email field - const checks = [subtype === 'emailAddress', this.hasLocalAddresses, data?.emailAddress === (0, _autofillUtils.formatDuckAddress)(this.#addresses.personalAddress)]; - if (checks.every(Boolean)) { - this.firePixel({ - pixelName: 'autofill_personal_address' - }); + { + // Also fire pixel when filling an identity with the personal duck address from an email field + const checks = [subtype === 'emailAddress', this.hasLocalAddresses, data?.emailAddress === (0, _autofillUtils.formatDuckAddress)(this.#addresses.personalAddress)]; + if (checks.every(Boolean)) { + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + } + break; } - break; } } // some platforms do not include a `success` object, why? @@ -5113,13 +5113,17 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials || {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, username: this.emailProtection.lastGenerated }); - this.storeFormData(formData, 'formSubmission'); + + // If credentials has only username field, and no password field, then trigger is a partialSave + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; + this.storeFormData(formData, trigger); } } @@ -5540,6 +5544,7 @@ function initFormSubmissionsApi(forms, matching) { // @ts-ignore if (btns.find(btn => btn.contains(realTarget))) return true; + return false; }); matchingForm?.submitHandler('global pointerdown event + matching form'); if (!matchingForm) { @@ -5635,7 +5640,7 @@ function overlayApi(device) { * @returns {Promise} */ async selectedDetail(data, type) { - let detailsEntries = Object.entries(data).map(_ref => { + const detailsEntries = Object.entries(data).map(_ref => { let [key, value] = _ref; return [key, String(value)]; }); @@ -5929,7 +5934,7 @@ class Form { if (!input.classList.contains('ddg-autofilled')) return; (0, _autofillUtils.removeInlineStyles)(input, (0, _inputStyles.getIconStylesAutofilled)(input, this)); (0, _autofillUtils.removeInlineStyles)(input, { - 'cursor': 'pointer' + cursor: 'pointer' }); input.classList.remove('ddg-autofilled'); this.addAutofillStyles(input); @@ -6050,20 +6055,10 @@ class Form { if (this.form.matches(selector)) { this.addInput(this.form); } else { - /** @type {Element[] | NodeList} */ - let foundInputs = []; - // Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable. - if (this.form instanceof HTMLFormElement && this.form.elements != null && Symbol.iterator in Object(this.form.elements)) { - // For form elements we use .elements to catch fields outside the form itself using the form attribute. - // It also catches all elements when the markup is broken. - // We use .filter to avoid fieldset, button, textarea etc. - const formElements = [...this.form.elements].filter(el => el.matches(selector)); - // If there are no form elements, we try to look for all - // enclosed elements within the form. - foundInputs = formElements.length > 0 ? formElements : (0, _autofillUtils.findEnclosedElements)(this.form, selector); - } else { - foundInputs = this.form.querySelectorAll(selector); - } + // Attempt to get form's control elements first as it can catch elements when markup is broke, or if the fields are outside the form. + // Other wise use queryElementsWithShadow, that can scan for shadow tree. + const formControlElements = (0, _autofillUtils.getFormControlElements)(this.form, selector); + const foundInputs = formControlElements != null ? [...formControlElements, ...(0, _autofillUtils.findElementsInShadowTree)(this.form, selector)] : (0, _autofillUtils.queryElementsWithShadow)(this.form, selector, true); if (foundInputs.length < MAX_INPUTS_PER_FORM) { foundInputs.forEach(input => this.addInput(input)); } else { @@ -6124,7 +6119,7 @@ class Form { } get submitButtons() { const selector = this.matching.cssSelector('submitButtonSelector'); - const allButtons = /** @type {HTMLElement[]} */(0, _autofillUtils.findEnclosedElements)(this.form, selector); + const allButtons = /** @type {HTMLElement[]} */(0, _autofillUtils.queryElementsWithShadow)(this.form, selector); return allButtons.filter(btn => (0, _autofillUtils.isPotentiallyViewable)(btn) && (0, _autofillUtils.isLikelyASubmitButton)(btn, this.matching) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); } attemptSubmissionIfNeeded() { @@ -6247,12 +6242,12 @@ class Form { if ((0, _autofillUtils.wasAutofilledByChrome)(input)) return; if ((0, _autofillUtils.isEventWithinDax)(e, e.target)) { (0, _autofillUtils.addInlineStyles)(e.target, { - 'cursor': 'pointer', + cursor: 'pointer', ...onMouseMove }); } else { (0, _autofillUtils.removeInlineStyles)(e.target, { - 'cursor': 'pointer' + cursor: 'pointer' }); // Only overwrite active icon styles if tooltip is closed if (!this.device.isTooltipActive()) { @@ -6264,7 +6259,7 @@ class Form { }); this.addListener(input, 'mouseleave', e => { (0, _autofillUtils.removeInlineStyles)(e.target, { - 'cursor': 'pointer' + cursor: 'pointer' }); // Only overwrite active icon styles if tooltip is closed if (!this.device.isTooltipActive()) { @@ -6362,7 +6357,7 @@ class Form { this.touched.add(input); this.device.attachTooltip({ form: this, - input: input, + input, click: clickCoords, trigger: 'userInitiated', triggerMetaData: { @@ -6591,7 +6586,7 @@ class Form { }, 'credentials'); this.device.attachTooltip({ form: this, - input: input, + input, click: null, trigger: 'autoprompt', triggerMetaData: { @@ -6826,6 +6821,23 @@ class FormAnalyzer { } }); } + + /** + * Function that checks if the element is an external link or a custom web element that + * encapsulates a link. + * @param {any} el + * @returns {boolean} + */ + isElementExternalLink(el) { + // Checks if the element is present in the cusotm elements registry and ends with a '-link' suffix. + // If it does, it checks if it contains an anchor element inside. + const tagName = el.nodeName.toLowerCase(); + const isCustomWebElementLink = customElements?.get(tagName) != null && /-link$/.test(tagName) && (0, _autofillUtils.findElementsInShadowTree)(el, 'a').length > 0; + + // if an external link matches one of the regexes, we assume the match is not pertinent to the current form + const isElementLink = el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]'); + return isCustomWebElementLink || isElementLink; + } evaluateElement(el) { const string = (0, _autofillUtils.getTextShallow)(el); if (el.matches(this.matching.cssSelector('password'))) { @@ -6846,7 +6858,7 @@ class FormAnalyzer { if (likelyASubmit) { this.form.querySelectorAll('input[type=submit], button[type=submit]').forEach(submit => { // If there is another element marked as submit and this is not, flip back to false - if (el.type !== 'submit' && el !== submit) { + if (el.getAttribute('type') !== 'submit' && el !== submit) { likelyASubmit = false; } }); @@ -6865,8 +6877,7 @@ class FormAnalyzer { }); return; } - // if an external link matches one of the regexes, we assume the match is not pertinent to the current form - if (el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]')) { + if (this.isElementExternalLink(el)) { let shouldFlip = true; let strength = 1; // Don't flip forgotten password links @@ -6885,9 +6896,10 @@ class FormAnalyzer { }); } else { // any other case + const isH1Element = el.tagName === 'H1'; this.updateSignal({ string, - strength: 1, + strength: isH1Element ? 3 : 1, signalType: `generic: ${string}`, shouldCheckUnifiedForm: true }); @@ -6905,7 +6917,7 @@ class FormAnalyzer { // Check form contents (noisy elements are skipped with the safeUniversalSelector) const selector = this.matching.cssSelector('safeUniversalSelector'); - const formElements = (0, _autofillUtils.findEnclosedElements)(this.form, selector); + const formElements = (0, _autofillUtils.queryElementsWithShadow)(this.form, selector); for (let i = 0; i < formElements.length; i++) { // Safety cutoff to avoid huge DOMs freezing the browser if (i >= 200) break; @@ -6965,7 +6977,7 @@ class FormAnalyzer { } // Match form textContent against common cc fields (includes hidden labels) - const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); + const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/gi); // De-dupe matches to avoid counting the same element more than once const deDupedMatches = new Set(textMatches?.map(match => match.toLowerCase())); @@ -7284,7 +7296,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { Anguilla: 'AI', Albania: 'AL', Armenia: 'AM', - 'Curaçao': 'CW', + Curaçao: 'CW', Angola: 'AO', Antarctica: 'AQ', Argentina: 'AR', @@ -7473,7 +7485,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { Paraguay: 'PY', Qatar: 'QA', 'Outlying Oceania': 'QO', - 'Réunion': 'RE', + Réunion: 'RE', Zimbabwe: 'ZW', Romania: 'RO', Russia: 'SU', @@ -7550,6 +7562,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -7722,21 +7735,10 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = _ref3 => { - let { - credentials - } = _ref3; - return Boolean(credentials.password); -}; - -/** - * @param {InternalDataStorageObject} credentials - * @return {boolean} - */ -const shouldStoreIdentities = _ref4 => { +const shouldStoreIdentities = _ref3 => { let { identities - } = _ref4; + } = _ref3; return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; @@ -7744,10 +7746,10 @@ const shouldStoreIdentities = _ref4 => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreCreditCards = _ref5 => { +const shouldStoreCreditCards = _ref4 => { let { creditCards - } = _ref5; + } = _ref4; if (!creditCards.cardNumber) return false; if (creditCards.cardSecurityCode) return true; // Some forms (Amazon) don't have the cvv, so we still save if there's the expiration @@ -7783,14 +7785,14 @@ const prepareFormValuesForStorage = formValues => { creditCards.cardName = identities?.fullName || formatFullName(identities); } - /** Fixes for credentials **/ - // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues)) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + /** Fixes for credentials */ + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress || identities.phone; + } + + // If we still don't have any credentials, we discard the object + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -7846,7 +7848,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ +},{"../autofill-utils.js":54,"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7889,7 +7891,7 @@ const getBasicStyles = (input, icon) => ({ 'background-repeat': 'no-repeat', 'background-origin': 'content-box', 'background-image': `url(${icon})`, - 'transition': 'background 0s' + transition: 'background 0s' }); /** @@ -7932,7 +7934,7 @@ const getIconStylesAutofilled = (input, form) => { return { ...iconStyle, 'background-color': '#F8F498', - 'color': '#333333' + color: '#333333' }; }; exports.getIconStylesAutofilled = getIconStylesAutofilled; @@ -8220,14 +8222,14 @@ const extractElementStrings = element => { // only take the string when it's an explicit text node if (el.nodeType === el.TEXT_NODE || !el.childNodes.length) { - let trimmedText = (0, _matching.removeExcessWhitespace)(el.textContent); + const trimmedText = (0, _matching.removeExcessWhitespace)(el.textContent); if (trimmedText) { strings.add(trimmedText); } return; } - for (let node of el.childNodes) { - let nodeType = node.nodeType; + for (const node of el.childNodes) { + const nodeType = node.nodeType; if (nodeType !== node.ELEMENT_NODE && nodeType !== node.TEXT_NODE) { continue; } @@ -8629,7 +8631,7 @@ const matchingConfiguration = exports.matchingConfiguration = { match: /sign.?up|join|register|enroll|(create|new).+account|newsletter|subscri(be|ption)|settings|preferences|profile|update|iscri(viti|zione)|registra(ti|zione)|(?:nuovo|crea(?:zione)?) account|contatt(?:ac)?i|sottoscriv|sottoscrizione|impostazioni|preferenze|aggiorna|anmeld(en|ung)|registrier(en|ung)|neukunde|neuer (kunde|benutzer|nutzer)|registreren|eigenschappen|profiel|bijwerken|s.inscrire|inscription|s.abonner|abonnement|préférences|profil|créer un compte|regis(trarse|tro)|regístrate|inscr(ibirse|ipción|íbete)|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo|registrer(a|ing)|(nytt|öppna) konto|nyhetsbrev|prenumer(era|ation)|kontakt|skapa|starta|inställningar|min (sida|kundvagn)|uppdatera/iu }, resetPasswordLink: { - match: /(forgot(ten)?|reset|don't remember) (your )?password|password forgotten|password dimenticata|reset(?:ta) password|recuper[ao] password|(vergessen|verloren|verlegt|wiederherstellen) passwort|wachtwoord (vergeten|reset)|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)|glömt lösenord|återställ lösenord/iu + match: /(forgot(ten)?|reset|don't remember).?(your )?password|password forgotten|password dimenticata|reset(?:ta) password|recuper[ao] password|(vergessen|verloren|verlegt|wiederherstellen) passwort|wachtwoord (vergeten|reset)|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)|glömt lösenord|återställ lösenord/iu }, loginProvidersRegex: { match: / with | con | mit | met | avec /iu @@ -8894,8 +8896,8 @@ class Matching { * * `email: [{type: "email", strategies: {cssSelector: "email", ... etc}]` */ - for (let [listName, matcherNames] of Object.entries(this.#config.matchers.lists)) { - for (let fieldName of matcherNames) { + for (const [listName, matcherNames] of Object.entries(this.#config.matchers.lists)) { + for (const fieldName of matcherNames) { if (!this.#matcherLists[listName]) { this.#matcherLists[listName] = []; } @@ -9012,7 +9014,7 @@ class Matching { * @type {string[]} */ const selectors = []; - for (let matcher of matcherList) { + for (const matcher of matcherList) { if (matcher.strategies.cssSelector) { const css = this.cssSelector(matcher.strategies.cssSelector); if (css) { @@ -9149,12 +9151,12 @@ class Matching { /** * Loop through each strategy in order */ - for (let strategyName of this.#defaultStrategyOrder) { + for (const strategyName of this.#defaultStrategyOrder) { let result; /** * Now loop through each matcher in the list. */ - for (let matcher of matchers) { + for (const matcher of matchers) { /** * for each `strategyName` (such as cssSelector), check * if the current matcher implements it. @@ -9279,16 +9281,16 @@ class Matching { if (!ddgMatcher || !ddgMatcher.match) { return defaultResult; } - let matchRexExp = this.getDDGMatcherRegex(lookup); + const matchRexExp = this.getDDGMatcherRegex(lookup); if (!matchRexExp) { return defaultResult; } - let requiredScore = ['match', 'forceUnknown', 'maxDigits'].filter(ddgMatcherProp => ddgMatcherProp in ddgMatcher).length; + const requiredScore = ['match', 'forceUnknown', 'maxDigits'].filter(ddgMatcherProp => ddgMatcherProp in ddgMatcher).length; /** @type {MatchableStrings[]} */ const matchableStrings = ddgMatcher.matchableStrings || ['labelText', 'placeholderAttr', 'relatedText']; - for (let stringName of matchableStrings) { - let elementString = this.activeElementStrings[stringName]; + for (const stringName of matchableStrings) { + const elementString = this.activeElementStrings[stringName]; if (!elementString) continue; // Scoring to ensure all DDG tests are valid @@ -9304,7 +9306,7 @@ class Matching { // If a negated regex was provided, ensure it does not match // If it DOES match - then we need to prevent any future strategies from continuing if (ddgMatcher.forceUnknown) { - let notRegex = ddgMatcher.forceUnknown; + const notRegex = ddgMatcher.forceUnknown; if (!notRegex) { return { ...result, @@ -9323,7 +9325,7 @@ class Matching { } } if (ddgMatcher.skip) { - let skipRegex = ddgMatcher.skip; + const skipRegex = ddgMatcher.skip; if (!skipRegex) { return { ...result, @@ -9388,8 +9390,8 @@ class Matching { } /** @type {MatchableStrings[]} */ const stringsToMatch = ['placeholderAttr', 'nameAttr', 'labelText', 'id', 'relatedText']; - for (let stringName of stringsToMatch) { - let elementString = this.activeElementStrings[stringName]; + for (const stringName of stringsToMatch) { + const elementString = this.activeElementStrings[stringName]; if (!elementString) continue; if ((0, _autofillUtils.safeRegexTest)(regex, elementString)) { return { @@ -9463,14 +9465,14 @@ class Matching { fields: {} }, strategies: { - 'vendorRegex': { + vendorRegex: { rules: {}, ruleSets: [] }, - 'ddgMatcher': { + ddgMatcher: { matchers: {} }, - 'cssSelector': { + cssSelector: { selectors: {} } } @@ -9641,7 +9643,7 @@ const removeExcessWhitespace = function () { exports.removeExcessWhitespace = removeExcessWhitespace; const getExplicitLabelsText = el => { const labelTextCandidates = []; - for (let label of el.labels || []) { + for (const label of el.labels || []) { labelTextCandidates.push(...(0, _labelUtil.extractElementStrings)(label)); } if (el.hasAttribute('aria-label')) { @@ -9696,7 +9698,7 @@ const getRelatedText = (el, form, cssSelector) => { // If we didn't find a container, try looking for an adjacent label if (scope === el) { - let previousEl = recursiveGetPreviousElSibling(el); + const previousEl = recursiveGetPreviousElSibling(el); if (previousEl instanceof HTMLElement) { scope = previousEl; } @@ -10355,19 +10357,18 @@ class DefaultScanner { if (this.device.globalConfig.isDDGDomain) { return this; } - if ('matches' in context && context.matches?.(this.matching.cssSelector('formInputsSelectorWithoutSelect'))) { + const formInputsSelectorWithoutSelect = this.matching.cssSelector('formInputsSelectorWithoutSelect'); + if ('matches' in context && context.matches?.(formInputsSelectorWithoutSelect)) { this.addInput(context); } else { - const selector = this.matching.cssSelector('formInputsSelectorWithoutSelect'); - const inputs = context.querySelectorAll(selector); + const inputs = context.querySelectorAll(formInputsSelectorWithoutSelect); if (inputs.length > this.options.maxInputsPerPage) { this.setMode('stopped', `Too many input fields in the given context (${inputs.length}), stop scanning`, context); return this; } inputs.forEach(input => this.addInput(input)); if (context instanceof HTMLFormElement && this.forms.get(context)?.hasShadowTree) { - const selector = this.matching.cssSelector('formInputsSelectorWithoutSelect'); - (0, _autofillUtils.findEnclosedElements)(context, selector).forEach(input => { + (0, _autofillUtils.findElementsInShadowTree)(context, formInputsSelectorWithoutSelect).forEach(input => { if (input instanceof HTMLInputElement) { this.addInput(input, context); } @@ -10453,12 +10454,16 @@ class DefaultScanner { } if (element.parentElement) { element = element.parentElement; - const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector')); - const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector')); - // If we find a button or another input, we assume that's our form - if (inputs.length > 1 || buttons.length) { - // found related input, return common ancestor - return element; + // If the parent is a redundant component (only contains a single element or is a shadowRoot) do not increase the traversal count. + if (element.childElementCount > 1) { + const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector')); + const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector')); + // If we find a button or another input, we assume that's our form + if (inputs.length > 1 || buttons.length) { + // found related input, return common ancestor + return element; + } + traversalLayerCount++; } } else { // possibly a shadow boundary, so traverse through the shadow root and find the form @@ -10466,9 +10471,11 @@ class DefaultScanner { if (root instanceof ShadowRoot && root.host) { // @ts-ignore element = root.host; + } else { + // We're in a strange state (no parent or shadow root), just break out of the loop for safety + break; } } - traversalLayerCount++; } return input; } @@ -10551,7 +10558,7 @@ class DefaultScanner { this.changedElements.clear(); } else if (!this.rescanAll) { // otherwise keep adding each element to the queue - for (let element of htmlElements) { + for (const element of htmlElements) { this.changedElements.add(element); } } @@ -10575,7 +10582,7 @@ class DefaultScanner { this.findEligibleInputs(document); return; } - for (let element of this.changedElements) { + for (const element of this.changedElements) { if (element.isConnected) { this.findEligibleInputs(element); } @@ -10596,7 +10603,7 @@ class DefaultScanner { const outgoing = []; for (const mutationRecord of mutationList) { if (mutationRecord.type === 'childList') { - for (let addedNode of mutationRecord.addedNodes) { + for (const addedNode of mutationRecord.addedNodes) { if (!(addedNode instanceof HTMLElement)) continue; if (addedNode.nodeName === 'DDG-AUTOFILL') continue; outgoing.push(addedNode); @@ -10630,12 +10637,13 @@ class DefaultScanner { // find the enclosing parent form, and scan it. if (realTarget instanceof HTMLInputElement && !realTarget.hasAttribute(ATTR_INPUT_TYPE)) { const parentForm = this.getParentForm(realTarget); - if (parentForm && parentForm instanceof HTMLFormElement) { - const hasShadowTree = event.target?.shadowRoot != null; - const form = new _Form.Form(parentForm, realTarget, this.device, this.matching, this.shouldAutoprompt, hasShadowTree); - this.forms.set(parentForm, form); - this.findEligibleInputs(parentForm); - } + + // If the parent form is an input element we bail. + if (parentForm instanceof HTMLInputElement) return; + const hasShadowTree = event.target?.shadowRoot != null; + const form = new _Form.Form(parentForm, realTarget, this.device, this.matching, this.shouldAutoprompt, hasShadowTree); + this.forms.set(parentForm, form); + this.findEligibleInputs(parentForm); } window.performance?.mark?.('scan_shadow:init:end'); (0, _autofillUtils.logPerformance)('scan_shadow'); @@ -11334,7 +11342,7 @@ ${css} if (btn.matches('.wrapper:not(.top-autofill) button:hover, .currentFocus')) { callbacks.onSelect(btn.id); } else { - console.warn('The button doesn\'t seem to be hovered. Please check.'); + console.warn("The button doesn't seem to be hovered. Please check."); } }); }); @@ -11538,7 +11546,9 @@ const defaultOptions = exports.defaultOptions = { }`, css: ``, setSize: undefined, - remove: () => {/** noop */}, + remove: () => { + /** noop */ + }, testMode: false, checkVisibility: true, hasCaret: false, @@ -11566,9 +11576,9 @@ class HTMLTooltip { this.tooltip = null; this.getPosition = getPosition; const forcedVisibilityStyles = { - 'display': 'block', - 'visibility': 'visible', - 'opacity': '1' + display: 'block', + visibility: 'visible', + opacity: '1' }; // @ts-ignore how to narrow this.host to HTMLElement? (0, _autofillUtils.addInlineStyles)(this.host, forcedVisibilityStyles); @@ -11826,7 +11836,7 @@ class HTMLTooltip { checkVisibility: this.options.checkVisibility }); } else { - console.warn('The button doesn\'t seem to be hovered. Please check.'); + console.warn("The button doesn't seem to be hovered. Please check."); } } } @@ -12770,10 +12780,14 @@ Object.defineProperty(exports, "__esModule", { }); exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; exports.escapeXML = escapeXML; -exports.findEnclosedElements = findEnclosedElements; +exports.findElementsInShadowTree = findElementsInShadowTree; exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = exports.getDaxBoundingBox = void 0; +exports.getDaxBoundingBox = void 0; +exports.getFormControlElements = getFormControlElements; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -12782,6 +12796,7 @@ exports.isValidTLD = isValidTLD; exports.logPerformance = logPerformance; exports.notifyWebApp = void 0; exports.pierceShadowTree = pierceShadowTree; +exports.queryElementsWithShadow = queryElementsWithShadow; exports.safeExecute = exports.removeInlineStyles = void 0; exports.safeRegexTest = safeRegexTest; exports.setValue = exports.sendAndWaitForAnswer = void 0; @@ -13154,8 +13169,9 @@ const isLikelyASubmitButton = (el, matching) => { // has high-signal submit classes safeRegexTest(/submit/i, dataTestId) || safeRegexTest(matching.getDDGMatcherRegex('submitButtonRegex'), text) || // has high-signal text - el.offsetHeight * el.offsetWidth >= 10000 && !safeRegexTest(/secondary/i, el.className) // it's a large element 250x40px - ) && el.offsetHeight * el.offsetWidth >= 2000 && + el.offsetHeight * el.offsetWidth >= 10000 && !safeRegexTest(/secondary/i, el.className)) && + // it's a large element 250x40px + el.offsetHeight * el.offsetWidth >= 2000 && // it's not a very small button like inline links and such !safeRegexTest(matching.getDDGMatcherRegex('submitButtonUnlikelyRegex'), text + ' ' + ariaLabel); }; @@ -13383,22 +13399,16 @@ function getActiveElement() { } /** - * Takes a root element and tries to find visible elements first, and if it fails, it tries to find shadow elements + * Takes a root element and tries to find elements in shadow DOMs that match the selector * @param {HTMLElement|HTMLFormElement} root * @param {string} selector * @returns {Element[]} */ -function findEnclosedElements(root, selector) { - // Check if there are any normal elements that match the selector - const elements = root.querySelectorAll(selector); - if (elements.length > 0) { - return Array.from(elements); - } - - // Check if there are any shadow elements that match the selector +function findElementsInShadowTree(root, selector) { const shadowElements = []; const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); - let node = walker.nextNode(); + /** @type {Node|null} */ + let node = walker.currentNode; while (node) { if (node instanceof HTMLElement && node.shadowRoot) { shadowElements.push(...node.shadowRoot.querySelectorAll(selector)); @@ -13408,6 +13418,52 @@ function findEnclosedElements(root, selector) { return shadowElements; } +/** + * The function looks for form's control elements, and returns them if they're iterable. + * @param {HTMLElement} form + * @param {string} selector + * @returns {Element[]|null} + */ +function getFormControlElements(form, selector) { + // Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable. + if (form instanceof HTMLFormElement && form.elements != null && Symbol.iterator in Object(form.elements)) { + // For form elements we use .elements to catch fields outside the form itself using the form attribute. + // It also catches all elements when the markup is broken. + // We use .filter to avoid specific types of elements. + const formControls = [...form.elements].filter(el => el.matches(selector)); + return [...formControls]; + } else { + return null; + } +} + +/** + * Default operation: finds elements using querySelectorAll. + * Optionally, can be forced to scan the shadow tree. + * @param {HTMLElement} element + * @param {string} selector + * @param {boolean} forceScanShadowTree + * @returns {Element[]} + */ +function queryElementsWithShadow(element, selector) { + let forceScanShadowTree = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + /** @type {Element[]|NodeListOf} element */ + const elements = element.querySelectorAll(selector); + if (forceScanShadowTree || elements.length === 0) { + return [...elements, ...findElementsInShadowTree(element, selector)]; + } + return [...elements]; +} + +/** + * Checks if there is a single username-like identity, i.e. email or phone + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":34,"./constants.js":57,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],55:[function(require,module,exports){ "use strict"; @@ -13447,7 +13503,8 @@ Object.defineProperty(exports, "__esModule", { }); exports.DDG_DOMAIN_REGEX = void 0; exports.createGlobalConfig = createGlobalConfig; -const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = new RegExp(/^https:\/\/(([a-z0-9-_]+?)\.)?duckduckgo\.com\/email/); +/* eslint-disable prefer-const */ +const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = /^https:\/\/(([a-z0-9-_]+?)\.)?duckduckgo\.com\/email/; /** * This is a centralised place to contain all string/variable replacements @@ -13828,25 +13885,25 @@ const contentScopeSchema = exports.contentScopeSchema = null; const userPreferencesSchema = exports.userPreferencesSchema = null; const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = null; const availableInputTypesSchema = exports.availableInputTypesSchema = null; -const availableInputTypes1Schema = exports.availableInputTypes1Schema = null; -const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = null; -const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = null; -const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = null; -const storeFormDataSchema = exports.storeFormDataSchema = null; -const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = null; const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = null; const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = null; -const autofillSettingsSchema = exports.autofillSettingsSchema = null; +const availableInputTypes1Schema = exports.availableInputTypes1Schema = null; +const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null; +const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = null; const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = null; const emailProtectionGetUserDataResultSchema = exports.emailProtectionGetUserDataResultSchema = null; const emailProtectionGetCapabilitiesResultSchema = exports.emailProtectionGetCapabilitiesResultSchema = null; const emailProtectionGetAddressesResultSchema = exports.emailProtectionGetAddressesResultSchema = null; const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtectionRefreshPrivateAddressResultSchema = null; -const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = null; -const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null; -const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = null; +const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = null; +const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = null; +const storeFormDataSchema = exports.storeFormDataSchema = null; +const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = null; const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = null; const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProviderStatusResultSchema = null; +const autofillSettingsSchema = exports.autofillSettingsSchema = null; +const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = null; +const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = null; const apiSchema = exports.apiSchema = null; },{}],60:[function(require,module,exports){ @@ -13964,7 +14021,7 @@ function waitForResponse(expectedResponse, config) { return; } try { - let data = JSON.parse(e.data); + const data = JSON.parse(e.data); if (data.type === expectedResponse) { window.removeEventListener('message', handler); return resolve(data); @@ -14119,7 +14176,7 @@ async function extensionSpecificRuntimeConfiguration(deviceApi) { return { success: { // @ts-ignore - contentScope: contentScope, + contentScope, // @ts-ignore userPreferences: { // Copy locale to user preferences as 'language' to match expected payload @@ -14313,6 +14370,7 @@ function waitForWindowsResponse(responseId, options) { if (options?.signal?.aborted) { return reject(new DOMException('Aborted', 'AbortError')); } + // eslint-disable-next-line prefer-const let teardown; // The event handler @@ -17199,7 +17257,6 @@ exports.default = void 0; window.requestIdleCallback = window.requestIdleCallback || function (cb) { return setTimeout(function () { const start = Date.now(); - // eslint-disable-next-line standard/no-callback-literal cb({ didTimeout: false, timeRemaining: function () { diff --git a/package-lock.json b/package-lock.json index f331e7509a7f..2a5d1d560a3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@duckduckgo/autoconsent": "^12.3.0", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#15.1.0", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#16.0.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.41.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#7.3.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1724449523" @@ -58,7 +58,7 @@ } }, "node_modules/@duckduckgo/autofill": { - "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#c992041d16ec10d790e6204dce9abf9966d1363c", + "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#88982a3802ac504e2f1a118a73bfdf2d8f4a7735", "hasInstallScript": true, "license": "Apache-2.0" }, diff --git a/package.json b/package.json index 52aeff559b23..cfb6f34815aa 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@duckduckgo/autoconsent": "^12.3.0", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#15.1.0", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#16.0.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.41.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#7.3.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1724449523"