0

[CrOS Bluetooth] Sanitize Bluetooth device names

This CL updates getDeviceName for in bluetooth_utils.ts and marks it as
unsafe. This is because we allow users to enter any string as the device
name. We make sure to use  loadTimeData.getStringF when displaying
the device name in HTML so the HTML is not rendered.

Fixed: b/298724102
Test: Deployed to DUT, added test and ran CQ
Change-Id: Icddf7ad76fff13bcf4511f118a6500b0009e275f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5238635
Commit-Queue: Theo Johnson-kanu <tjohnsonkanu@google.com>
Reviewed-by: Chad Duffin <chadduffin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1257566}
This commit is contained in:
Theo Johnson-Kanu
2024-02-07 21:01:50 +00:00
committed by Chromium LUCI CQ
parent e027c63ebf
commit 473d45cf37
14 changed files with 143 additions and 73 deletions

@@ -7,16 +7,20 @@ import {BluetoothDeviceProperties, PairedBluetoothDeviceProperties} from 'chrome
import {BatteryType} from './bluetooth_types.js'; import {BatteryType} from './bluetooth_types.js';
export function getDeviceName(device: PairedBluetoothDeviceProperties | null): string {
if (!device) { /**
* WARNING: The returned string may contain malicious HTML and should not be
* used for Polymer bindings in CSS code. For additional information see
* b/298724102.
*/
export function getDeviceNameUnsafe(device: PairedBluetoothDeviceProperties|
null): string {
if (!device || (!device.nickname && !device.deviceProperties?.publicName)) {
return ''; return '';
} }
if (device.nickname) { return device.nickname ||
return device.nickname; mojoString16ToString(device.deviceProperties.publicName);
}
return mojoString16ToString(device.deviceProperties.publicName);
} }
/** /**

@@ -11,7 +11,7 @@ import '../settings_shared.css.js';
import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js'; import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js'; import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {getDeviceName} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js'; import {getDeviceNameUnsafe} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js';
import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js'; import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js'; import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js'; import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
@@ -52,6 +52,11 @@ export class SettingsBluetoothChangeDeviceNameDialogElement extends
value: MAX_INPUT_LENGTH, value: MAX_INPUT_LENGTH,
}, },
/**
* WARNING: This string may contain malicious HTML and should not be used
* for Polymer bindings in CSS code. For additional information see
* b/298724102.
*/
deviceName_: { deviceName_: {
type: String, type: String,
value: '', value: '',
@@ -71,7 +76,7 @@ export class SettingsBluetoothChangeDeviceNameDialogElement extends
private isInputInvalid_: boolean; private isInputInvalid_: boolean;
private onDeviceChanged_(): void { private onDeviceChanged_(): void {
this.deviceName_ = getDeviceName(this.device); this.deviceName_ = getDeviceNameUnsafe(this.device);
} }
private onCancelClick_(): void { private onCancelClick_(): void {
@@ -118,7 +123,7 @@ export class SettingsBluetoothChangeDeviceNameDialogElement extends
} }
private isDoneDisabled_(): boolean { private isDoneDisabled_(): boolean {
if (this.deviceName_ === getDeviceName(this.device)) { if (this.deviceName_ === getDeviceNameUnsafe(this.device)) {
return true; return true;
} }
@@ -128,6 +133,10 @@ export class SettingsBluetoothChangeDeviceNameDialogElement extends
return false; return false;
} }
getNameForTest(): string|null {
return getDeviceNameUnsafe(this.device);
}
} }
declare global { declare global {

@@ -104,7 +104,7 @@
aria-hidden="true"> aria-hidden="true">
$i18n{bluetoothDeviceDetailName} $i18n{bluetoothDeviceDetailName}
<div class="secondary" id="bluetoothDeviceNameLabel"> <div class="secondary" id="bluetoothDeviceNameLabel">
[[getDeviceName_(device_.*)]] [[getDeviceNameUnsafe_(device_.*)]]
</div> </div>
</div> </div>
<cr-button id="changeNameBtn" <cr-button id="changeNameBtn"

@@ -17,7 +17,7 @@ import 'chrome://resources/ash/common/bluetooth/bluetooth_device_battery_info.js
import {BluetoothUiSurface, recordBluetoothUiSurfaceMetrics} from 'chrome://resources/ash/common/bluetooth/bluetooth_metrics_utils.js'; import {BluetoothUiSurface, recordBluetoothUiSurfaceMetrics} from 'chrome://resources/ash/common/bluetooth/bluetooth_metrics_utils.js';
import {BatteryType} from 'chrome://resources/ash/common/bluetooth/bluetooth_types.js'; import {BatteryType} from 'chrome://resources/ash/common/bluetooth/bluetooth_types.js';
import {getBatteryPercentage, getDeviceName, hasAnyDetailedBatteryInfo, hasDefaultImage, hasTrueWirelessImages} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js'; import {getBatteryPercentage, getDeviceNameUnsafe, hasAnyDetailedBatteryInfo, hasDefaultImage, hasTrueWirelessImages} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js';
import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js'; import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js'; import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js'; import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
@@ -211,11 +211,8 @@ export class SettingsBluetoothDeviceDetailSubpageElement extends
this.i18n('bluetoothDeviceDetailDisconnected'); this.i18n('bluetoothDeviceDetailDisconnected');
} }
private getDeviceName_(): string { private getDeviceNameUnsafe_(): string {
if (!this.device_) { return getDeviceNameUnsafe(this.device_);
return '';
}
return getDeviceName(this.device_);
} }
private shouldShowConnectDisconnectBtn_(): boolean { private shouldShowConnectDisconnectBtn_(): boolean {
@@ -235,7 +232,7 @@ export class SettingsBluetoothDeviceDetailSubpageElement extends
return; return;
} }
(this.parentNode as OsSettingsSubpageElement).pageTitle = (this.parentNode as OsSettingsSubpageElement).pageTitle =
getDeviceName(this.device_); getDeviceNameUnsafe(this.device_);
// Special case a where user is still on detail page and has // Special case a where user is still on detail page and has
// tried to connect to device but failed. The current |pageState_| // tried to connect to device but failed. The current |pageState_|
@@ -299,9 +296,9 @@ export class SettingsBluetoothDeviceDetailSubpageElement extends
return ''; return '';
} }
return this.i18n( return loadTimeData.getStringF(
'bluetoothDeviceDetailChangeDeviceNameBtnA11yLabel', 'bluetoothDeviceDetailChangeDeviceNameBtnA11yLabel',
this.getDeviceName_()); getDeviceNameUnsafe(this.device_));
} }
private getMultipleBatteryInfoA11yLabel_(): string { private getMultipleBatteryInfoA11yLabel_(): string {
@@ -363,20 +360,22 @@ export class SettingsBluetoothDeviceDetailSubpageElement extends
switch (this.pageState_) { switch (this.pageState_) {
case PageState.CONNECTING: case PageState.CONNECTING:
return this.i18n( return loadTimeData.getStringF(
'bluetoothDeviceDetailConnectingA11yLabel', this.getDeviceName_()); 'bluetoothDeviceDetailConnectingA11yLabel',
getDeviceNameUnsafe(this.device_));
case PageState.CONNECTED: case PageState.CONNECTED:
return this.i18n( return loadTimeData.getStringF(
'bluetoothDeviceDetailConnectedA11yLabel', this.getDeviceName_()); 'bluetoothDeviceDetailConnectedA11yLabel',
getDeviceNameUnsafe(this.device_));
case PageState.CONNECTION_FAILED: case PageState.CONNECTION_FAILED:
return this.i18n( return loadTimeData.getStringF(
'bluetoothDeviceDetailConnectionFailureA11yLabel', 'bluetoothDeviceDetailConnectionFailureA11yLabel',
this.getDeviceName_()); getDeviceNameUnsafe(this.device_));
case PageState.DISCONNECTED: case PageState.DISCONNECTED:
case PageState.DISCONNECTING: case PageState.DISCONNECTING:
return this.i18n( return loadTimeData.getStringF(
'bluetoothDeviceDetailDisconnectedA11yLabel', 'bluetoothDeviceDetailDisconnectedA11yLabel',
this.getDeviceName_()); getDeviceNameUnsafe(this.device_));
default: default:
assertNotReached(); assertNotReached();
} }
@@ -525,8 +524,9 @@ export class SettingsBluetoothDeviceDetailSubpageElement extends
} }
private getForgetA11yLabel_(): string { private getForgetA11yLabel_(): string {
return this.i18n( return loadTimeData.getStringF(
'bluetoothDeviceDetailForgetA11yLabel', this.getDeviceName_()); 'bluetoothDeviceDetailForgetA11yLabel',
getDeviceNameUnsafe(this.device_));
} }
private onForgetButtonClicked_(): void { private onForgetButtonClicked_(): void {

@@ -10,7 +10,7 @@ import '../settings_shared.css.js';
import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js'; import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js'; import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {getDeviceName} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js'; import {getDeviceNameUnsafe} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js'; import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js'; import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js'; import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
@@ -45,15 +45,11 @@ class SettingsBluetoothForgetDeviceDialogElement extends
private device_: PairedBluetoothDeviceProperties; private device_: PairedBluetoothDeviceProperties;
private getForgetDeviceDialogBodyText_(): string { private getForgetDeviceDialogBodyText_(): string {
return this.i18n( return loadTimeData.getStringF(
'bluetoothDevicesDialogLabel', this.getDeviceName_(), 'bluetoothDevicesDialogLabel', getDeviceNameUnsafe(this.device_),
loadTimeData.getString('primaryUserEmail')); loadTimeData.getString('primaryUserEmail'));
} }
private getDeviceName_(): string {
return getDeviceName(this.device_);
}
private onForgetClick_(event: Event): void { private onForgetClick_(event: Event): void {
const fireEvent = new CustomEvent( const fireEvent = new CustomEvent(
'forget-bluetooth-device', {bubbles: true, composed: true}); 'forget-bluetooth-device', {bubbles: true, composed: true});

@@ -12,7 +12,7 @@ import '../settings_shared.css.js';
import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js'; import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js'; import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import {getDeviceName} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js'; import {getDeviceNameUnsafe} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js';
import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js'; import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js';
import {getInstance as getAnnouncerInstance} from 'chrome://resources/ash/common/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js'; import {getInstance as getAnnouncerInstance} from 'chrome://resources/ash/common/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js'; import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
@@ -175,31 +175,32 @@ export class SettingsBluetoothSummaryElement extends
} }
const isA11yLabel = labelType === LabelType.A11Y; const isA11yLabel = labelType === LabelType.A11Y;
const firstConnectedDeviceName = getDeviceName(connectedDevices[0]); const firstConnectedDeviceName = getDeviceNameUnsafe(connectedDevices[0]);
if (connectedDevices.length === 1) { if (connectedDevices.length === 1) {
return isA11yLabel ? this.i18n( return isA11yLabel ? loadTimeData.getStringF(
'bluetoothSummaryPageConnectedA11yOneDevice', 'bluetoothSummaryPageConnectedA11yOneDevice',
firstConnectedDeviceName) : firstConnectedDeviceName) :
firstConnectedDeviceName; firstConnectedDeviceName;
} }
if (connectedDevices.length === 2) { if (connectedDevices.length === 2) {
const secondConnectedDeviceName = getDeviceName(connectedDevices[1]); const secondConnectedDeviceName =
getDeviceNameUnsafe(connectedDevices[1]);
return isA11yLabel ? return isA11yLabel ?
this.i18n( loadTimeData.getStringF(
'bluetoothSummaryPageConnectedA11yTwoDevices', 'bluetoothSummaryPageConnectedA11yTwoDevices',
firstConnectedDeviceName, secondConnectedDeviceName) : firstConnectedDeviceName, secondConnectedDeviceName) :
this.i18n( loadTimeData.getStringF(
'bluetoothSummaryPageTwoDevicesDescription', 'bluetoothSummaryPageTwoDevicesDescription',
firstConnectedDeviceName, secondConnectedDeviceName); firstConnectedDeviceName, secondConnectedDeviceName);
} }
return isA11yLabel ? return isA11yLabel ?
this.i18n( loadTimeData.getStringF(
'bluetoothSummaryPageConnectedA11yTwoOrMoreDevices', 'bluetoothSummaryPageConnectedA11yTwoOrMoreDevices',
firstConnectedDeviceName, connectedDevices.length - 1) : firstConnectedDeviceName, connectedDevices.length - 1) :
this.i18n( loadTimeData.getStringF(
'bluetoothSummaryPageTwoOrMoreDevicesDescription', 'bluetoothSummaryPageTwoOrMoreDevicesDescription',
firstConnectedDeviceName, connectedDevices.length - 1); firstConnectedDeviceName, connectedDevices.length - 1);
} }

