Merge pull request #1265 from dermotduffy/qs-delimiter

Accept `.` as a query string delimiter and reject non-alphanumeric `card_id` parameters
This commit is contained in:
Dermot Duffy
2023-09-06 21:27:28 -07:00
committed by GitHub
5 changed files with 125 additions and 13 deletions
+8 -6
View File
@@ -3807,7 +3807,7 @@ The card can respond to actions in the query string (see [below](#query-string-a
This example assumes the dashboard URL is `https://ha.mydomain.org/lovelace-test/0`. This example assumes the dashboard URL is `https://ha.mydomain.org/lovelace-test/0`.
``` ```
https://ha.mydomain.org/lovelace-test/0?frigate-card-action:camera_select=kitchen&frigate-card-action:expand https://ha.mydomain.org/lovelace-test/0?frigate-card-action.camera_select=kitchen&frigate-card-action.expand
``` ```
</details> </details>
@@ -3826,7 +3826,7 @@ cameras:
``` ```
``` ```
https://ha.mydomain.org/lovelace-test/0?frigate-card-action:main:clips https://ha.mydomain.org/lovelace-test/0?frigate-card-action.main.clips
``` ```
</details> </details>
@@ -3859,7 +3859,7 @@ elements:
left: 30% left: 30%
tap_action: tap_action:
action: navigate action: navigate
navigation_path: /lovelace-frigate/map?frigate-card-action:camera_select=camera.living_room navigation_path: /lovelace-frigate/map?frigate-card-action.camera_select=camera.living_room
- type: icon - type: icon
icon: mdi:cctv icon: mdi:cctv
style: style:
@@ -3867,7 +3867,7 @@ elements:
left: 42% left: 42%
tap_action: tap_action:
action: navigate action: navigate
navigation_path: /lovelace-frigate/map?frigate-card-action:camera_select=camera.landing navigation_path: /lovelace-frigate/map?frigate-card-action.camera_select=camera.landing
``` ```
</details> </details>
@@ -3998,13 +3998,13 @@ The Frigate card will execute these actions in the following circumstances:
To send an action to *all* Frigate cards: To send an action to *all* Frigate cards:
``` ```
[PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action:[ACTION]=[VALUE] [PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action.[ACTION]=[VALUE]
``` ```
To send an action to a named Frigate card: To send an action to a named Frigate card:
``` ```
[PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action:[CARD_ID]:[ACTION]=[VALUE] [PATH_TO_YOUR_HA_DASHBOARD]?frigate-card-action.[CARD_ID].[ACTION]=[VALUE]
``` ```
| Parameter | Description | | Parameter | Description |
@@ -4013,6 +4013,8 @@ To send an action to a named Frigate card:
| `CARD_ID` | When specified only cards that have a `card_id` parameter will act. | | `CARD_ID` | When specified only cards that have a `card_id` parameter will act. |
| `VALUE` | An optional value to use with the `camera_select` and `live_substream_select` actions. | | `VALUE` | An optional value to use with the `camera_select` and `live_substream_select` actions. |
**Note**: Both `.` and `:` may be used as the delimiter. If you use `:` some browsers may require it be escaped to `%3A`.
**Note**: If a dashboard has multiple Frigate cards on it, even if they are on **Note**: If a dashboard has multiple Frigate cards on it, even if they are on
different 'tabs' within that dashboard, they will all respond to the actions different 'tabs' within that dashboard, they will all respond to the actions
unless the action is targeted with a `CARD_ID` as shown above. unless the action is targeted with a `CARD_ID` as shown above.
+4 -2
View File
@@ -749,7 +749,7 @@ class FrigateCard extends LitElement {
// the default view and the querystring view, see: // the default view and the querystring view, see:
// https://github.com/dermotduffy/frigate-hass-card/issues/1200 // https://github.com/dermotduffy/frigate-hass-card/issues/1200
if (!this._view) { if (!this._view) {
const querystringActions = getActionsFromQueryString(); const querystringActions = getActionsFromQueryString(window.location.search);
if ( if (
!querystringActions.find( !querystringActions.find(
(action) => (action) =>
@@ -1594,7 +1594,9 @@ class FrigateCard extends LitElement {
protected _locationChangeHandler = (): void => { protected _locationChangeHandler = (): void => {
// Only execute actions when the card has rendered at least once. // Only execute actions when the card has rendered at least once.
if (this.hasUpdated) { if (this.hasUpdated) {
getActionsFromQueryString().forEach((action) => this._cardActionHandler(action)); getActionsFromQueryString(window.location.search).forEach((action) =>
this._cardActionHandler(action),
);
} }
}; };
+4 -1
View File
@@ -217,7 +217,10 @@ const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
.or(z.literal('fire-dom-event')), .or(z.literal('fire-dom-event')),
// Card this command is intended for. // Card this command is intended for.
card_id: z.string().optional(), card_id: z
.string()
.regex(/^\w+$/, 'card_id parameter can only contain [a-z][A-Z][0-9_]')
.optional(),
}); });
const FRIGATE_CARD_GENERAL_ACTIONS = [ const FRIGATE_CARD_GENERAL_ACTIONS = [
+7 -4
View File
@@ -1,11 +1,14 @@
import { FrigateCardCustomAction } from '../types'; import { FrigateCardCustomAction } from '../types';
import { createFrigateCardCustomAction } from './action.js'; import { createFrigateCardCustomAction } from './action.js';
export const getActionsFromQueryString = (): FrigateCardCustomAction[] => { export const getActionsFromQueryString = (
const params = new URLSearchParams(window.location.search); queryString: string,
): FrigateCardCustomAction[] => {
const params = new URLSearchParams(queryString);
const actions: FrigateCardCustomAction[] = []; const actions: FrigateCardCustomAction[] = [];
const actionRE = new RegExp(/^frigate-card-action(:(?<cardID>\w+))?:(?<action>\w+)/); const actionRE = new RegExp(
/^frigate-card-action([.:](?<cardID>\w+))?[.:](?<action>\w+)/,
);
for (const [key, value] of params.entries()) { for (const [key, value] of params.entries()) {
const match = key.match(actionRE); const match = key.match(actionRE);
if (!match || !match.groups) { if (!match || !match.groups) {
+102
View File
@@ -0,0 +1,102 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { getActionsFromQueryString } from '../../src/utils/querystring';
// @vitest-environment jsdom
describe('getActionsFromQueryString', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should reject malformed query string', () => {
expect(getActionsFromQueryString(`?BOGUS_KEY=BOGUS_VALUE`)).toEqual([]);
});
it('should accept colon as delimiter', () => {
expect(getActionsFromQueryString(`?frigate-card-action:id:clips=`)).toEqual([
{
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: 'clips',
},
]);
});
describe('should get simple action from query string', () => {
it.each([
['camera_ui' as const],
['clip' as const],
['clips' as const],
['default' as const],
['diagnostics' as const],
['download' as const],
['expand' as const],
['image' as const],
['live' as const],
['menu_toggle' as const],
['recording' as const],
['recordings' as const],
['snapshot' as const],
['snapshots' as const],
['timeline' as const],
])('%s', (action: string) => {
expect(getActionsFromQueryString(`?frigate-card-action.id.${action}=`)).toEqual([
{
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: action,
},
]);
});
});
it('should get camera_select action', () => {
expect(
getActionsFromQueryString(`?frigate-card-action.id.camera_select=camera.foo`),
).toEqual([
{
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: 'camera_select',
camera: 'camera.foo',
},
]);
});
it('should get live_substream_select action', () => {
expect(
getActionsFromQueryString(
`?frigate-card-action.id.live_substream_select=camera.bar`,
),
).toEqual([
{
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: 'live_substream_select',
camera: 'camera.bar',
},
]);
});
describe('should reject value-based actions without value', () => {
it.each([['camera_select' as const], ['live_substream_select' as const]])(
'%s',
(action: string) => {
expect(getActionsFromQueryString(`?frigate-card-action.id.${action}=`)).toEqual(
[],
);
},
);
});
it('should log unknown but correctly formed action', () => {
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
expect(
getActionsFromQueryString(`?frigate-card-action.id.not_a_real_action}=`),
).toEqual([]);
expect(spy).toBeCalledWith(
'Frigate card received unknown card action in query string: not_a_real_action',
);
});
});