Commit 7539e99a authored by Victor Hugo Vianna Silva's avatar Victor Hugo Vianna Silva Committed by Commit Bot

[Passwords/Settings] Restrict plaintext pwd to ShowPasswordBehavior

No behavior is changed.

Before this CL, passwords_section was responsible for initializing and
storing the displayed password strings of each element in savedPasswords
and multiStoreSavedPasswords. Those were even initialized to the empty
string, relying on implementation details of ShowPasswordBehavior
(an empty string means the plaintext password is not being shown).

This CL makes the password string a property of ShowPasswordBehavior,
simplifying the passwords_section code. As a consequence, the
PasswordUiEntryWithPassword and MultiStorePasswordUiEntryWithPassword
typedefs are no longer needed. RemovePasswordBehavior's entryToRemove
property and ShowPasswordBehavior's item property are both renamed to
entry, since they represent the same object.

Bug: None
Change-Id: I58b58626999de5144913730d853ae0a4b839ae56
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2215321
Commit-Queue: Victor Vianna <victorvianna@google.com>
Reviewed-by: default avatarFriedrich [CET] <fhorschig@chromium.org>
Reviewed-by: default avatarMarc Treib <treib@chromium.org>
Reviewed-by: default avatarJan Wilken Dörrie <jdoerrie@chromium.org>
Cr-Commit-Position: refs/heads/master@{#771813}
parent 90150a27
......@@ -124,8 +124,3 @@ export class MultiStorePasswordUiEntry {
* }}
*/
MultiStorePasswordUiEntry.Contents;
/**
* @typedef {{ entry: !MultiStorePasswordUiEntry, password: string }}
*/
export let MultiStorePasswordUiEntryWithPassword;
......@@ -36,20 +36,20 @@
<div hidden="[[!shouldShowStorageDetails]]" id="storageDetails"
inner-h-t-m-l="[[getStorageDetailsMessage_()]]"></div>
<cr-input id="websiteInput" label="$i18n{editPasswordWebsiteLabel}"
value="[[item.entry.urls.link]]" on-blur="onInputBlur_" readonly>
value="[[entry.urls.link]]" on-blur="onInputBlur_" readonly>
</cr-input>
<cr-input id="usernameInput" label="$i18n{editPasswordUsernameLabel}"
value="[[item.entry.username]]" on-blur="onInputBlur_" readonly>
value="[[entry.username]]" on-blur="onInputBlur_" readonly>
</cr-input>
<cr-input id="passwordInput" label="$i18n{editPasswordPasswordLabel}"
type="[[getPasswordInputType_(item.password)]]"
value="[[getPassword_(item.password)]]" on-blur="onInputBlur_"
type="[[getPasswordInputType_(password)]]"
value="[[getPassword_(password)]]" on-blur="onInputBlur_"
class="password-input" readonly>
<cr-icon-button id="showPasswordButton"
class$="[[getIconClass_(item.password)]]" slot="suffix"
hidden$="[[item.entry.federationText]]"
class$="[[getIconClass_(password)]]" slot="suffix"
hidden$="[[entry.federationText]]"
on-click="onShowPasswordButtonTap_"
title="[[showPasswordTitle_(item.password,
title="[[showPasswordTitle_(password,
'$i18nPolymer{hidePassword}',
'$i18nPolymer{showPassword}')]]">
</cr-icon-button>
......
......@@ -65,7 +65,7 @@ Polymer({
getStorageDetailsMessage_() {
// TODO(crbug.com/1049141): Add support and tests for the multi-store case
// when dedup is being done.
return this.item.entry.isPresentInAccount() ?
return this.entry.isPresentInAccount() ?
this.i18nAdvanced('passwordStoredInAccount', {tags: ['b']}) :
this.i18nAdvanced('passwordStoredOnDevice', {tags: ['b']});
}
......
......@@ -27,44 +27,44 @@
</style>
<div class="list-item" focus-row-container>
<div class="website-column no-min-width">
<site-favicon url="[[item.entry.urls.link]]"></site-favicon>
<site-favicon url="[[entry.urls.link]]"></site-favicon>
<a id="originUrl" target="_blank" class="no-min-width"
href="[[item.entry.urls.link]]"
href="[[entry.urls.link]]"
focus-row-control focus-type="originUrl">
<span class="text-elide">
<!-- This bdo tag is necessary to fix the display of domains
starting with numbers. -->
<bdo dir="ltr">[[item.entry.urls.shown]]</bdo>
<bdo dir="ltr">[[entry.urls.shown]]</bdo>
</span>
</a>
</div>
<input id="username" class="username-column password-field"
aria-label="$i18n{editPasswordUsernameLabel}"
readonly value="[[item.entry.username]]"
readonly value="[[entry.username]]"
focus-row-control focus-type="username">
<div class="password-column">
<template is="dom-if" if="[[!item.entry.federationText]]">
<template is="dom-if" if="[[!entry.federationText]]">
<input id="password" aria-label=$i18n{editPasswordPasswordLabel}
type="[[getPasswordInputType_(item.password)]]"
type="[[getPasswordInputType_(password)]]"
class="password-field password-input" readonly
disabled$="[[!item.password]]" on-click="onReadonlyInputTap_"
value="[[getPassword_(item.password)]]"
disabled$="[[!password]]" on-click="onReadonlyInputTap_"
value="[[getPassword_(password)]]"
focus-row-control focus-type="passwordField">
<cr-icon-button id="showPasswordButton"
class$="[[getIconClass_(item.password)]]"
class$="[[getIconClass_(password)]]"
on-click="onShowPasswordButtonTap_"
title="[[showPasswordTitle_(item.password,
title="[[showPasswordTitle_(password,
'$i18nPolymer{hidePassword}',
'$i18nPolymer{showPassword}')]]"
focus-row-control focus-type="showPassword"></cr-icon-button>
</template>
<span class="password-field text-elide" id="federated"
hidden$="[[!item.entry.federationText]]">
[[item.entry.federationText]]
hidden$="[[!entry.federationText]]">
[[entry.federationText]]
</span>
</div>
<cr-icon-button class="icon-more-vert" id="passwordMenu"
on-click="onPasswordMenuTap_" title="$i18n{moreActions}"
focus-row-control focus-type="passwordMenu"
aria-label$="[[getMoreActionsLabel_(item)]]"></cr-icon-button>
aria-label$="[[getMoreActionsLabel_(entry)]]"></cr-icon-button>
</div>
......@@ -18,7 +18,7 @@ import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bun
import {loadTimeData} from '../i18n_setup.js';
import {MultiStorePasswordUiEntryWithPassword} from './multi_store_password_ui_entry.js';
import {MultiStorePasswordUiEntry} from './multi_store_password_ui_entry.js';
import {RemovePasswordBehavior} from './remove_password_behavior.js';
import {ShowPasswordBehavior} from './show_password_behavior.js';
......@@ -38,7 +38,7 @@ Polymer({
* @private
*/
onReadonlyInputTap_() {
if (this.item.password) {
if (this.password) {
this.$$('#password').select();
}
},
......@@ -54,16 +54,15 @@ Polymer({
/**
* Get the aria label for the More Actions button on this row.
* @param {!MultiStorePasswordUiEntryWithPassword} item This row's item.
* @private
*/
getMoreActionsLabel_(item) {
getMoreActionsLabel_() {
// Avoid using I18nBehavior.i18n, because it will filter sequences, which
// are otherwise not illegal for usernames. Polymer still protects against
// XSS injection.
return loadTimeData.getStringF(
(item.entry.federationText) ? 'passwordRowFederatedMoreActionsButton' :
(this.entry.federationText) ? 'passwordRowFederatedMoreActionsButton' :
'passwordRowMoreActionsButton',
item.entry.username, item.entry.urls.shown);
this.entry.username, this.entry.urls.shown);
},
});
......@@ -265,11 +265,6 @@ PasswordManagerProxy.UrlCollection;
/** @typedef {chrome.passwordsPrivate.ExceptionEntry} */
PasswordManagerProxy.ExceptionEntry;
/**
* @typedef {{ entry: !PasswordManagerProxy.PasswordUiEntry, password: string }}
*/
PasswordManagerProxy.UiEntryWithPassword;
/** @typedef {chrome.passwordsPrivate.PasswordExportProgress} */
PasswordManagerProxy.PasswordExportProgress;
......
......@@ -169,7 +169,9 @@
class="cr-separators list-with-header"
scroll-target="[[subpageScrollTarget]]" risk-selection>
<template>
<password-list-item item="[[item]]" entry-to-remove="[[item.entry]]"
<!-- The entry property is shared by ShowPasswordBehavior and
RemovePasswordBehavior. -->
<password-list-item entry="[[item]]"
tabindex$="[[tabIndex]]" focus-row-index="[[index]]"
<if expr="chromeos">
token-request-manager="[[tokenRequestManager_]]"
......@@ -188,7 +190,7 @@
<template is="dom-if" if="[[enablePasswordCheck_]]">
<button id="menuCopyPassword" class="dropdown-item"
on-click="onMenuCopyPasswordButtonTap_"
hidden$="[[activePassword.item.entry.federationText]]">
hidden$="[[activePassword.entry.federationText]]">
$i18n{copyPassword}
</button>
</template>
......@@ -221,7 +223,8 @@
<if expr="chromeos">
token-request-manager="[[tokenRequestManager_]]"
</if>
item="[[activePassword.item]]"
entry="[[activePassword.entry]]"
password="{{activePassword.password}}"
should-show-storage-details=
"[[shouldShowStorageDetailsInEditDialog_]]">
</password-edit-dialog>
......
......@@ -8,8 +8,6 @@
* save any passwords.
*/
/** @typedef {!{model: !{item: !PasswordManagerProxy.UiEntryWithPassword}}} */
let PasswordUiEntryEvent;
/** @typedef {!{model: !{item: !chrome.passwordsPrivate.ExceptionEntry}}} */
let ExceptionEntryEntryEvent;
......@@ -17,7 +15,7 @@ let ExceptionEntryEntryEvent;
import {afterNextRender, html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.m.js';
import {MultiStorePasswordUiEntry, MultiStorePasswordUiEntryWithPassword} from './multi_store_password_ui_entry.js';
import {MultiStorePasswordUiEntry} from './multi_store_password_ui_entry.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.m.js';
import 'chrome://resources/cr_elements/cr_link_row/cr_link_row.m.js';
......@@ -102,7 +100,7 @@ Polymer({
/**
* An array of passwords with all the stored versions.
* @type {!Array<!PasswordManagerProxy.UiEntryWithPassword>}
* @type {!Array<!PasswordManagerProxy.PasswordUiEntry>}
*/
savedPasswords: {
type: Array,
......@@ -112,7 +110,7 @@ Polymer({
/**
* Saved passwords after deduplicating versions that are repeated in the
* account and on the device.
* @type {!Array<!MultiStorePasswordUiEntryWithPassword>}
* @type {!Array<!MultiStorePasswordUiEntry>}
*/
multiStoreSavedPasswords: {
type: Array,
......@@ -190,8 +188,8 @@ Polymer({
shouldShowStorageDetailsInEditDialog_: {
type: Boolean,
value: false,
computed: 'computeShouldShowStorageDetailsInEditDialog_('+
'eligibleForAccountStorage_, isOptedInForAccountStorage_)',
computed: 'computeShouldShowStorageDetailsInEditDialog_(' +
'eligibleForAccountStorage_, isOptedInForAccountStorage_)',
},
/** @private */
......@@ -247,7 +245,7 @@ Polymer({
value: '',
},
/** @private {!MultiStorePasswordUiEntryWithPassword} */
/** @private {!MultiStorePasswordUiEntry} */
lastFocused_: Object,
/** @private */
......@@ -314,11 +312,12 @@ Polymer({
};
const setSavedPasswordsListener = list => {
const newList = list.map(entry => ({entry: entry, password: ''}));
// Because the backend guarantees that item.entry.id uniquely identifies a
// Because the backend guarantees that entry.id uniquely identifies a
// given entry and is stable with regard to mutations to the list, it is
// sufficient to just use this id to create a item uid.
this.updateList('savedPasswords', item => item.entry.id, newList);
// sufficient to just use this id to create an entry uid.
// TODO(crbug.com/1049141): Get rid of the savedPasswords property by
// finding a nice id to use here, such as [account_id]_[device_id].
this.updateList('savedPasswords', entry => entry.id, list);
this.hasStoredPasswords_ = list.length > 0;
};
......@@ -457,10 +456,6 @@ Polymer({
onPasswordEditDialogClosed_() {
this.showPasswordEditDialog_ = false;
focusWithoutInk(assert(this.activeDialogAnchorStack_.pop()));
// Trigger a re-evaluation of the activePassword as the visibility state of
// the password might have changed.
this.activePassword.notifyPath('item.password');
},
/**
......@@ -474,15 +469,12 @@ Polymer({
},
/**
* @return {!Array<!MultiStorePasswordUiEntryWithPassword>}
* @return {!Array<!MultiStorePasswordUiEntry>}
* @private
*/
computeMultiStoreSavedPasswords_() {
return this.savedPasswords.map(
item => ({
entry: new MultiStorePasswordUiEntry(item.entry),
password: item.password
}));
entry => new MultiStorePasswordUiEntry(entry));
},
/**
......@@ -524,7 +516,7 @@ Polymer({
/**
* @param {string} filter
* @return {!Array<!MultiStorePasswordUiEntryWithPassword>}
* @return {!Array<!MultiStorePasswordUiEntry>}
* @private
*/
getFilteredMultiStorePasswords_(filter) {
......@@ -533,7 +525,7 @@ Polymer({
}
return this.multiStoreSavedPasswords.filter(
p => [p.entry.urls.shown, p.entry.username].some(
p => [p.urls.shown, p.username].some(
term => term.toLowerCase().includes(filter.toLowerCase())));
},
......@@ -583,7 +575,7 @@ Polymer({
// result back to javascript.
this.passwordManager_
.requestPlaintextPassword(
this.activePassword.item.entry.getAnyId(),
this.activePassword.entry.getAnyId(),
chrome.passwordsPrivate.PlaintextReason.COPY)
.catch(error => {
// <if expr="chromeos">
......@@ -689,7 +681,7 @@ Polymer({
},
/**
* Returns true if the list exists and has items.
* Returns true if the list exists and is not empty.
* @param {Array<Object>} list
* @return {boolean}
* @private
......
......@@ -17,7 +17,7 @@ export const RemovePasswordBehavior = {
* The password that will be removed.
* @type {!MultiStorePasswordUiEntry}
*/
entryToRemove: Object,
entry: Object,
},
/**
......@@ -35,13 +35,13 @@ export const RemovePasswordBehavior = {
/** @type {!Array<number>} */
const idsToRemove = [];
if (this.entryToRemove.isPresentInAccount()) {
if (this.entry.isPresentInAccount()) {
result.removedFromAccount = true;
idsToRemove.push(this.entryToRemove.accountId);
idsToRemove.push(this.entry.accountId);
}
if (this.entryToRemove.isPresentOnDevice()) {
if (this.entry.isPresentOnDevice()) {
result.removedFromDevice = true;
idsToRemove.push(this.entryToRemove.deviceId);
idsToRemove.push(this.entry.deviceId);
}
if (idsToRemove.length) {
......
......@@ -5,7 +5,7 @@
// <if expr="chromeos">
import {BlockingRequestManager} from './blocking_request_manager.js';
// </if>
import {MultiStorePasswordUiEntryWithPassword} from './multi_store_password_ui_entry.js';
import {MultiStorePasswordUiEntry} from './multi_store_password_ui_entry.js';
import {PasswordManagerImpl} from './password_manager_proxy.js';
/**
......@@ -18,10 +18,18 @@ export const ShowPasswordBehavior = {
properties: {
/**
* The password that is being displayed.
* @type {!MultiStorePasswordUiEntryWithPassword}
* @type {!MultiStorePasswordUiEntry}
*/
item: Object,
entry: Object,
/** The password that is being displayed. */
password: {
type: String,
value: '',
// If the password is initialized by the parent component, changes in
// visibility should be reflected there too.
notify: true
},
// <if expr="chromeos">
/** @type BlockingRequestManager */
......@@ -35,8 +43,7 @@ export const ShowPasswordBehavior = {
* @private
*/
getPasswordInputType_() {
return this.item.password || this.item.entry.federationText ? 'text' :
'password';
return this.password || this.entry.federationText ? 'text' : 'password';
},
/**
......@@ -56,7 +63,7 @@ export const ShowPasswordBehavior = {
* @private
*/
getIconClass_() {
return this.item.password ? 'icon-visibility-off' : 'icon-visibility';
return this.password ? 'icon-visibility-off' : 'icon-visibility';
},
/**
......@@ -66,12 +73,12 @@ export const ShowPasswordBehavior = {
* @private
*/
getPassword_() {
if (!this.item) {
if (!this.entry) {
return '';
}
const NUM_PLACEHOLDERS = 10;
return this.item.entry.federationText || this.item.password ||
return this.entry.federationText || this.password ||
' '.repeat(NUM_PLACEHOLDERS);
},
......@@ -80,17 +87,16 @@ export const ShowPasswordBehavior = {
* @private
*/
onShowPasswordButtonTap_() {
if (this.item.password) {
this.set('item.password', '');
if (this.password) {
this.password = '';
return;
}
PasswordManagerImpl.getInstance()
.requestPlaintextPassword(
this.item.entry.getAnyId(),
chrome.passwordsPrivate.PlaintextReason.VIEW)
this.entry.getAnyId(), chrome.passwordsPrivate.PlaintextReason.VIEW)
.then(
password => {
this.set('item.password', password);
this.password = password;
},
error => {
// <if expr="chromeos">
......
......@@ -6,7 +6,7 @@ import './settings_ui/settings_ui.js';
export {AboutPageBrowserProxy, AboutPageBrowserProxyImpl, PromoteUpdaterStatus, UpdateStatus} from './about_page/about_page_browser_proxy.m.js';
export {AppearanceBrowserProxy, AppearanceBrowserProxyImpl} from './appearance_page/appearance_browser_proxy.js';
export {MultiStorePasswordUiEntry, MultiStorePasswordUiEntryWithPassword} from './autofill_page/multi_store_password_ui_entry.js';
export {MultiStorePasswordUiEntry} from './autofill_page/multi_store_password_ui_entry.js';
export {PasswordManagerImpl, PasswordManagerProxy} from './autofill_page/password_manager_proxy.js';
// <if expr="not chromeos">
export {DefaultBrowserBrowserProxyImpl} from './default_browser_page/default_browser_browser_proxy.js';
......
......@@ -186,10 +186,7 @@ suite('PasswordsAndForms', function() {
passwordManager.lastCallback.addSavedPasswordListChangedListener(list);
flush();
assertDeepEquals(
list,
element.$$('#passwordSection')
.savedPasswords.map(entry => entry.entry));
assertDeepEquals(list, element.$$('#passwordSection').savedPasswords);
// The callback is coming from the manager, so the element shouldn't
// have additional calls to the manager after the base expectations.
......
......@@ -231,10 +231,8 @@ export class PasswordSectionElementFactory {
*/
createPasswordListItem(passwordEntry) {
const passwordListItem = this.document.createElement('password-list-item');
passwordListItem.item = {
entry: new MultiStorePasswordUiEntry(passwordEntry),
password: ''
};
passwordListItem.entry = new MultiStorePasswordUiEntry(passwordEntry);
passwordListItem.password = '';
this.document.body.appendChild(passwordListItem);
flush();
return passwordListItem;
......@@ -247,10 +245,8 @@ export class PasswordSectionElementFactory {
*/
createPasswordEditDialog(passwordEntry) {
const passwordDialog = this.document.createElement('password-edit-dialog');
passwordDialog.item = {
entry: new MultiStorePasswordUiEntry(passwordEntry),
password: ''
};
passwordDialog.entry = new MultiStorePasswordUiEntry(passwordEntry);
passwordDialog.password = '';
this.document.body.appendChild(passwordDialog);
flush();
return passwordDialog;
......
......@@ -44,8 +44,7 @@ function validatePasswordList(passwordsSection, passwordList) {
assertEquals(passwordInfo.urls.link, node.$$('#originUrl').href);
assertEquals(passwordInfo.username, node.$$('#username').value);
assertDeepEquals(
listElement.items[index].entry,
new MultiStorePasswordUiEntry(passwordInfo));
listElement.items[index], new MultiStorePasswordUiEntry(passwordInfo));
}
}
......@@ -186,7 +185,7 @@ suite('PasswordsSection', function() {
// then other expectations will also fail.
assertDeepEquals(
passwordList.map(entry => new MultiStorePasswordUiEntry(entry)),
passwordsSection.$.passwordList.items.map(entry => entry.entry));
passwordsSection.$.passwordList.items);
validatePasswordList(passwordsSection, passwordList);
......@@ -212,9 +211,8 @@ suite('PasswordsSection', function() {
passwordList);
flush();
assertFalse(listContainsUrl(
passwordsSection.savedPasswords.map(entry => entry.entry),
'longwebsite.com'));
assertFalse(
listContainsUrl(passwordsSection.savedPasswords, 'longwebsite.com'));
assertFalse(listContainsUrl(passwordList, 'longwebsite.com'));
validatePasswordList(passwordsSection, passwordList);
......@@ -590,7 +588,7 @@ suite('PasswordsSection', function() {
assertFalse(passwordDialog.$.showPasswordButton.hidden);
passwordDialog.set('item.password', PASSWORD);
passwordDialog.password = PASSWORD;
flush();
assertEquals(PASSWORD, passwordDialog.$.passwordInput.value);
......@@ -606,7 +604,7 @@ suite('PasswordsSection', function() {
// Hidden passwords should be disabled.
assertTrue(passwordListItem.$$('#password').disabled);
passwordListItem.set('item.password', PASSWORD);
passwordListItem.password = PASSWORD;
flush();
assertEquals(PASSWORD, passwordListItem.$$('#password').value);
......@@ -626,7 +624,7 @@ suite('PasswordsSection', function() {
const expectedItem = createPasswordEntry('goo.gl', 'bart', 1);
const passwordDialog =
elementFactory.createPasswordEditDialog(expectedItem);
assertEquals('', passwordDialog.item.password);
assertEquals('', passwordDialog.password);
passwordManager.setPlaintextPassword('password');
passwordDialog.$.showPasswordButton.click();
......@@ -634,7 +632,7 @@ suite('PasswordsSection', function() {
.then(({id, reason}) => {
assertEquals(1, id);
assertEquals('VIEW', reason);
assertEquals('password', passwordDialog.item.password);
assertEquals('password', passwordDialog.password);
});
});
......@@ -642,7 +640,7 @@ suite('PasswordsSection', function() {
const expectedItem = createPasswordEntry('goo.gl', 'bart', 1);
const passwordListItem =
elementFactory.createPasswordListItem(expectedItem);
assertEquals('', passwordListItem.item.password);
assertEquals('', passwordListItem.password);
passwordManager.setPlaintextPassword('password');
passwordListItem.$$('#showPasswordButton').click();
......@@ -650,7 +648,7 @@ suite('PasswordsSection', function() {
.then(({id, reason}) => {
assertEquals(1, id);
assertEquals('VIEW', reason);
assertEquals('password', passwordListItem.item.password);
assertEquals('password', passwordListItem.password);
});
});
......
......@@ -13,7 +13,7 @@
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {BlockingRequestManager} from 'chrome://settings/lazy_load.js';
import {MultiStorePasswordUiEntryWithPassword, PasswordManagerImpl} from 'chrome://settings/settings.js';
import {MultiStorePasswordUiEntry, PasswordManagerImpl} from 'chrome://settings/settings.js';
import {MockTimer} from 'chrome://test/mock_timer.m.js';
import {createPasswordEntry, PasswordSectionElementFactory} from 'chrome://test/settings/passwords_and_autofill_fake_data.js';
import {runCancelExportTest, runExportFlowErrorRetryTest, runExportFlowErrorTest, runExportFlowFastTest, runExportFlowSlowTest, runFireCloseEventAfterExportCompleteTest,runStartExportTest} from 'chrome://test/settings/passwords_export_test.js';
......@@ -49,8 +49,7 @@ suite('PasswordsSection_Cros', function() {
* Tests of the password-section element need to use the full
* implementation, which is created by default when the element is
* attached.
* @param {MultiStorePasswordUiEntryWithPassword} passwordItem Wrapper
* for a PasswordUiEntry and the corresponding password.
* @param {MultiStorePasswordUiEntry} passwordItem
*/
constructor(document, tokenRequestManager, passwordItem) {
super(document);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment