Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept . as a query string delimiter and reject non-alphanumeric card_id parameters #1265

Merged
merged 1 commit into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

```
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>

Expand All @@ -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>

Expand Down Expand Up @@ -3859,15 +3859,15 @@ elements:
left: 30%
tap_action:
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
icon: mdi:cctv
style:
top: 71%
left: 42%
tap_action:
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>
Expand Down Expand Up @@ -3998,13 +3998,13 @@ The Frigate card will execute these actions in the following circumstances:
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:

```
[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 |
Expand All @@ -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. |
| `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
different 'tabs' within that dashboard, they will all respond to the actions
unless the action is targeted with a `CARD_ID` as shown above.
Expand Down
6 changes: 4 additions & 2 deletions src/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ class FrigateCard extends LitElement {
// the default view and the querystring view, see:
// https://github.com/dermotduffy/frigate-hass-card/issues/1200
if (!this._view) {
const querystringActions = getActionsFromQueryString();
const querystringActions = getActionsFromQueryString(window.location.search);
if (
!querystringActions.find(
(action) =>
Expand Down Expand Up @@ -1594,7 +1594,9 @@ class FrigateCard extends LitElement {
protected _locationChangeHandler = (): void => {
// Only execute actions when the card has rendered at least once.
if (this.hasUpdated) {
getActionsFromQueryString().forEach((action) => this._cardActionHandler(action));
getActionsFromQueryString(window.location.search).forEach((action) =>
this._cardActionHandler(action),
);
}
};

Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
.or(z.literal('fire-dom-event')),

// 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 = [
Expand Down
11 changes: 7 additions & 4 deletions src/utils/querystring.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { FrigateCardCustomAction } from '../types';
import { createFrigateCardCustomAction } from './action.js';

export const getActionsFromQueryString = (): FrigateCardCustomAction[] => {
const params = new URLSearchParams(window.location.search);
export const getActionsFromQueryString = (
queryString: string,
): FrigateCardCustomAction[] => {
const params = new URLSearchParams(queryString);
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()) {
const match = key.match(actionRE);
if (!match || !match.groups) {
Expand Down
102 changes: 102 additions & 0 deletions tests/utils/querystring.test.ts
Original file line number Diff line number Diff line change
@@ -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',
);
});
});