@@ -14,7 +14,7 @@
on-click="onSelected_"> on-click="onSelected_">
<bluetooth-icon device="[[device.deviceProperties]]"></bluetooth-icon> <bluetooth-icon device="[[device.deviceProperties]]"></bluetooth-icon>
<div class="middle" aria-hidden="true"> <div class="middle" aria-hidden="true">
<div id="deviceName">[[getDeviceName_(device)]]</div> <div id="deviceName">[[getDeviceNameUnsafe_(device)]]</div>
<template is="dom-if" <template is="dom-if"
if="[[shouldShowBatteryInfo_(device)]]" restamp> if="[[shouldShowBatteryInfo_(device)]]" restamp>
<bluetooth-device-battery-info <bluetooth-device-battery-info

@@ -16,10 +16,11 @@ import 'chrome://resources/ash/common/bluetooth/bluetooth_icon.js';
import 'chrome://resources/ash/common/bluetooth/bluetooth_device_battery_info.js'; import 'chrome://resources/ash/common/bluetooth/bluetooth_device_battery_info.js';
import {BatteryType} from 'chrome://resources/ash/common/bluetooth/bluetooth_types.js'; import {BatteryType} from 'chrome://resources/ash/common/bluetooth/bluetooth_types.js';
import {getBatteryPercentage, getDeviceName, hasAnyDetailedBatteryInfo} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js'; import {getBatteryPercentage, getDeviceNameUnsafe, hasAnyDetailedBatteryInfo} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js';
import {FocusRowMixin} from 'chrome://resources/ash/common/cr_elements/focus_row_mixin.js'; import {FocusRowMixin} from 'chrome://resources/ash/common/cr_elements/focus_row_mixin.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js'; import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js'; import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {DeviceConnectionState, DeviceType, PairedBluetoothDeviceProperties} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js'; import {DeviceConnectionState, DeviceType, PairedBluetoothDeviceProperties} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
@@ -101,8 +102,9 @@ export class SettingsPairedBluetoothListItemElement extends
Router.getInstance().navigateTo(routes.BLUETOOTH_DEVICE_DETAIL, params); Router.getInstance().navigateTo(routes.BLUETOOTH_DEVICE_DETAIL, params);
} }
private getDeviceName_(device: PairedBluetoothDeviceProperties): string { private getDeviceNameUnsafe_(device: PairedBluetoothDeviceProperties):
return getDeviceName(device); string {
return getDeviceNameUnsafe(device);
} }
private shouldShowBatteryInfo_(device: PairedBluetoothDeviceProperties): private shouldShowBatteryInfo_(device: PairedBluetoothDeviceProperties):
@@ -159,9 +161,9 @@ export class SettingsPairedBluetoothListItemElement extends
private getAriaLabel_(device: PairedBluetoothDeviceProperties): string { private getAriaLabel_(device: PairedBluetoothDeviceProperties): string {
// Start with the base information of the device name and location within // Start with the base information of the device name and location within
// the list of devices with the same connection state. // the list of devices with the same connection state.
let a11yLabel = this.i18n( let a11yLabel = loadTimeData.getStringF(
'bluetoothA11yDeviceName', this.itemIndex + 1, this.listSize, 'bluetoothA11yDeviceName', this.itemIndex + 1, this.listSize,
this.getDeviceName_(device)); this.getDeviceNameUnsafe_(device));
// Include the connection status. // Include the connection status.
a11yLabel += a11yLabel +=

@@ -28,7 +28,7 @@
</template> </template>
</div> </div>
<div class="middle" aria-hidden="true"> <div class="middle" aria-hidden="true">
<div id="deviceName">[[getDeviceName_(device)]]</div> <div id="deviceName">[[getDeviceNameUnsafe_(device)]]</div>
</div> </div>
<div> <div>
<cr-icon-button class="icon-more-vert" <cr-icon-button class="icon-more-vert"

@@ -16,8 +16,8 @@ import 'chrome://resources/ash/common/cr_elements/cr_action_menu/cr_action_menu.
import {FastPairSavedDevicesUiEvent, recordSavedDevicesUiEventMetrics} from 'chrome://resources/ash/common/bluetooth/bluetooth_metrics_utils.js'; import {FastPairSavedDevicesUiEvent, recordSavedDevicesUiEventMetrics} from 'chrome://resources/ash/common/bluetooth/bluetooth_metrics_utils.js';
import {CrActionMenuElement} from 'chrome://resources/ash/common/cr_elements/cr_action_menu/cr_action_menu.js'; import {CrActionMenuElement} from 'chrome://resources/ash/common/cr_elements/cr_action_menu/cr_action_menu.js';
import {FocusRowMixin} from 'chrome://resources/ash/common/cr_elements/focus_row_mixin.js'; import {FocusRowMixin} from 'chrome://resources/ash/common/cr_elements/focus_row_mixin.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js'; import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './os_saved_devices_list_item.html.js'; import {getTemplate} from './os_saved_devices_list_item.html.js';
@@ -30,7 +30,7 @@ interface SettingsSavedDevicesListItemElement {
} }
const SettingsSavedDevicesListItemElementBase = const SettingsSavedDevicesListItemElementBase =
FocusRowMixin(WebUiListenerMixin(I18nMixin(PolymerElement))); FocusRowMixin(WebUiListenerMixin(PolymerElement));
class SettingsSavedDevicesListItemElement extends class SettingsSavedDevicesListItemElement extends
SettingsSavedDevicesListItemElementBase { SettingsSavedDevicesListItemElementBase {
@@ -69,7 +69,7 @@ class SettingsSavedDevicesListItemElement extends
listSize: number; listSize: number;
private shouldShowRemoveSavedDeviceDialog_: boolean; private shouldShowRemoveSavedDeviceDialog_: boolean;
private getDeviceName_(device: FastPairSavedDevice): string { private getDeviceNameUnsafe_(device: FastPairSavedDevice): string {
return device.name; return device.name;
} }
@@ -95,15 +95,16 @@ class SettingsSavedDevicesListItemElement extends
} }
private getAriaLabel_(device: FastPairSavedDevice): string { private getAriaLabel_(device: FastPairSavedDevice): string {
const deviceName = this.getDeviceName_(device); const deviceName = this.getDeviceNameUnsafe_(device);
return this.i18n( return loadTimeData.getStringF(
'savedDeviceItemA11yLabel', this.itemIndex + 1, this.listSize, 'savedDeviceItemA11yLabel', this.itemIndex + 1, this.listSize,
deviceName); deviceName);
} }
private getSubpageButtonA11yLabel_(device: FastPairSavedDevice): string { private getSubpageButtonA11yLabel_(device: FastPairSavedDevice): string {
const deviceName = this.getDeviceName_(device); const deviceName = this.getDeviceNameUnsafe_(device);
return this.i18n('savedDeviceItemButtonA11yLabel', deviceName); return loadTimeData.getStringF(
'savedDeviceItemButtonA11yLabel', deviceName);
} }
} }

@@ -15,13 +15,13 @@ import '../settings_shared.css.js';
import '../os_settings_icons.html.js'; import '../os_settings_icons.html.js';
import './menu_item.js'; import './menu_item.js';
import {getDeviceName} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js'; import {getDeviceNameUnsafe} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js';
import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js'; import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js';
import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin, WebUiListenerMixinInterface} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {MojoInterfaceProviderImpl} from 'chrome://resources/ash/common/network/mojo_interface_provider.js'; import {MojoInterfaceProviderImpl} from 'chrome://resources/ash/common/network/mojo_interface_provider.js';
import {NetworkListenerBehavior, NetworkListenerBehaviorInterface} from 'chrome://resources/ash/common/network/network_listener_behavior.js'; import {NetworkListenerBehavior, NetworkListenerBehaviorInterface} from 'chrome://resources/ash/common/network/network_listener_behavior.js';
import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js'; import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js';
import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin, WebUiListenerMixinInterface} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {BluetoothSystemProperties, BluetoothSystemState, DeviceConnectionState, PairedBluetoothDeviceProperties, SystemPropertiesObserverReceiver as BluetoothPropertiesObserverReceiver} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js'; import {BluetoothSystemProperties, BluetoothSystemState, DeviceConnectionState, PairedBluetoothDeviceProperties, SystemPropertiesObserverReceiver as BluetoothPropertiesObserverReceiver} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js';
import {CrosNetworkConfigInterface, FilterType, NO_LIMIT} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js'; import {CrosNetworkConfigInterface, FilterType, NO_LIMIT} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {NetworkType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js'; import {NetworkType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
@@ -664,7 +664,7 @@ export class OsSettingsMenuElement extends OsSettingsMenuElementBase {
if (connectedDevices.length === 1) { if (connectedDevices.length === 1) {
const device = castExists(connectedDevices[0]); const device = castExists(connectedDevices[0]);
this.bluetoothMenuItemDescription_ = getDeviceName(device); this.bluetoothMenuItemDescription_ = getDeviceNameUnsafe(device);
return; return;
} }

@@ -6,7 +6,7 @@ import 'chrome://os-settings/lazy_load.js';
import {SettingsBluetoothChangeDeviceNameDialogElement} from 'chrome://os-settings/lazy_load.js'; import {SettingsBluetoothChangeDeviceNameDialogElement} from 'chrome://os-settings/lazy_load.js';
import {CrInputElement} from 'chrome://os-settings/os_settings.js'; import {CrInputElement} from 'chrome://os-settings/os_settings.js';
import {getDeviceName} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js'; import {getDeviceNameUnsafe} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js';
import {setBluetoothConfigForTesting} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js'; import {setBluetoothConfigForTesting} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js';
import {DeviceConnectionState} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js'; import {DeviceConnectionState} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
@@ -18,6 +18,7 @@ suite('<os-settings-bluetooth-change-device-name-dialog>', () => {
let bluetoothDeviceChangeNameDialog: let bluetoothDeviceChangeNameDialog:
SettingsBluetoothChangeDeviceNameDialogElement; SettingsBluetoothChangeDeviceNameDialogElement;
let bluetoothConfig: FakeBluetoothConfig; let bluetoothConfig: FakeBluetoothConfig;
const deviceId = '12//345&6789';
setup(() => { setup(() => {
bluetoothConfig = new FakeBluetoothConfig(); bluetoothConfig = new FakeBluetoothConfig();
@@ -61,7 +62,7 @@ suite('<os-settings-bluetooth-change-device-name-dialog>', () => {
test('Input is sanitized', async () => { test('Input is sanitized', async () => {
const device1 = createDefaultBluetoothDevice( const device1 = createDefaultBluetoothDevice(
/*id=*/ '12//345&6789', /*id=*/ deviceId,
/*publicName=*/ 'BeatsX', /*publicName=*/ 'BeatsX',
/*connectionState=*/ /*connectionState=*/
DeviceConnectionState.kConnected, DeviceConnectionState.kConnected,
@@ -109,8 +110,10 @@ suite('<os-settings-bluetooth-change-device-name-dialog>', () => {
}); });
test('Device name is changed', async () => { test('Device name is changed', async () => {
const id = '12//345&6789'; const initialNickname = 'device1';
const nickname = 'Nickname'; const newNickname = 'nickname';
const htmlNickname = '<a>html</a>';
const getDoneBtn = () => { const getDoneBtn = () => {
const doneButton = bluetoothDeviceChangeNameDialog.shadowRoot! const doneButton = bluetoothDeviceChangeNameDialog.shadowRoot!
.querySelector<HTMLButtonElement>('#done'); .querySelector<HTMLButtonElement>('#done');
@@ -119,11 +122,11 @@ suite('<os-settings-bluetooth-change-device-name-dialog>', () => {
}; };
const device = createDefaultBluetoothDevice( const device = createDefaultBluetoothDevice(
id, deviceId,
/*publicName=*/ 'BeatsX', /*publicName=*/ 'BeatsX',
/*connectionState=*/ /*connectionState=*/
DeviceConnectionState.kConnected, DeviceConnectionState.kConnected,
/*opt_nickname=*/ 'device1'); /*opt_nickname=*/ initialNickname);
bluetoothDeviceChangeNameDialog.set('device', {...device}); bluetoothDeviceChangeNameDialog.set('device', {...device});
bluetoothConfig.appendToPairedDeviceList([device]); bluetoothConfig.appendToPairedDeviceList([device]);
@@ -132,18 +135,27 @@ suite('<os-settings-bluetooth-change-device-name-dialog>', () => {
const input = bluetoothDeviceChangeNameDialog.shadowRoot! const input = bluetoothDeviceChangeNameDialog.shadowRoot!
.querySelector<CrInputElement>('#changeNameInput'); .querySelector<CrInputElement>('#changeNameInput');
assertTrue(!!input); assertTrue(!!input);
assertEquals('device1', input.value); assertEquals(initialNickname, input.value);
assertTrue(getDoneBtn().disabled); assertTrue(getDoneBtn().disabled);
input.value = nickname; input.value = newNickname;
await flushTasks(); await flushTasks();
assertFalse(getDoneBtn().disabled); assertFalse(getDoneBtn().disabled);
getDoneBtn().click(); getDoneBtn().click();
await flushTasks(); await flushTasks();
assertEquals(
newNickname,
getDeviceNameUnsafe(bluetoothConfig.getPairedDeviceById(deviceId)));
const newName = getDeviceName(bluetoothConfig.getPairedDeviceById(id)); input.value = htmlNickname;
await flushTasks();
assertFalse(getDoneBtn().disabled);
assertEquals(nickname, newName); getDoneBtn().click();
await flushTasks();
assertEquals(
htmlNickname,
getDeviceNameUnsafe(bluetoothConfig.getPairedDeviceById(deviceId)));
}); });
}); });

@@ -195,6 +195,32 @@ suite('<os-settings-paired-bluetooth-list-item>', () => {
getItemA11yLabel()); getItemA11yLabel());
}); });
test('Device name is set correctly', async () => {
const getDeviceName = () => {
const deviceName =
pairedBluetoothListItem.shadowRoot!.querySelector<HTMLElement>(
'#deviceName');
assertTrue(!!deviceName);
return deviceName;
};
const publicName = 'BeatsX';
const nameWithHtml = '<a>testname</a>';
const device = createDefaultBluetoothDevice(
/*id=*/ '123456789', /*publicName=*/ publicName,
/*connectionState=*/
DeviceConnectionState.kConnected);
pairedBluetoothListItem.set('device', device);
await flushTasks();
assertEquals(publicName, getDeviceName().innerText);
device.nickname = nameWithHtml;
pairedBluetoothListItem.set('device', {...device});
await flushTasks();
assertTrue(!!getDeviceName());
assertEquals(nameWithHtml, getDeviceName().innerText);
});
test('Battery percentage out of bounds', async () => { test('Battery percentage out of bounds', async () => {
const device = createDefaultBluetoothDevice( const device = createDefaultBluetoothDevice(
/*id=*/ '123456789', /*publicName=*/ 'BeatsX', /*id=*/ '123456789', /*publicName=*/ 'BeatsX',

@@ -177,4 +177,23 @@ suite('<os-settings-saved-devices-list>', () => {
assertTrue(isVisible( assertTrue(isVisible(
getListItems()[1]!.shadowRoot!.querySelector('#deviceImage'))); getListItems()[1]!.shadowRoot!.querySelector('#deviceImage')));
}); });
test('Device names are set properly', async () => {
const getDeviceName = () => {
return getListItems()[0]!.shadowRoot!
.querySelector<HTMLElement>('#deviceName')!.innerText;
};
const deviceName = 'deviceName';
const device = {name: deviceName, imageUrl: 'fakeUrl', accountKey: '1'};
savedDevicesList.set('devices', [device]);
await flushTasks();
assertEquals(deviceName, getDeviceName());
const nameWithHtml = '<a>test</a>';
device.name = nameWithHtml;
savedDevicesList.set('devices', [{...device}]);
await flushTasks();
assertEquals(nameWithHtml, getDeviceName());
});
}); });