From 742acf66b44d4b9d6110072ce28395db6e970602 Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Tue, 10 Jun 2025 11:49:33 -0300 Subject: [PATCH 1/7] feat: add userMetadata subscription and types --- src/components/generic-link-share/subscription.ts | 11 ++++++++++- src/components/generic-link-share/types.ts | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/generic-link-share/subscription.ts b/src/components/generic-link-share/subscription.ts index 8c23a5f..e60e749 100644 --- a/src/components/generic-link-share/subscription.ts +++ b/src/components/generic-link-share/subscription.ts @@ -9,4 +9,13 @@ subscription MeetingSubscription { } `; -export default MEETING_SUBSCRIPTION; +const USER_METADATA = ` + subscription { + user_metadata { + parameter + value + } +} +`; + +export { MEETING_SUBSCRIPTION, USER_METADATA }; diff --git a/src/components/generic-link-share/types.ts b/src/components/generic-link-share/types.ts index 57e0b96..c9db8ac 100644 --- a/src/components/generic-link-share/types.ts +++ b/src/components/generic-link-share/types.ts @@ -18,4 +18,13 @@ interface DataToGenericLink { viewerUrl?: string } +export interface UserMetadata { + parameter: string; + value: string; +} + +export interface UserMetadataGraphqlResponse { + user_metadata: UserMetadata[]; +} + export { DecreaseVolumeOnSpeakProps, ExternalVideoMeetingSubscription, DataToGenericLink }; From 4829898d93c542edb65441e6af73ad5d4bc75cc4 Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Tue, 10 Jun 2025 11:52:52 -0300 Subject: [PATCH 2/7] feat: replace userdata placeholders in the URL --- .../generic-link-share/component.tsx | 25 +++++++++++------ src/components/generic-link-share/utils.ts | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/components/generic-link-share/component.tsx b/src/components/generic-link-share/component.tsx index 5a911c8..415bdf9 100644 --- a/src/components/generic-link-share/component.tsx +++ b/src/components/generic-link-share/component.tsx @@ -18,11 +18,12 @@ import { parseTags } from '../utils'; import GenericComponentLinkShare from '../generic-component/component'; -import { DataToGenericLink, DecreaseVolumeOnSpeakProps } from './types'; +import { DataToGenericLink, DecreaseVolumeOnSpeakProps, UserMetadataGraphqlResponse } from './types'; import { ModalToShareLink } from '../modal-to-share-link/component'; import { LinkTag } from '../modal-to-share-link/types'; import { REGEX } from './constants'; -import { replaceUrlPlaceholders } from './utils'; +import { replaceUrlPlaceholders, replaceUrlPlaceholdersSecure } from './utils'; +import { USER_METADATA } from './subscription'; function GenericLinkShare( { pluginUuid: uuid }: DecreaseVolumeOnSpeakProps, @@ -46,6 +47,17 @@ function GenericLinkShare( url: null, }); + const userMetaDataSubscription = pluginApi + .useCustomSubscription(USER_METADATA); + const { data } = userMetaDataSubscription; + + const currentUserPlaceholders = { + name: currentUser?.name ?? '', + extId: currentUser?.extId ?? '', + role: currentUser?.role ?? '', + presenter: !!currentUser?.presenter, + }; + useEffect(() => { const isGenericComponentInPile = currentLayout.some((gc) => ( gc.currentElement === UiLayouts.GENERIC_CONTENT @@ -246,12 +258,9 @@ function GenericLinkShare( const root = ReactDOM.createRoot(element); root.render( , ); return root; diff --git a/src/components/generic-link-share/utils.ts b/src/components/generic-link-share/utils.ts index 8953da3..eb68f2c 100644 --- a/src/components/generic-link-share/utils.ts +++ b/src/components/generic-link-share/utils.ts @@ -1,3 +1,5 @@ +import { UserMetadata } from './types'; + type PlaceholderValues = { name: string; extId: string; @@ -12,3 +14,29 @@ export function replaceUrlPlaceholders(url: string, values: PlaceholderValues): .replace(/{role}/g, encodeURIComponent(values.role)) .replace(/{presenter}/g, encodeURIComponent(String(values.presenter))); } + +export function replaceUrlPlaceholdersSecure( + url: string, + placeholderValues?: PlaceholderValues, + userdataParams?: UserMetadata[], +): string { + // list available userdata parameters + const allowedUserdataParams = ['plugin_generic_link_share_mykey']; + + // filter allowed userdata parameters + const foundUserdataParams = userdataParams?.filter( + (entry) => allowedUserdataParams.includes(entry.parameter), + ) || []; + + let result = replaceUrlPlaceholders(url, placeholderValues); + + foundUserdataParams.forEach((userdata) => { + const regexPattern = userdata.parameter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + result = result.replace( + new RegExp(regexPattern, 'g'), + encodeURIComponent(userdata.value), + ); + }); + + return result; +} From d6da3b51330164dbcc8ab47b1507213593d9bd3a Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Tue, 10 Jun 2025 14:53:19 -0300 Subject: [PATCH 3/7] feat: clarify placeholder usage for user metadata in shared URLs --- README.md | 33 ++++++++++++++++++++++ src/components/generic-link-share/utils.ts | 14 ++++----- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9f62c45..eb9f1b3 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,39 @@ pluginManifests=[{"url":"/path/to/manifest.json"}] Or additionally, you can add this same configuration in the `.properties` file from `bbb-web` in `/usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties` +## URL Placeholders + +When sharing a link, you can use placeholders in the URL that will be dynamically replaced with user or session information. This allows you to personalize the shared content for each participant. + +### Supported Placeholders + +- `{name}`: The user's display name +- `{extId}`: The user's external ID +- `{role}`: The user's role +- `{presenter}`: `true` if the user is a presenter, otherwise `false` +- `{genericLinkShare_KEY}`: Any additional user metadata field, where `KEY` is the metadata key name (e.g., `{genericLinkShare_info}` will be replaced with the user's *info* if available) + +**Note:** +To use user metadata as a placeholder, the metadata key must start with the prefix `genericLinkShare_`. For example, if you pass `userdata-genericLinkShare_mykey=12345` when joining, you can use `{genericLinkShare_mykey}` in your shared URL. + + +If additional user metadata is available, you can also use placeholders matching those metadata keys. + + +### Example + +If you enter the following link to share: + +``` +https://bigbluebutton.org/?name={name}&id={extId}&role={role}&presenter={presenter}&pass={genericLinkShare_mykey} +``` + +Each participant will see a personalized link with their own name, role, and the value of `genericLinkShare_mykey` (if set) substituted in the URL. + +### Security + +Sensitive placeholders are replaced securely to prevent injection or misuse. Only the placeholders listed above and those present in user metadata with the `genericLinkShare_` prefix are supported. + ## Development mode As for development mode (running this plugin from source), please, refer back to https://github.com/bigbluebutton/bigbluebutton-html-plugin-sdk section `Running the Plugin from Source` diff --git a/src/components/generic-link-share/utils.ts b/src/components/generic-link-share/utils.ts index eb68f2c..9a06230 100644 --- a/src/components/generic-link-share/utils.ts +++ b/src/components/generic-link-share/utils.ts @@ -7,6 +7,8 @@ type PlaceholderValues = { presenter: boolean; }; +const DEFAULT_PREFIX = 'genericLinkShare'; + export function replaceUrlPlaceholders(url: string, values: PlaceholderValues): string { return url .replace(/{name}/g, encodeURIComponent(values.name)) @@ -20,17 +22,13 @@ export function replaceUrlPlaceholdersSecure( placeholderValues?: PlaceholderValues, userdataParams?: UserMetadata[], ): string { - // list available userdata parameters - const allowedUserdataParams = ['plugin_generic_link_share_mykey']; - - // filter allowed userdata parameters - const foundUserdataParams = userdataParams?.filter( - (entry) => allowedUserdataParams.includes(entry.parameter), - ) || []; + const allowedUserdataParams = userdataParams?.filter( + (entry) => entry.parameter.startsWith(DEFAULT_PREFIX), + ); let result = replaceUrlPlaceholders(url, placeholderValues); - foundUserdataParams.forEach((userdata) => { + allowedUserdataParams.forEach((userdata) => { const regexPattern = userdata.parameter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); result = result.replace( new RegExp(regexPattern, 'g'), From 751341d1142fb65ba56ef2af0b937f5b1be9268a Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Wed, 11 Jun 2025 13:32:26 -0300 Subject: [PATCH 4/7] refac: consolidate URL placeholder replacement logic for maintainability --- .../generic-link-share/component.tsx | 10 +++-- src/components/generic-link-share/utils.ts | 38 ++++++++++--------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/components/generic-link-share/component.tsx b/src/components/generic-link-share/component.tsx index 415bdf9..88c42fd 100644 --- a/src/components/generic-link-share/component.tsx +++ b/src/components/generic-link-share/component.tsx @@ -22,7 +22,7 @@ import { DataToGenericLink, DecreaseVolumeOnSpeakProps, UserMetadataGraphqlRespo import { ModalToShareLink } from '../modal-to-share-link/component'; import { LinkTag } from '../modal-to-share-link/types'; import { REGEX } from './constants'; -import { replaceUrlPlaceholders, replaceUrlPlaceholdersSecure } from './utils'; +import { replaceUrlPlaceholdersSecure } from './utils'; import { USER_METADATA } from './subscription'; function GenericLinkShare( @@ -259,8 +259,12 @@ function GenericLinkShare( root.render( , ); return root; diff --git a/src/components/generic-link-share/utils.ts b/src/components/generic-link-share/utils.ts index 9a06230..5156d1d 100644 --- a/src/components/generic-link-share/utils.ts +++ b/src/components/generic-link-share/utils.ts @@ -9,32 +9,34 @@ type PlaceholderValues = { const DEFAULT_PREFIX = 'genericLinkShare'; -export function replaceUrlPlaceholders(url: string, values: PlaceholderValues): string { - return url - .replace(/{name}/g, encodeURIComponent(values.name)) - .replace(/{extId}/g, encodeURIComponent(values.extId)) - .replace(/{role}/g, encodeURIComponent(values.role)) - .replace(/{presenter}/g, encodeURIComponent(String(values.presenter))); -} - export function replaceUrlPlaceholdersSecure( url: string, placeholderValues?: PlaceholderValues, userdataParams?: UserMetadata[], ): string { - const allowedUserdataParams = userdataParams?.filter( - (entry) => entry.parameter.startsWith(DEFAULT_PREFIX), - ); + let result = url; - let result = replaceUrlPlaceholders(url, placeholderValues); + if (placeholderValues) { + result = result + .replace(/{name}/g, encodeURIComponent(placeholderValues.name)) + .replace(/{extId}/g, encodeURIComponent(placeholderValues.extId)) + .replace(/{role}/g, encodeURIComponent(placeholderValues.role)) + .replace(/{presenter}/g, encodeURIComponent(String(placeholderValues.presenter))); + } - allowedUserdataParams.forEach((userdata) => { - const regexPattern = userdata.parameter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - result = result.replace( - new RegExp(regexPattern, 'g'), - encodeURIComponent(userdata.value), + if (userdataParams) { + const allowedUserdataParams = userdataParams?.filter( + (entry) => entry.parameter.startsWith(DEFAULT_PREFIX), ); - }); + + allowedUserdataParams.forEach((userdata) => { + const userdataUrlParameterPattern = `{${userdata.parameter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}}`; + const userdataUrlValuePattern = userdata.value.replace(/[.*+?^${}()|[\]\\]/g, ''); + + result = result + .replace(new RegExp(userdataUrlParameterPattern, 'g'), encodeURIComponent(userdataUrlValuePattern)); + }); + } return result; } From e7688b2e9b7d064e38a888b9df2ff0305db36281 Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Wed, 11 Jun 2025 13:37:02 -0300 Subject: [PATCH 5/7] docs: add examples for user metadata placeholders in README --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eb9f1b3..7e9939e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,12 @@ When sharing a link, you can use placeholders in the URL that will be dynamicall - `{genericLinkShare_KEY}`: Any additional user metadata field, where `KEY` is the metadata key name (e.g., `{genericLinkShare_info}` will be replaced with the user's *info* if available) **Note:** -To use user metadata as a placeholder, the metadata key must start with the prefix `genericLinkShare_`. For example, if you pass `userdata-genericLinkShare_mykey=12345` when joining, you can use `{genericLinkShare_mykey}` in your shared URL. +To use user metadata as a placeholder, the metadata key must start with the prefix `genericLinkShare_`. +For example, if the metadata is `{genericLinkShare_mykey}`, in the join call, it is necessary to send: + +``` + userdata-genericLinkShare_mykey=abc_example +``` If additional user metadata is available, you can also use placeholders matching those metadata keys. From 75954c77e156f874337a91c9aaaac2e4f45a2ac2 Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Fri, 13 Jun 2025 11:35:30 -0300 Subject: [PATCH 6/7] feat: refactor replaceUrlPlaceholders to accept a placeholder list --- .../generic-link-share/component.tsx | 30 +++++------ src/components/generic-link-share/types.ts | 7 +++ src/components/generic-link-share/utils.ts | 50 +++++++++++-------- 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/src/components/generic-link-share/component.tsx b/src/components/generic-link-share/component.tsx index 88c42fd..8cec561 100644 --- a/src/components/generic-link-share/component.tsx +++ b/src/components/generic-link-share/component.tsx @@ -18,11 +18,16 @@ import { parseTags } from '../utils'; import GenericComponentLinkShare from '../generic-component/component'; -import { DataToGenericLink, DecreaseVolumeOnSpeakProps, UserMetadataGraphqlResponse } from './types'; +import { + CurrentUserData, + DataToGenericLink, + DecreaseVolumeOnSpeakProps, + UserMetadataGraphqlResponse, +} from './types'; import { ModalToShareLink } from '../modal-to-share-link/component'; import { LinkTag } from '../modal-to-share-link/types'; import { REGEX } from './constants'; -import { replaceUrlPlaceholdersSecure } from './utils'; +import { mergePlaceholdersList, replaceUrlPlaceholders } from './utils'; import { USER_METADATA } from './subscription'; function GenericLinkShare( @@ -30,8 +35,11 @@ function GenericLinkShare( ): React.ReactElement { BbbPluginSdk.initialize(uuid); const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); + const userMetaDataSubscription = pluginApi + .useCustomSubscription(USER_METADATA); const { data: currentUser } = pluginApi.useCurrentUser(); const { data: urlToGenericLink, pushEntry: pushEntryUrlToGenericLink, deleteEntry: deleteEntryUrlToGenericLink } = pluginApi.useDataChannel('urlToGenericLink'); + const { data: userData } = userMetaDataSubscription; const currentPresentationResponse = pluginApi.useCurrentPresentation(); const currentLayout = pluginApi.useUiData(LayoutPresentatioAreaUiDataNames.CURRENT_ELEMENT, [{ isOpen: true, @@ -47,11 +55,7 @@ function GenericLinkShare( url: null, }); - const userMetaDataSubscription = pluginApi - .useCustomSubscription(USER_METADATA); - const { data } = userMetaDataSubscription; - - const currentUserPlaceholders = { + const currentUserPlaceholders: CurrentUserData = { name: currentUser?.name ?? '', extId: currentUser?.extId ?? '', role: currentUser?.role ?? '', @@ -251,6 +255,10 @@ function GenericLinkShare( useEffect(() => { if (link && link !== '') { + const placeholdersList = mergePlaceholdersList( + currentUserPlaceholders, + userData.user_metadata, + ); pluginApi.setGenericContentItems([]); setGenericContentd(pluginApi.setGenericContentItems([ new GenericContentMainArea({ @@ -258,13 +266,7 @@ function GenericLinkShare( const root = ReactDOM.createRoot(element); root.render( , ); return root; diff --git a/src/components/generic-link-share/types.ts b/src/components/generic-link-share/types.ts index c9db8ac..3d0ee8c 100644 --- a/src/components/generic-link-share/types.ts +++ b/src/components/generic-link-share/types.ts @@ -18,6 +18,13 @@ interface DataToGenericLink { viewerUrl?: string } +export interface CurrentUserData { + name: string; + extId: string; + role: string; + presenter: boolean; +} + export interface UserMetadata { parameter: string; value: string; diff --git a/src/components/generic-link-share/utils.ts b/src/components/generic-link-share/utils.ts index 5156d1d..7cfff55 100644 --- a/src/components/generic-link-share/utils.ts +++ b/src/components/generic-link-share/utils.ts @@ -1,27 +1,23 @@ -import { UserMetadata } from './types'; +import { CurrentUserData, UserMetadata } from './types'; -type PlaceholderValues = { - name: string; - extId: string; - role: string; - presenter: boolean; -}; +type PlaceholderEntry = { + placeholder: string; + value: string | boolean; +} const DEFAULT_PREFIX = 'genericLinkShare'; -export function replaceUrlPlaceholdersSecure( - url: string, - placeholderValues?: PlaceholderValues, +export function mergePlaceholdersList( + currentUserParams?: CurrentUserData, userdataParams?: UserMetadata[], -): string { - let result = url; +): PlaceholderEntry[] { // nao esqueça de typar a função + const placeholdersList: PlaceholderEntry[] = []; - if (placeholderValues) { - result = result - .replace(/{name}/g, encodeURIComponent(placeholderValues.name)) - .replace(/{extId}/g, encodeURIComponent(placeholderValues.extId)) - .replace(/{role}/g, encodeURIComponent(placeholderValues.role)) - .replace(/{presenter}/g, encodeURIComponent(String(placeholderValues.presenter))); + if (currentUserParams) { + Object.entries(currentUserParams).forEach(([key, value]) => { + const placeholder = `{${key}}`; + placeholdersList.push({ placeholder, value }); + }); } if (userdataParams) { @@ -33,10 +29,24 @@ export function replaceUrlPlaceholdersSecure( const userdataUrlParameterPattern = `{${userdata.parameter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}}`; const userdataUrlValuePattern = userdata.value.replace(/[.*+?^${}()|[\]\\]/g, ''); - result = result - .replace(new RegExp(userdataUrlParameterPattern, 'g'), encodeURIComponent(userdataUrlValuePattern)); + placeholdersList + .push({ placeholder: userdataUrlParameterPattern, value: userdataUrlValuePattern }); }); } + return placeholdersList; +} + +export function replaceUrlPlaceholders( + url: string, + placeholdersList: PlaceholderEntry[], +): string { + let result = url; + + placeholdersList.forEach(({ placeholder, value }) => { + const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'); + result = result.replace(regex, encodeURIComponent(String(value))); + }); + return result; } From b936c5cd693eae02aa22f2d7ae3dadea7c2b1b0c Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Mon, 16 Jun 2025 13:16:52 -0300 Subject: [PATCH 7/7] refac: move PlaceHolderEntry to types --- src/components/generic-link-share/types.ts | 5 +++++ src/components/generic-link-share/utils.ts | 9 ++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/generic-link-share/types.ts b/src/components/generic-link-share/types.ts index 3d0ee8c..dda6949 100644 --- a/src/components/generic-link-share/types.ts +++ b/src/components/generic-link-share/types.ts @@ -34,4 +34,9 @@ export interface UserMetadataGraphqlResponse { user_metadata: UserMetadata[]; } +export interface PlaceholderEntry { + placeholder: string; + value: string | boolean; +} + export { DecreaseVolumeOnSpeakProps, ExternalVideoMeetingSubscription, DataToGenericLink }; diff --git a/src/components/generic-link-share/utils.ts b/src/components/generic-link-share/utils.ts index 7cfff55..df775b2 100644 --- a/src/components/generic-link-share/utils.ts +++ b/src/components/generic-link-share/utils.ts @@ -1,16 +1,11 @@ -import { CurrentUserData, UserMetadata } from './types'; - -type PlaceholderEntry = { - placeholder: string; - value: string | boolean; -} +import { CurrentUserData, PlaceholderEntry, UserMetadata } from './types'; const DEFAULT_PREFIX = 'genericLinkShare'; export function mergePlaceholdersList( currentUserParams?: CurrentUserData, userdataParams?: UserMetadata[], -): PlaceholderEntry[] { // nao esqueça de typar a função +): PlaceholderEntry[] { const placeholdersList: PlaceholderEntry[] = []; if (currentUserParams) {