Skip to content
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
1 change: 1 addition & 0 deletions public/assets/locale/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"SETTINGS_ENCRYPTED": "مشفر",
"SETTINGS_MERGED": "تم الدمج",
"SETTINGS_MASKED": "تم إخفاؤه",
"SETTINGS_MASKED_ERROR": "استبدل جميع القيم المخفية قبل حفظ الإعدادات المشفرة",
"SETTINGS_INHERITED": "إعداد موروث من [{{ parent }}]({{ path }})({{ type }})",
"SETTINGS_LOCAL": "إعداد محلي من {{ type }}",
"SETTINGS_SAVE_ERROR": "فشل حفظ الإعدادات. الخطأ {{ error }}",
Expand Down
1 change: 1 addition & 0 deletions public/assets/locale/en-AU.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"SETTINGS_ENCRYPTED": "Encrypted",
"SETTINGS_MERGED": "Merged",
"SETTINGS_MASKED": "MASKED",
"SETTINGS_MASKED_ERROR": "Replace all masked values before you save encrypted settings",
"SETTINGS_INHERITED": "Setting inherited from [{{ parent }}]({{ path }})({{ type }})",
"SETTINGS_LOCAL": "Local setting from {{ type }}",
"SETTINGS_SAVE_ERROR": "Failed to save settings. Error {{ error }}",
Expand Down
1 change: 1 addition & 0 deletions public/assets/locale/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"SETTINGS_ENCRYPTED": "Encrypted",
"SETTINGS_MERGED": "Merged",
"SETTINGS_MASKED": "MASKED",
"SETTINGS_MASKED_ERROR": "Replace all masked values before you save encrypted settings",
"SETTINGS_INHERITED": "Setting inherited from [{{ parent }}]({{ path }})({{ type }})",
"SETTINGS_LOCAL": "Local setting from {{ type }}",
"SETTINGS_SAVE_ERROR": "Failed to save settings. Error {{ error }}",
Expand Down
1 change: 1 addition & 0 deletions public/assets/locale/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"SETTINGS_ENCRYPTED": "Encrypted",
"SETTINGS_MERGED": "Merged",
"SETTINGS_MASKED": "MASKED",
"SETTINGS_MASKED_ERROR": "Replace all masked values before you save encrypted settings",
"SETTINGS_INHERITED": "Setting inherited from [{{ parent }}]({{ path }})({{ type }})",
"SETTINGS_LOCAL": "Local setting from {{ type }}",
"SETTINGS_SAVE_ERROR": "Failed to save settings. Error {{ error }}",
Expand Down
1 change: 1 addition & 0 deletions public/assets/locale/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"SETTINGS_ENCRYPTED": "Encriptado",
"SETTINGS_MERGED": "Combinado",
"SETTINGS_MASKED": "OCULTO",
"SETTINGS_MASKED_ERROR": "Reemplace todos los valores ocultos antes de guardar la configuración cifrada",
"SETTINGS_INHERITED": "Configuración heredada de [{{ parent }}]({{ path }})({{ type }})",
"SETTINGS_LOCAL": "Configuración local de {{ type }}",
"SETTINGS_SAVE_ERROR": "No se pudo guardar las configuraciones. Error {{ error }}",
Expand Down
1 change: 1 addition & 0 deletions public/assets/locale/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"SETTINGS_ENCRYPTED": "Chiffré",
"SETTINGS_MERGED": "Fusionné",
"SETTINGS_MASKED": "MASQUÉ",
"SETTINGS_MASKED_ERROR": "Remplacez toutes les valeurs masquées avant d'enregistrer les paramètres chiffrés",
"SETTINGS_INHERITED": "Paramètre hérité de [{{ parent }}]({{ path }})({{ type }})",
"SETTINGS_LOCAL": "Paramètre local depuis {{ type }}",
"SETTINGS_SAVE_ERROR": "Échec de l'enregistrement des paramètres. Erreur {{ error }}",
Expand Down
1 change: 1 addition & 0 deletions public/assets/locale/jp.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"SETTINGS_ENCRYPTED": "暗号化済み",
"SETTINGS_MERGED": "マージ済み",
"SETTINGS_MASKED": "マスク済み",
"SETTINGS_MASKED_ERROR": "暗号化された設定を保存する前に、マスクされた値をすべて置き換えてください",
"SETTINGS_INHERITED": "この設定は[{{ parent }}]({{ path }})({{ type }})から継承されています",
"SETTINGS_LOCAL": "{{ type }}のローカル設定",
"SETTINGS_SAVE_ERROR": "設定の保存に失敗しました。エラー:{{ error }}",
Expand Down
5 changes: 3 additions & 2 deletions src/app/admin/build-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { del, get } from '@placeos/ts-client';
import { toQueryString } from '../common/api';
import { escapeHtml } from '../common/general';
import { i18n } from '../common/locale.service';
import { notifyError, notifySuccess } from '../common/notifications';
import { openConfirmModal } from '../overlays/confirm-modal.component';
Expand Down Expand Up @@ -159,8 +160,8 @@ export class PlaceBuildListComponent implements OnInit {
{
title: i18n('ADMIN.BUILD_LIST_REMOVE'),
content: i18n('ADMIN.BUILD_LIST_REMOVE_MSG', {
driver: i.driver,
repo: i.repo,
driver: escapeHtml(i.driver),
repo: escapeHtml(i.repo),
}),
icon: { type: 'icon', content: 'delete' },
},
Expand Down
4 changes: 2 additions & 2 deletions src/app/admin/edge.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
removeEdge,
retrieveEdgeToken,
} from '@placeos/ts-client';
import { copyToClipboard } from '../common/general';
import { copyToClipboard, escapeHtml } from '../common/general';
import {
notifyError,
notifyInfo,
Expand Down Expand Up @@ -228,7 +228,7 @@ export class PlaceEdgeComponent implements OnInit {
const details = await openConfirmModal(
{
title: 'Remove edge?',
content: `Remove <strong>${i.name}</strong>?<br>You or your users may lose access to some data.`,
content: `Remove <strong>${escapeHtml(i.name)}</strong>?<br>You or your users may lose access to some data.`,
icon: { type: 'icon', content: 'delete' },
},
this._dialog,
Expand Down
10 changes: 3 additions & 7 deletions src/app/admin/extensions.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { PlaceDomain, updateDomain } from '@placeos/ts-client';
import { escapeHtml } from '../common/general';
import { notifyError } from '../common/notifications';
import { waitForEvent } from '../common/signals';
import { ApplicationIcon, DialogEvent } from '../common/types';
Expand All @@ -15,7 +16,6 @@ import {
ConfirmModalData,
} from '../overlays/confirm-modal.component';
import { IconComponent } from '../ui/icon.component';
import { SafePipe } from '../ui/pipes/safe.pipe';
import { SimpleTableComponent } from '../ui/simple-table.component';
import { TranslatePipe } from '../ui/translate.pipe';
import { AdminDataService } from './admin-data.service';
Expand Down Expand Up @@ -132,10 +132,7 @@ export interface BackofficeExtension {
</div>
</ng-template>
<ng-template #url_template let-row="row">
<a
class="truncate p-4 underline"
[href]="row.url | safe: 'url'"
>
<a class="truncate p-4 underline" [href]="row.url">
{{ row.url }}
</a>
</ng-template>
Expand Down Expand Up @@ -188,7 +185,6 @@ export interface BackofficeExtension {
MatFormFieldModule,
MatSelectModule,
FormsModule,
SafePipe,
],
})
export class PlaceExtensionsComponent implements OnInit {
Expand Down Expand Up @@ -256,7 +252,7 @@ export class PlaceExtensionsComponent implements OnInit {
{
data: {
title: 'Remove extension',
content: `Are you sure you want to remove the extension "${item.name}" from ${item.type}?`,
content: `Are you sure you want to remove the extension "${escapeHtml(item.name)}" from ${escapeHtml(item.type)}?`,
icon: { content: 'delete' },
},
},
Expand Down
3 changes: 2 additions & 1 deletion src/app/admin/resource-imports.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { addSystem, query, querySystemsWithEmails } from '@placeos/ts-client';
import { escapeHtml } from '../common/general';
import { i18n } from '../common/locale.service';
import { notifySuccess, notifyWarn } from '../common/notifications';
import { openConfirmModal } from '../overlays/confirm-modal.component';
Expand Down Expand Up @@ -215,7 +216,7 @@ export class ResourceImportsComponent implements OnInit {
count: missing.length,
})}</p>
<ul class="list-disc ml-4 text-left px-8 text-sm">${missing
.map((_) => `<li>${_.display_name}</li>`)
.map((_) => `<li>${escapeHtml(_.display_name)}</li>`)
.join('')}</ul>
`,
icon: { type: 'icon', content: 'publish' },
Expand Down
5 changes: 3 additions & 2 deletions src/app/admin/signage-ai/signage-ai.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MatRippleModule } from '@angular/material/core';
import { MatDialog } from '@angular/material/dialog';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { escapeHtml } from '../../common/general';
import { i18n } from '../../common/locale.service';
import { notifyError, notifySuccess } from '../../common/notifications';
import { openConfirmModal } from '../../overlays/confirm-modal.component';
Expand Down Expand Up @@ -284,7 +285,7 @@ export class SignageAIComponent implements OnInit {
{
title: i18n('ADMIN.AI_PROVIDER_TEST_TITLE'),
content: i18n('ADMIN.AI_PROVIDER_TEST_MSG', {
name: item.name,
name: escapeHtml(item.name),
}),
icon: { content: 'bolt' },
},
Expand Down Expand Up @@ -314,7 +315,7 @@ export class SignageAIComponent implements OnInit {
{
title: i18n('ADMIN.AI_PROVIDER_REMOVE_TITLE'),
content: i18n('ADMIN.AI_PROVIDER_REMOVE_MSG', {
name: item.name,
name: escapeHtml(item.name),
}),
icon: { content: 'delete_forever' },
},
Expand Down
27 changes: 14 additions & 13 deletions src/app/admin/signage-plugins/signage-plugin-embed.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ import { SafePipe } from '../../ui/pipes/safe.pipe';

export const SIGNAGE_PLUGIN_API_VERSION = 'signage-plugin/v1';

/** Resolve a plugin URI the same way as the iframe element. */
/**
* Resolve a plugin URI the same way as the iframe element.
* Returns `null` for invalid URIs and for schemes other than http(s).
*/
export function resolveSignagePluginUrl(
uri: string,
base_uri: string,
): URL | null {
if (!uri) return null;
try {
return new URL(uri, base_uri);
const url = new URL(uri, base_uri);
return ['http:', 'https:'].includes(url.protocol) ? url : null;
} catch {
return null;
}
Expand Down Expand Up @@ -87,7 +91,7 @@ export type PluginErrorPayload = {
@if (plugin_url(); as plugin_url) {
<iframe
#plugin_el
sandbox="allow-scripts allow-same-origin"
sandbox="allow-scripts"
referrerpolicy="no-referrer"
[src]="plugin_url.href | safe: 'resource'"
>
Expand Down Expand Up @@ -118,10 +122,6 @@ export class SignagePluginEmbedComponent
public readonly plugin_url = computed(() =>
resolveSignagePluginUrl(this.plugin()?.uri, this._document.baseURI),
);
public readonly plugin_origin = computed(
() => this.plugin_url()?.origin || '',
);

private _handle_messages = (e) => this._handleMessage(e);

public ngOnInit() {
Expand All @@ -137,9 +137,10 @@ export class SignagePluginEmbedComponent
type: SignageHostMessageType,
payload: PluginConfigPayload | null = null,
) {
this._plugin_el()?.nativeElement?.contentWindow.postMessage(
// The sandboxed frame has an opaque origin, so no origin can be named
this._plugin_el()?.nativeElement?.contentWindow?.postMessage(
{ api: SIGNAGE_PLUGIN_API_VERSION, type, payload },
this.plugin_origin(),
'*',
);
}

Expand All @@ -151,10 +152,10 @@ export class SignagePluginEmbedComponent
window.addEventListener('message', this._handle_messages);
}

private _handleMessage(event) {
if (event.origin !== this.plugin_origin()) return;
if (event.source !== this._plugin_el()?.nativeElement?.contentWindow)
return;
private _handleMessage(event: MessageEvent) {
// Origin is `'null'` for sandboxed frames, so match the window
const frame_window = this._plugin_el()?.nativeElement?.contentWindow;
if (!frame_window || event.source !== frame_window) return;

const msg = event.data;
if (
Expand Down
3 changes: 2 additions & 1 deletion src/app/admin/staff-api.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { del, get, PlaceDomain } from '@placeos/ts-client';
import { addDays, getUnixTime, startOfDay } from 'date-fns';
import { escapeHtml } from '../common/general';
import { notifyError, notifySuccess } from '../common/notifications';
import { HashMap } from '../common/types';
import { openConfirmModal } from '../overlays/confirm-modal.component';
Expand Down Expand Up @@ -247,7 +248,7 @@ export class PlaceStaffAPIComponent implements OnInit {
const details = await openConfirmModal(
{
title: 'Remove tenant?',
content: `Remove <strong>${tenant.name}</strong> from this domain?<br>
content: `Remove <strong>${escapeHtml(tenant.name)}</strong> from this domain?<br>
<p style="text-align: left; width: 100%;">This will remove all related:</p><br>
<ul style="list-style: disc;text-align: left;padding-left: 2rem">
<li>bookings (such as desk bookings)</li>
Expand Down
5 changes: 3 additions & 2 deletions src/app/admin/storage/storage.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { escapeHtml } from '../../common/general';
import { i18n } from '../../common/locale.service';
import { openConfirmModal } from '../../overlays/confirm-modal.component';
import { IconComponent } from '../../ui/icon.component';
Expand Down Expand Up @@ -220,8 +221,8 @@ export class StorageComponent implements OnInit {
{
title: i18n('ADMIN.STORAGE_REMOVE_TITLE'),
content: i18n('ADMIN.STORAGE_REMOVE_MSG', {
type: item.storage_type,
name: item.bucket_name,
type: escapeHtml(item.storage_type),
name: escapeHtml(item.bucket_name),
}),
icon: { content: 'delete_forever' },
},
Expand Down
3 changes: 2 additions & 1 deletion src/app/admin/upload-library.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { apiKey, cleanObject, query, remove, token } from '@placeos/ts-client';
import { AsyncHandler } from '../common/async-handler.class';
import { escapeHtml } from '../common/general';
import { i18n } from '../common/locale.service';
import {
notifyError,
Expand Down Expand Up @@ -531,7 +532,7 @@ export class UploadLibraryComponent extends AsyncHandler implements OnInit {
{
title: i18n('ADMIN.UPLOADS_LIB_REMOVE'),
content: i18n('ADMIN.UPLOADS_LIB_REMOVE_MSG', {
filename: upload.file_name,
filename: escapeHtml(upload.file_name),
}),
icon: { type: 'icon', content: 'delete' },
},
Expand Down
4 changes: 0 additions & 4 deletions src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
invalidateToken,
isMock,
isOnline,
setAPI_Key,
token,
} from '@placeos/ts-client';

Expand Down Expand Up @@ -169,9 +168,6 @@ export class AppComponent extends AsyncHandler implements OnInit {
this._locale?.setLocale(locale);
localStorage.setItem('BACKOFFICE.locale', locale);
}
if (params.has('x-api-key')) {
setAPI_Key(params.get('x-api-key'));
}
});
setNotifyOutlet(this._snackbar);
setTranslationService(this._locale);
Expand Down
28 changes: 28 additions & 0 deletions src/app/common/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,34 @@ export function toQueryString(map: HashMap<unknown>) {
return str;
}

/**
* Check an extension embed URL before it is loaded in an iframe.
* Returns the resolved URL if it is http(s) and matches one of the
* configured extension URLs for the item, else `null`.
* @param embed URL from the `embed` query param
* @param allowed_urls Extension URLs from `extensionsForItem`
* @param base Base for relative URLs
*/
export function allowedEmbedUrl(
embed: string,
allowed_urls: string[],
base = location.origin,
): string | null {
const resolve = (value: string) => {
try {
const url = new URL(value, base);
return ['http:', 'https:'].includes(url.protocol) ? url.href : null;
} catch {
return null;
}
};
const url = embed ? resolve(embed) : null;
if (!url) return null;
return allowed_urls.some((allowed) => resolve(allowed) === url)
? url
: null;
}

/**
* Calculate the index of the module
* @param module_list List of modules in the parent system
Expand Down
17 changes: 17 additions & 0 deletions src/app/common/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,23 @@ export function unique<T = string>(array: T[], key = '') {
);
}

const HTML_ENTITIES: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};

/**
* Escape a value for use in an HTML string, e.g. a user controlled name
* inside confirm modal content or editor HTML
* @param value Value to escape
*/
export function escapeHtml(value: unknown): string {
return `${value ?? ''}`.replace(/[&<>"']/g, (char) => HTML_ENTITIES[char]);
}

/**
* Generate a random number
* @param ceil Biggest value to generate not inclusive
Expand Down
Loading
Loading