From 1ea964223b13cd565b5deb30ba552d47dd7f5465 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 2 Oct 2024 18:34:10 -0300 Subject: [PATCH 01/13] [new-server-side-architecture-plugins] - Created all the data flow of plugins from a URL parameter of manifest.json until the pluginLoader component of the client. --- .../PluginDataChannelDeleteEntryMsgHdlr.scala | 78 ++++++++-------- .../PluginDataChannelPushEntryMsgHdlr.scala | 63 +++++++------ ...PluginDataChannelReplaceEntryMsgHdlr.scala | 79 ++++++++-------- .../PluginDataChannelResetMsgHdlr.scala | 54 +++++------ .../org/bigbluebutton/core/db/PluginDAO.scala | 30 ++++++ .../bigbluebutton/core/models/Plugins.scala | 71 ++++++++++++++ .../core/running/LiveMeeting.scala | 1 + .../core/running/MeetingActor.scala | 1 + .../core/running/RunningMeeting.scala | 12 ++- .../common2/domain/Meeting2x.scala | 3 + .../java/org/bigbluebutton/api/ApiParams.java | 1 + .../org/bigbluebutton/api/MeetingService.java | 93 ++++++++++++++++++- .../api/ParamsProcessorUtil.java | 8 ++ .../org/bigbluebutton/api/domain/Meeting.java | 20 ++++ .../bigbluebutton/api2/IBbbWebApiGWApp.java | 1 + .../bigbluebutton/api2/BbbWebApiGWApp.scala | 4 + bbb-graphql-server/bbb_schema.sql | 11 +++ .../BigBlueButton/tables/public_v_plugin.yaml | 27 ++++++ .../BigBlueButton/tables/tables.yaml | 1 + bigbluebutton-html5/client/meetingClient.jsx | 7 +- .../imports/ui/components/app/component.jsx | 3 +- .../plugins-engine/loader/manager.tsx | 4 +- .../components/plugins-engine/loader/types.ts | 2 +- .../ui/components/plugins-engine/manager.tsx | 42 +++++---- .../ui/components/plugins-engine/query.ts | 10 ++ .../ui/components/plugins-engine/styles.ts | 12 +-- .../ui/components/plugins-engine/types.ts | 9 ++ 27 files changed, 473 insertions(+), 174 deletions(-) create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala create mode 100644 bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml create mode 100644 bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala index 2cc9f5104c..a5a97212d7 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala @@ -1,10 +1,9 @@ package org.bigbluebutton.core.apps.plugin -import org.bigbluebutton.ClientSettings import org.bigbluebutton.common2.msgs.PluginDataChannelDeleteEntryMsg import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{ Roles, Users2x } +import org.bigbluebutton.core.models.{ PluginModel, Roles, Users2x } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } trait PluginDataChannelDeleteEntryMsgHdlr extends HandlerHelpers { @@ -17,45 +16,44 @@ trait PluginDataChannelDeleteEntryMsgHdlr extends HandlerHelpers { _ <- if (!pluginsDisabled) Some(()) else None user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) } yield { - val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile) - - if (!pluginsConfig.contains(msg.body.pluginName)) { - println(s"Plugin '${msg.body.pluginName}' not found.") - } else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.channelName)) { - println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") - } else { - val hasPermission = for { - replaceOrDeletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).replaceOrDeletePermission - } yield { - replaceOrDeletePermission.toLowerCase match { - case "all" => true - case "moderator" => user.role == Roles.MODERATOR_ROLE - case "presenter" => user.presenter - case "creator" => { - val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.body.entryId - ) - creatorUserId == msg.header.userId - } - case _ => false + PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { + case Some(p) => + p.manifest.content.dataChannels.find(dc => dc.name == msg.body.channelName) match { + case Some(dc) => + val hasPermission = for { + replaceOrDeletePermission <- dc.replaceOrDeletePermission + } yield { + replaceOrDeletePermission.toLowerCase match { + case "all" => true + case "moderator" => user.role == Roles.MODERATOR_ROLE + case "presenter" => user.presenter + case "creator" => { + val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator( + meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName, + msg.body.entryId + ) + creatorUserId == msg.header.userId + } + case _ => false + } + } + if (!hasPermission.contains(true)) { + println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") + } else { + PluginDataChannelEntryDAO.delete( + meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName, + msg.body.entryId + ) + } + case None => println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") } - } - - if (!hasPermission.contains(true)) { - println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") - } else { - PluginDataChannelEntryDAO.delete( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.body.entryId - ) - } + case None => println(s"Plugin '${msg.body.pluginName}' not found.") } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala index fc6b8c4c45..97d79fa3f4 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala @@ -1,10 +1,9 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.common2.msgs.PluginDataChannelPushEntryMsg -import org.bigbluebutton.ClientSettings import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{ Roles, Users2x } +import org.bigbluebutton.core.models.{ PluginModel, Roles, Users2x } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers { @@ -17,38 +16,38 @@ trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers { _ <- if (!pluginsDisabled) Some(()) else None user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) } yield { - val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile) + PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { + case Some(p) => + p.manifest.content.dataChannels.find(dc => dc.name == msg.body.channelName) match { + case Some(dc) => + val hasPermission = for { + pushPermission <- dc.pushPermission + } yield { + pushPermission.toLowerCase match { + case "all" => true + case "moderator" => user.role == Roles.MODERATOR_ROLE + case "presenter" => user.presenter + case _ => false + } + } - if (!pluginsConfig.contains(msg.body.pluginName)) { - println(s"Plugin '${msg.body.pluginName}' not found.") - } else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.channelName)) { - println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") - } else { - val hasPermission = for { - pushPermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).pushPermission - } yield { - pushPermission.toLowerCase match { - case "all" => true - case "moderator" => user.role == Roles.MODERATOR_ROLE - case "presenter" => user.presenter - case _ => false + if (!hasPermission.contains(true)) { + println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") + } else { + PluginDataChannelEntryDAO.insert( + meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName, + msg.header.userId, + msg.body.payloadJson, + msg.body.toRoles, + msg.body.toUserIds + ) + } + case None => println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") } - } - - if (!hasPermission.contains(true)) { - println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") - } else { - PluginDataChannelEntryDAO.insert( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.header.userId, - msg.body.payloadJson, - msg.body.toRoles, - msg.body.toUserIds - ) - } + case None => println(s"Plugin '${msg.body.pluginName}' not found.") } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala index 453f64587d..63ccbe619e 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala @@ -1,10 +1,9 @@ package org.bigbluebutton.core.apps.plugin -import org.bigbluebutton.ClientSettings import org.bigbluebutton.common2.msgs.PluginDataChannelReplaceEntryMsg import org.bigbluebutton.core.db.{JsonUtils, PluginDataChannelEntryDAO} import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{Roles, Users2x} +import org.bigbluebutton.core.models.{PluginModel, Roles, Users2x} import org.bigbluebutton.core.running.{HandlerHelpers, LiveMeeting} trait PluginDataChannelReplaceEntryMsgHdlr extends HandlerHelpers { @@ -17,46 +16,46 @@ trait PluginDataChannelReplaceEntryMsgHdlr extends HandlerHelpers { _ <- if (!pluginsDisabled) Some(()) else None user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) } yield { - val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile) + PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { + case Some(p) => + p.manifest.content.dataChannels.find(dc => dc.name == msg.body.channelName) match { + case Some(dc) => + val hasPermission = for { + replaceOrDeletePermission <- dc.replaceOrDeletePermission + } yield { + replaceOrDeletePermission.toLowerCase match { + case "all" => true + case "moderator" => user.role == Roles.MODERATOR_ROLE + case "presenter" => user.presenter + case "creator" => { + val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator( + meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName, + msg.body.entryId + ) + creatorUserId == msg.header.userId + } + case _ => false + } + } - if (!pluginsConfig.contains(msg.body.pluginName)) { - println(s"Plugin '${msg.body.pluginName}' not found.") - } else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.channelName)) { - println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") - } else { - val hasPermission = for { - replaceOrDeletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).replaceOrDeletePermission - } yield { - replaceOrDeletePermission.toLowerCase match { - case "all" => true - case "moderator" => user.role == Roles.MODERATOR_ROLE - case "presenter" => user.presenter - case "creator" => { - val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.body.entryId - ) - creatorUserId == msg.header.userId - } - case _ => false + if (!hasPermission.contains(true)) { + println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") + } else { + PluginDataChannelEntryDAO.replace( + msg.header.meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName, + msg.body.entryId, + JsonUtils.mapToJson(msg.body.payloadJson), + ) + } + case None => println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") + case None => println(s"Plugin '${msg.body.pluginName}' not found.") } - } - - if (!hasPermission.contains(true)) { - println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") - } else { - PluginDataChannelEntryDAO.replace( - msg.header.meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.body.entryId, - JsonUtils.mapToJson(msg.body.payloadJson), - ) - } } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala index 32a403f862..05002f28fa 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala @@ -4,7 +4,7 @@ import org.bigbluebutton.ClientSettings import org.bigbluebutton.common2.msgs.PluginDataChannelResetMsg import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{ Roles, Users2x } +import org.bigbluebutton.core.models.{ PluginModel, Roles, Users2x } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } trait PluginDataChannelResetMsgHdlr extends HandlerHelpers { @@ -17,34 +17,34 @@ trait PluginDataChannelResetMsgHdlr extends HandlerHelpers { _ <- if (!pluginsDisabled) Some(()) else None user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) } yield { - val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile) + PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { + case Some(p) => + p.manifest.content.dataChannels.find(dc => dc.name == msg.body.channelName) match { + case Some(dc) => + val hasPermission = for { + replaceOrDeletePermission <- dc.replaceOrDeletePermission + } yield { + replaceOrDeletePermission.toLowerCase match { + case "all" => true + case "moderator" => user.role == Roles.MODERATOR_ROLE + case "presenter" => user.presenter + case _ => false + } + } - if (!pluginsConfig.contains(msg.body.pluginName)) { - println(s"Plugin '${msg.body.pluginName}' not found.") - } else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.channelName)) { - println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") - } else { - val hasPermission = for { - replaceOrDeletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).replaceOrDeletePermission - } yield { - replaceOrDeletePermission.toLowerCase match { - case "all" => true - case "moderator" => user.role == Roles.MODERATOR_ROLE - case "presenter" => user.presenter - case _ => false + if (!hasPermission.contains(true)) { + println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") + } else { + PluginDataChannelEntryDAO.reset( + meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName + ) + } + case None => println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") } - } - - if (!hasPermission.contains(true)) { - println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") - } else { - PluginDataChannelEntryDAO.reset( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName - ) - } + case None => println(s"Plugin '${msg.body.pluginName}' not found.") } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala new file mode 100644 index 0000000000..4549ea158a --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala @@ -0,0 +1,30 @@ +package org.bigbluebutton.core.db + +import PostgresProfile.api._ + +case class PluginDbModel( + meetingId: String, + name: String, + javascriptEntrypointUrl: String, +) + +class PluginDbTableDef(tag: Tag) extends Table[PluginDbModel](tag, None, "plugin") { + val meetingId = column[String]("meetingId", O.PrimaryKey) + val name = column[String]("name", O.PrimaryKey) + val javascriptEntrypointUrl = column[String]("javascriptEntrypointUrl") + override def * = (meetingId, name, javascriptEntrypointUrl) <> (PluginDbModel.tupled, PluginDbModel.unapply) +} + +object PluginDAO { + def insert(meetingId: String, name: String, javascriptEntrypointUrl: String) = { + DatabaseConnection.enqueue( + TableQuery[PluginDbTableDef].forceInsert( + PluginDbModel( + meetingId = meetingId, + name = name, + javascriptEntrypointUrl = javascriptEntrypointUrl, + ) + ) + ) + } +} \ No newline at end of file diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala new file mode 100644 index 0000000000..af7bdbb61e --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala @@ -0,0 +1,71 @@ +package org.bigbluebutton.core.models + +import org.bigbluebutton.common2.util.JsonUtil +import org.bigbluebutton.core.db.PluginDAO + +case class RateLimiting( + messagesAllowedPerSecond: Int, + messagesAllowedPerMinute: Int +) + +case class EventPersistence( + isEnabled: Boolean, + maximumPayloadSizeInBytes: Int, + rateLimiting: RateLimiting +) + +case class DataChannel( + name: String, + pushPermission: List[String], + replaceOrDeletePermission: List[String] +) + +case class RemoteDataSource( + name: String, + url: String, + fetchMode: String, + permissions: List[String] +) + +case class PluginManifestContent( + requiredSdkVersion: String, + name: String, + javascriptEntrypointUrl: String, + localesBaseUrl: String, + eventPersistence: EventPersistence, + dataChannels: List[DataChannel], + remoteDataSources: List[RemoteDataSource] +) + +case class PluginManifest( + url: String, + content: PluginManifestContent +) + +case class Plugin( + manifest: PluginManifest +) + +object PluginModel { + def getPluginByName(instance: PluginModel, pluginName: String): Option[Plugin] = { + instance.plugins.get(pluginName) + } + def getPlugins(instance: PluginModel): Map[String, Plugin] = { + instance.plugins + } + def createPluginModelFromJson(jsonString: String): PluginModel = { + val instance = new PluginModel() + instance.plugins = JsonUtil.fromJson[Map[String, Plugin]](jsonString).getOrElse(Map()) + instance + } + def persistPluginsForClient(instance: PluginModel, meetingId: String): Unit = { + instance.plugins.foreach { case (_, plugin) => + PluginDAO.insert(meetingId, plugin.manifest.content.name, plugin.manifest.content.javascriptEntrypointUrl) + } + } +} + +class PluginModel { + private var plugins: Map[String, Plugin] = Map() +} + diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/LiveMeeting.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/LiveMeeting.scala index 01aa16fcde..418eafce25 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/LiveMeeting.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/LiveMeeting.scala @@ -27,4 +27,5 @@ class LiveMeeting( val users2x: Users2x, val guestsWaiting: GuestsWaiting, val clientSettings: Map[String, Object], + val plugins: PluginModel, ) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index da2cd095e6..fa823a2074 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -173,6 +173,7 @@ class MeetingActor( //Insert meeting into the database MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings) + PluginModel.persistPluginsForClient(liveMeeting.plugins, props.meetingProp.intId) // Create a default public group chat state = groupChatApp.handleCreateDefaultPublicGroupChat(state, liveMeeting, msgBus) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala index 16bdd59e68..3ae0031595 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala @@ -1,14 +1,16 @@ package org.bigbluebutton.core.running +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.scala.DefaultScalaModule import org.apache.pekko.actor.ActorContext import org.bigbluebutton.ClientSettings -import org.bigbluebutton.ClientSettings.{getConfigPropertyValueByPathAsBooleanOrElse, getConfigPropertyValueByPathAsStringOrElse} +import org.bigbluebutton.ClientSettings.{getConfigPropertyValueByPathAsBooleanOrElse, getConfigPropertyValueByPathAsStringOrElse, logger} import org.bigbluebutton.common2.domain.DefaultProps import org.bigbluebutton.core.apps._ import org.bigbluebutton.core.bus._ import org.bigbluebutton.core.models._ import org.bigbluebutton.core.OutMessageGateway -import org.bigbluebutton.core.apps.pads.PadslHdlrHelpers +import org.bigbluebutton.core.models.PluginModel.getPlugins import org.bigbluebutton.core2.MeetingStatus2x object RunningMeeting { @@ -19,9 +21,11 @@ object RunningMeeting { class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway, eventBus: InternalEventBus)(implicit val context: ActorContext) { - + val objectMapper: ObjectMapper = new ObjectMapper(); + objectMapper.registerModule(new DefaultScalaModule()) private val externalVideoModel = new ExternalVideoModel() private val chatModel = new ChatModel() + private val plugins = PluginModel.createPluginModelFromJson(objectMapper.writeValueAsString(props.pluginProp)) private val layouts = new Layouts() private val pads = new Pads() private val wbModel = new WhiteboardModel() @@ -45,7 +49,7 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway, // easy to test. private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, audioCaptions, timerModel, chatModel, externalVideoModel, layouts, pads, registeredUsers, polls2x, wbModel, presModel, captionModel, - webcams, voiceUsers, users2x, guestsWaiting, clientSettings) + webcams, voiceUsers, users2x, guestsWaiting, clientSettings, plugins) GuestsWaiting.setGuestPolicy( liveMeeting.props.meetingProp.intId, diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala index 2e2c9dd904..443b31b0e6 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala @@ -1,5 +1,7 @@ package org.bigbluebutton.common2.domain +import java.util + case class DurationProps(duration: Int, createdTime: Long, createdDate: String, meetingExpireIfNoUserJoinedInMinutes: Int, meetingExpireWhenLastUserLeftInMinutes: Int, userInactivityInspectTimerInMinutes: Int, userInactivityThresholdInMinutes: Int, @@ -85,6 +87,7 @@ case class GroupProps( ) case class DefaultProps( + pluginProp: util.Map[String, AnyRef], meetingProp: MeetingProp, breakoutProps: BreakoutProps, durationProps: DurationProps, diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java index 2de1966241..e4128661d2 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java @@ -73,6 +73,7 @@ public class ApiParams { public static final String ROLE = "role"; public static final String GROUPS = "groups"; public static final String DISABLED_FEATURES = "disabledFeatures"; + public static final String PLUGINS = "plugins"; public static final String DISABLED_FEATURES_EXCLUDE = "disabledFeaturesExclude"; public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn"; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 2269382841..7b8dcdf829 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -19,7 +19,10 @@ package org.bigbluebutton.api; import java.io.File; +import java.math.BigInteger; import java.net.URI; +import java.net.URL; +import java.security.MessageDigest; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.BlockingQueue; @@ -29,6 +32,9 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.JsonObject; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; @@ -43,6 +49,7 @@ import org.bigbluebutton.api.messaging.messages.*; import org.bigbluebutton.api2.IBbbWebApiGWApp; import org.bigbluebutton.api2.domain.UploadedTrack; import org.bigbluebutton.common2.redis.RedisStorageService; +import org.bigbluebutton.common2.util.JsonUtil; import org.bigbluebutton.presentation.PresentationUrlDownloadService; import org.bigbluebutton.presentation.imp.SlidesGenerationProgressNotifier; import org.bigbluebutton.web.services.UserCleanupTimerTask; @@ -57,6 +64,8 @@ import com.google.gson.Gson; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.springframework.data.domain.*; @@ -97,6 +106,8 @@ public class MeetingService implements MessageListener { private HashMap uploadAuthzTokens; + ObjectMapper objectMapper = new ObjectMapper(); + public MeetingService() { meetings = new ConcurrentHashMap(8, 0.9f, 1); sessions = new ConcurrentHashMap(8, 0.9f, 1); @@ -339,6 +350,84 @@ public class MeetingService implements MessageListener { : Collections.unmodifiableCollection(sessions.values()); } + public String replaceMetaParametersIntoManifestTemplate(String manifestContent, Map metadata) + throws NoSuchFieldException { + // Pattern to match ${variable} in the input string + Pattern pattern = Pattern.compile("\\$\\{(\\w+)}"); + Matcher matcher = pattern.matcher(manifestContent); + + StringBuilder result = new StringBuilder(); + + // Iterate over all matches + while (matcher.find()) { + + String variableName = matcher.group(1); + if (variableName.length() > 5){ + // Remove "meta_" and turn everything lower case + variableName = variableName.substring(5).toLowerCase(); + } else { + throw new NoSuchFieldException("Metadata " + variableName + " is malformed, please provide a valid one"); + } + + String replacement; + if (metadata.containsKey(variableName)) + replacement = metadata.get(variableName); + else throw new NoSuchFieldException("Metadata " + variableName + " not found in URL parameters"); + + // Replace the placeholder with the value from the map + matcher.appendReplacement(result, replacement); + } + matcher.appendTail(result); + + return result.toString(); + } + public Map requestPluginManifests(Meeting m) { + Map urlContents = new HashMap<>(); + Map metadata = m.getMetadata(); + + // Fetch content for each URL and store in the map + for (String manifestUrl : m.getPluginManifestUrls()) { + try { + URL url = new URL(manifestUrl); + BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); + StringBuilder content = new StringBuilder(); + String inputLine; + + while ((inputLine = in.readLine()) != null) { + content.append(inputLine).append("\n"); + } + in.close(); + + // Parse the JSON content and get the "name" field + JsonNode jsonNode = objectMapper.readTree(content.toString()); + String name; + if (jsonNode.has("name")) { + name = jsonNode.get("name").asText(); + } else { + throw new NoSuchFieldException("For url " + manifestUrl + "there is no name field configured."); + } + + String pluginKey = name; + HashMap manifestObject = new HashMap<>(); + manifestObject.put("url", manifestUrl); + String manifestContent = replaceMetaParametersIntoManifestTemplate(content.toString(), metadata); + + ObjectMapper mapper = new ObjectMapper(); + Map MappedManifestContent = mapper.readValue(manifestContent, new TypeReference<>() {}); + + manifestObject.put("content", MappedManifestContent); + Map manifestWrapper = new HashMap(); + manifestWrapper.put( + "manifest", manifestObject + ); + urlContents.put(pluginKey, manifestWrapper); + } catch(Exception e) { + log.error("Failed with the following plugin manifest URL: {}. Error: {} therefore this plugin will not load", + manifestUrl, e); + } + } + return urlContents; + } public synchronized boolean createMeeting(Meeting m) { String internalMeetingId = paramsProcessorUtil.convertToInternalMeetingId(m.getExternalId()); Meeting existingId = getNotEndedMeetingWithId(internalMeetingId); @@ -346,6 +435,8 @@ public class MeetingService implements MessageListener { Meeting existingWebVoice = getNotEndedMeetingWithWebVoice(m.getWebVoice()); if (existingId == null && existingTelVoice == null && existingWebVoice == null) { meetings.put(m.getInternalId(), m); + Map requestedManifests = requestPluginManifests(m); + m.setPlugins(requestedManifests); handle(new CreateMeeting(m)); return true; } @@ -431,7 +522,7 @@ public class MeetingService implements MessageListener { m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(), m.breakoutRoomsParams, m.lockSettingsParams, m.getLoginUrl(), m.getLogoutUrl(), m.getCustomLogoURL(), m.getCustomDarkLogoURL(), m.getBannerText(), m.getBannerColor(), m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(), - m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(), + m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(), m.getPlugins(), m.getOverrideClientSettings()); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index 697edfd427..d2d0ecddff 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -540,6 +540,13 @@ public class ParamsProcessorUtil { listOfDisabledFeatures.replaceAll(String::trim); listOfDisabledFeatures = new ArrayList<>(new HashSet<>(listOfDisabledFeatures)); + // Process Plugin Manifest Urls + ArrayList listOfPlugins = new ArrayList(); + if (!StringUtils.isEmpty(params.get(ApiParams.PLUGINS))) { + String pluginsParam = params.get(ApiParams.PLUGINS); + listOfPlugins.addAll(Arrays.asList(pluginsParam.split(","))); + } + // Check Disabled Features Exclude list -- passed as a CREATE parameter to cancel the disabling (typically from bbb-web's properties file) ArrayList listOfDisabledFeaturesExclude = new ArrayList<>(); if (!StringUtils.isEmpty(params.get(ApiParams.DISABLED_FEATURES_EXCLUDE))) { @@ -790,6 +797,7 @@ public class ParamsProcessorUtil { .withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins) .withLearningDashboardAccessToken(learningDashboardAccessToken) .withGroups(groups) + .withPluginManifestUrls(listOfPlugins) .withDisabledFeatures(listOfDisabledFeatures) .withNotifyRecordingIsOn(notifyRecordingIsOn) .withPresentationUploadExternalDescription(presentationUploadExternalDescription) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java index 45d66a6baa..fb501b89b4 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java @@ -78,6 +78,8 @@ public class Meeting { private String dialNumber; private String defaultAvatarURL; private String defaultWebcamBackgroundURL; + private Map plugins; + private ArrayList pluginManifestUrls; private String guestPolicy = GuestPolicy.ASK_MODERATOR; private String guestLobbyMessage = ""; private Map usersWithGuestLobbyMessages; @@ -128,6 +130,7 @@ public class Meeting { extMeetingId = builder.externalId; intMeetingId = builder.internalId; disabledFeatures = builder.disabledFeatures; + pluginManifestUrls = builder.pluginManifestUrls; notifyRecordingIsOn = builder.notifyRecordingIsOn; presentationUploadExternalDescription = builder.presentationUploadExternalDescription; presentationUploadExternalUrl = builder.presentationUploadExternalUrl; @@ -437,6 +440,17 @@ public class Meeting { public ArrayList getDisabledFeatures() { return disabledFeatures; } + public Map getPlugins() { + return plugins; + } + + public void setPlugins(Map p) { + plugins = p; + } + + public ArrayList getPluginManifestUrls() { + return pluginManifestUrls; + } public Boolean getNotifyRecordingIsOn() { return notifyRecordingIsOn; @@ -925,6 +939,7 @@ public class Meeting { private int learningDashboardCleanupDelayInMinutes; private String learningDashboardAccessToken; private ArrayList disabledFeatures; + private ArrayList pluginManifestUrls; private Boolean notifyRecordingIsOn; private String presentationUploadExternalDescription; private String presentationUploadExternalUrl; @@ -1059,6 +1074,11 @@ public class Meeting { return this; } + public Builder withPluginManifestUrls(ArrayList map) { + this.pluginManifestUrls = map; + return this; + } + public Builder withNotifyRecordingIsOn(Boolean b) { this.notifyRecordingIsOn = b; return this; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java index 3c8bf97d4e..9765878967 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java @@ -69,6 +69,7 @@ public interface IBbbWebApiGWApp { Boolean notifyRecordingIsOn, String presentationUploadExternalDescription, String presentationUploadExternalUrl, + Map plugins, String overrideClientSettings); void registerUser(String meetingID, String internalUserId, String fullname, String role, diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala index a21444e9e8..8f8c53754b 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala @@ -18,6 +18,8 @@ import scala.concurrent.duration._ import org.bigbluebutton.common2.redis._ import org.bigbluebutton.common2.bus._ +import java.util + class BbbWebApiGWApp( val oldMessageReceivedGW: OldMessageReceivedGW, redisHost: String, @@ -165,6 +167,7 @@ class BbbWebApiGWApp( notifyRecordingIsOn: java.lang.Boolean, presentationUploadExternalDescription: String, presentationUploadExternalUrl: String, + plugins: util.Map[String, AnyRef], overrideClientSettings: String): Unit = { val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector @@ -262,6 +265,7 @@ class BbbWebApiGWApp( val groupsAsVector: Vector[GroupProps] = groups.asScala.toVector.map(g => GroupProps(g.getGroupId(), g.getName(), g.getUsersExtId().asScala.toVector)) val defaultProps = DefaultProps( + plugins, meetingProp, breakoutProps, durationProps, diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index 23fc0ea875..86dcca20bb 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1962,6 +1962,17 @@ and n."createdAt" > current_timestamp - '5 seconds'::interval; create index idx_notification on notification("meetingId","userId","role","createdAt"); +-- ========== Plugin tables + +create table "plugin" ( + "meetingId" varchar(100), + "name" varchar(100), + "javascriptEntrypointUrl" varchar(500), + CONSTRAINT "plugin_pk" PRIMARY KEY ("meetingId","name"), + FOREIGN KEY ("meetingId") REFERENCES "meeting"("meetingId") ON DELETE CASCADE +); + +create view "v_plugin" as select * from "plugin"; -------------------------------- ---Plugins Data Channel diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml new file mode 100644 index 0000000000..857e370880 --- /dev/null +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml @@ -0,0 +1,27 @@ +table: + name: v_plugin + schema: public +configuration: + column_config: {} + custom_column_names: {} + custom_name: plugin + custom_root_fields: {} +select_permissions: + - role: bbb_client + permission: + columns: + - javascriptEntrypointUrl + - name + filter: + meetingId: + _eq: X-Hasura-MeetingId + comment: "" + - role: bbb_client_not_in_meeting + permission: + columns: + - javascriptEntrypointUrl + - name + filter: + meetingId: + _eq: X-Hasura-MeetingId + comment: "" diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml index 3f79b29b6a..ab79831fc3 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml @@ -25,6 +25,7 @@ - "!include public_v_meeting_usersPolicies.yaml" - "!include public_v_meeting_voiceSettings.yaml" - "!include public_v_notification.yaml" +- "!include public_v_plugin.yaml" - "!include public_v_pluginDataChannelEntry.yaml" - "!include public_v_poll.yaml" - "!include public_v_poll_option.yaml" diff --git a/bigbluebutton-html5/client/meetingClient.jsx b/bigbluebutton-html5/client/meetingClient.jsx index 1718c9e364..4e7267a4f9 100755 --- a/bigbluebutton-html5/client/meetingClient.jsx +++ b/bigbluebutton-html5/client/meetingClient.jsx @@ -31,6 +31,8 @@ import { LoadingContext } from '/imports/ui/components/common/loading-screen/loa import IntlAdapter from '/imports/startup/client/intlAdapter'; import PresenceAdapter from '../imports/ui/components/presence-adapter/component'; import CustomUsersSettings from '/imports/ui/components/join-handler/custom-users-settings/component'; +import createUseSubscription from '/imports/ui/core/hooks/createUseSubscription'; +import PLUGIN_CONFIGURATION_QUERY from '/imports/ui/components/plugins-engine/query'; // eslint-disable-next-line import/prefer-default-export const Startup = () => { @@ -60,11 +62,14 @@ const Startup = () => { }, message); }); + const { data: pluginConfig } = createUseSubscription( + PLUGIN_CONFIGURATION_QUERY, + )((obj) => obj); return ( - + diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 6a5f77d315..4d60bc678b 100644 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -250,6 +250,7 @@ class App extends Component { presentationIsOpen, darkTheme, intl, + pluginConfig, genericMainContentId, } = this.props; @@ -261,7 +262,7 @@ class App extends Component { return ( <> - + diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx index b3bd320bfb..30c131be3e 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx @@ -6,7 +6,7 @@ const PluginLoaderManager = (props: PluginLoaderManagerProps) => { const { uuid, containerRef, - loadedPlugins, + setNumberOfLoadedPlugins, setLastLoadedPlugin, pluginConfig: plugin, } = props; @@ -22,7 +22,7 @@ const PluginLoaderManager = (props: PluginLoaderManagerProps) => { const script: HTMLScriptElement = document.createElement('script'); script.onload = () => { - loadedPlugins.current += 1; + setNumberOfLoadedPlugins((current) => current + 1); setLastLoadedPlugin(script); logger.info({ logCode: 'plugin_loaded', diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/types.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/types.ts index 7c1993a7b1..901a102941 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/types.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/types.ts @@ -3,7 +3,7 @@ import { PluginConfig } from '../types'; export interface PluginLoaderManagerProps { uuid: string; containerRef: React.RefObject; - loadedPlugins: React.MutableRefObject; + setNumberOfLoadedPlugins: React.Dispatch>; setLastLoadedPlugin: React.Dispatch>; pluginConfig: PluginConfig; } diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/manager.tsx index de09ba0837..213130416c 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/manager.tsx @@ -1,5 +1,5 @@ import React, { - useEffect, useRef, useState, useMemo, + useEffect, useRef, useState, } from 'react'; import logger from '/imports/startup/client/logger'; import { @@ -9,7 +9,7 @@ import * as PluginSdk from 'bigbluebutton-html-plugin-sdk'; import * as uuidLib from 'uuid'; import PluginDataConsumptionManager from './data-consumption/manager'; import PluginsEngineComponent from './component'; -import { PluginConfig, EffectivePluginConfig } from './types'; +import { EffectivePluginConfig, PluginsEngineManagerProps } from './types'; import PluginLoaderManager from './loader/manager'; import ExtensibleAreaStateManager from './extensible-areas/manager'; import PluginDataChannelManager from './data-channel/manager'; @@ -18,34 +18,38 @@ import PluginDomElementManipulationManager from './dom-element-manipulation/mana import PluginServerCommandsHandler from './server-commands/handler'; import PluginLearningAnalyticsDashboardManager from './learning-analytics-dashboard/manager'; -const PluginsEngineManager = () => { +const PluginsEngineManager = (props: PluginsEngineManagerProps) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - temporary, while meteor exists in the project - const PLUGINS_CONFIG = window.meetingClientSettings.public.plugins; + const { pluginConfig } = props; // If there is no plugin to load, the engine simply returns null - if (!PLUGINS_CONFIG) return null; const containerRef = useRef(null); const [lastLoadedPlugin, setLastLoadedPlugin] = useState(); - const loadedPlugins = useRef(0); + const [effectivePluginsConfig, setEffectivePluginsConfig] = useState(); + const [numberOfLoadedPlugins, setNumberOfLoadedPlugins] = useState(0); - const effectivePluginsConfig: EffectivePluginConfig[] = useMemo( - () => PLUGINS_CONFIG.map((p: PluginConfig) => ({ - ...p, - uuid: uuidLib.v4(), - } as EffectivePluginConfig)), [ - PLUGINS_CONFIG, - ], - ); + useEffect(() => { + setEffectivePluginsConfig( + pluginConfig?.map((p) => ({ + ...p, + name: p.name, + url: p.javascriptEntrypointUrl, + uuid: uuidLib.v4(), + } as EffectivePluginConfig)), + ); + }, [ + pluginConfig, + ]); - const totalNumberOfPlugins = PLUGINS_CONFIG?.length; + const totalNumberOfPlugins = pluginConfig?.length; window.React = React; useEffect(() => { - logger.info(`${loadedPlugins.current}/${totalNumberOfPlugins} plugins loaded`); + if (totalNumberOfPlugins) logger.info(`${numberOfLoadedPlugins}/${totalNumberOfPlugins} plugins loaded`); }, - [loadedPlugins.current, lastLoadedPlugin]); + [numberOfLoadedPlugins, lastLoadedPlugin]); return ( <> @@ -59,7 +63,7 @@ const PluginsEngineManager = () => { { - effectivePluginsConfig.map((effectivePluginConfig: EffectivePluginConfig) => { + effectivePluginsConfig?.map((effectivePluginConfig: EffectivePluginConfig) => { const { uuid, name: pluginName } = effectivePluginConfig; const pluginApi: PluginSdk.PluginApi = BbbPluginSdk.getPluginApi(uuid, pluginName); return ( @@ -68,7 +72,7 @@ const PluginsEngineManager = () => { {...{ uuid, containerRef, - loadedPlugins, + setNumberOfLoadedPlugins, setLastLoadedPlugin, pluginConfig: effectivePluginConfig, }} diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts new file mode 100644 index 0000000000..8da1c0e52d --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; + +const PLUGIN_CONFIGURATION_QUERY = gql`query PluginConfigurationQuery { + plugin { + name, + javascriptEntrypointUrl, + } +}`; + +export default PLUGIN_CONFIGURATION_QUERY; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/styles.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/styles.ts index 45680b5b62..5333d82962 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/styles.ts @@ -2,10 +2,10 @@ import styled from 'styled-components'; // eslint-disable-next-line import/prefer-default-export export const PluginsEngine = styled.div` - position: 'absolute', - top: 0, - left: 0, - width: '100vw', - height: '100vh', - zIndex: -1, + position: 'absolute'; + top: 0; + left: 0; + width: '100vw'; + height: '100vh'; + z-index: -1; `; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts index 42fae08f88..fbec42eb72 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts @@ -4,6 +4,15 @@ export interface PluginsEngineComponentProps { containerRef: React.RefObject; } +export interface PluginConfigFromGraphql { + javascriptEntrypointUrl: string; + name: string; +} + +export interface PluginsEngineManagerProps { + pluginConfig: PluginConfigFromGraphql[] | undefined; +} + export interface PluginConfig { name: string; url: string; From b77a6ae483da18b7bd791d94032986663d9ef685 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Tue, 8 Oct 2024 14:32:01 -0300 Subject: [PATCH 02/13] [new-server-side-architecture] - Changes in review --- .../scala/org/bigbluebutton/core/db/MeetingDAO.scala | 4 +++- .../scala/org/bigbluebutton/core/models/Plugins.scala | 8 +++++++- .../org/bigbluebutton/core/running/MeetingActor.scala | 3 +-- .../bigbluebutton/core/running/RunningMeeting.scala | 8 +------- .../java/org/bigbluebutton/api/MeetingService.java | 10 +++++----- .../org/bigbluebutton/api/ParamsProcessorUtil.java | 8 +++++++- 6 files changed, 24 insertions(+), 17 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/MeetingDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/MeetingDAO.scala index 7b20dc3e14..e148332a6a 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/MeetingDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/MeetingDAO.scala @@ -3,6 +3,7 @@ package org.bigbluebutton.core.db import org.bigbluebutton.common2.domain.DefaultProps import PostgresProfile.api._ import org.bigbluebutton.core.apps.groupchats.GroupChatApp +import org.bigbluebutton.core.models.PluginModel case class MeetingSystemColumnsDbModel( loginUrl: Option[String], @@ -85,7 +86,7 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet } object MeetingDAO { - def insert(meetingProps: DefaultProps, clientSettings: Map[String, Object]) = { + def insert(meetingProps: DefaultProps, clientSettings: Map[String, Object], pluginProps: PluginModel) = { DatabaseConnection.enqueue( TableQuery[MeetingDbTableDef].forceInsert( MeetingDbModel( @@ -148,6 +149,7 @@ object MeetingDAO { MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps) LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout) MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings)) + PluginModel.persistPluginsForClient(pluginProps, meetingProps.meetingProp.intId) } def updateMeetingDurationByParentMeeting(parentMeetingId: String, newDurationInSeconds: Int) = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala index af7bdbb61e..227d539764 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala @@ -1,7 +1,10 @@ package org.bigbluebutton.core.models +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.scala.DefaultScalaModule import org.bigbluebutton.common2.util.JsonUtil import org.bigbluebutton.core.db.PluginDAO +import java.util case class RateLimiting( messagesAllowedPerSecond: Int, @@ -47,13 +50,16 @@ case class Plugin( ) object PluginModel { + val objectMapper: ObjectMapper = new ObjectMapper() + objectMapper.registerModule(new DefaultScalaModule()) def getPluginByName(instance: PluginModel, pluginName: String): Option[Plugin] = { instance.plugins.get(pluginName) } def getPlugins(instance: PluginModel): Map[String, Plugin] = { instance.plugins } - def createPluginModelFromJson(jsonString: String): PluginModel = { + def createPluginModelFromJson(json: util.Map[String, AnyRef]): PluginModel = { + val jsonString = objectMapper.writeValueAsString(json) val instance = new PluginModel() instance.plugins = JsonUtil.fromJson[Map[String, Plugin]](jsonString).getOrElse(Map()) instance diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index fa823a2074..c307381f14 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -172,8 +172,7 @@ class MeetingActor( outGW.send(msgEvent) //Insert meeting into the database - MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings) - PluginModel.persistPluginsForClient(liveMeeting.plugins, props.meetingProp.intId) + MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings, liveMeeting.plugins) // Create a default public group chat state = groupChatApp.handleCreateDefaultPublicGroupChat(state, liveMeeting, msgBus) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala index 3ae0031595..96bdef6a97 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala @@ -1,16 +1,12 @@ package org.bigbluebutton.core.running -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.scala.DefaultScalaModule import org.apache.pekko.actor.ActorContext import org.bigbluebutton.ClientSettings -import org.bigbluebutton.ClientSettings.{getConfigPropertyValueByPathAsBooleanOrElse, getConfigPropertyValueByPathAsStringOrElse, logger} import org.bigbluebutton.common2.domain.DefaultProps import org.bigbluebutton.core.apps._ import org.bigbluebutton.core.bus._ import org.bigbluebutton.core.models._ import org.bigbluebutton.core.OutMessageGateway -import org.bigbluebutton.core.models.PluginModel.getPlugins import org.bigbluebutton.core2.MeetingStatus2x object RunningMeeting { @@ -21,11 +17,9 @@ object RunningMeeting { class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway, eventBus: InternalEventBus)(implicit val context: ActorContext) { - val objectMapper: ObjectMapper = new ObjectMapper(); - objectMapper.registerModule(new DefaultScalaModule()) private val externalVideoModel = new ExternalVideoModel() private val chatModel = new ChatModel() - private val plugins = PluginModel.createPluginModelFromJson(objectMapper.writeValueAsString(props.pluginProp)) + private val plugins = PluginModel.createPluginModelFromJson(props.pluginProp) private val layouts = new Layouts() private val pads = new Pads() private val wbModel = new WhiteboardModel() diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 7b8dcdf829..3c7014298e 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -362,8 +362,8 @@ public class MeetingService implements MessageListener { while (matcher.find()) { String variableName = matcher.group(1); - if (variableName.length() > 5){ - // Remove "meta_" and turn everything lower case + if (variableName.startsWith("meta_") && variableName.length() > 5) { + // Remove "meta_" and convert to lower case variableName = variableName.substring(5).toLowerCase(); } else { throw new NoSuchFieldException("Metadata " + variableName + " is malformed, please provide a valid one"); @@ -375,7 +375,7 @@ public class MeetingService implements MessageListener { else throw new NoSuchFieldException("Metadata " + variableName + " not found in URL parameters"); // Replace the placeholder with the value from the map - matcher.appendReplacement(result, replacement); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); } matcher.appendTail(result); @@ -413,9 +413,9 @@ public class MeetingService implements MessageListener { String manifestContent = replaceMetaParametersIntoManifestTemplate(content.toString(), metadata); ObjectMapper mapper = new ObjectMapper(); - Map MappedManifestContent = mapper.readValue(manifestContent, new TypeReference<>() {}); + Map mappedManifestContent = mapper.readValue(manifestContent, new TypeReference<>() {}); - manifestObject.put("content", MappedManifestContent); + manifestObject.put("content", mappedManifestContent); Map manifestWrapper = new HashMap(); manifestWrapper.put( "manifest", manifestObject diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index d2d0ecddff..a4e3d7d883 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -26,6 +26,8 @@ import java.nio.charset.StandardCharsets; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; + import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -544,7 +546,11 @@ public class ParamsProcessorUtil { ArrayList listOfPlugins = new ArrayList(); if (!StringUtils.isEmpty(params.get(ApiParams.PLUGINS))) { String pluginsParam = params.get(ApiParams.PLUGINS); - listOfPlugins.addAll(Arrays.asList(pluginsParam.split(","))); + listOfPlugins.addAll(Arrays.stream(pluginsParam.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList())); + } // Check Disabled Features Exclude list -- passed as a CREATE parameter to cancel the disabling (typically from bbb-web's properties file) From 6756eef841adef392fd02b71958b6fc5be391ba2 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Tue, 8 Oct 2024 17:46:36 -0300 Subject: [PATCH 03/13] [new-server-side-architecture] - Fix case where some properties were not correctly passed to manifest.json --- .../PluginDataChannelDeleteEntryMsgHdlr.scala | 2 +- .../PluginDataChannelPushEntryMsgHdlr.scala | 2 +- ...PluginDataChannelReplaceEntryMsgHdlr.scala | 2 +- .../PluginDataChannelResetMsgHdlr.scala | 2 +- .../bigbluebutton/core/models/Plugins.scala | 27 +++++++++++++------ 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala index a5a97212d7..c321c6dbf5 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala @@ -18,7 +18,7 @@ trait PluginDataChannelDeleteEntryMsgHdlr extends HandlerHelpers { } yield { PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { case Some(p) => - p.manifest.content.dataChannels.find(dc => dc.name == msg.body.channelName) match { + p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { case Some(dc) => val hasPermission = for { replaceOrDeletePermission <- dc.replaceOrDeletePermission diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala index 97d79fa3f4..5ac622881d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala @@ -18,7 +18,7 @@ trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers { } yield { PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { case Some(p) => - p.manifest.content.dataChannels.find(dc => dc.name == msg.body.channelName) match { + p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { case Some(dc) => val hasPermission = for { pushPermission <- dc.pushPermission diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala index 63ccbe619e..edbdd0ee02 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala @@ -18,7 +18,7 @@ trait PluginDataChannelReplaceEntryMsgHdlr extends HandlerHelpers { } yield { PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { case Some(p) => - p.manifest.content.dataChannels.find(dc => dc.name == msg.body.channelName) match { + p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { case Some(dc) => val hasPermission = for { replaceOrDeletePermission <- dc.replaceOrDeletePermission diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala index 05002f28fa..27e6a23d88 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala @@ -19,7 +19,7 @@ trait PluginDataChannelResetMsgHdlr extends HandlerHelpers { } yield { PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { case Some(p) => - p.manifest.content.dataChannels.find(dc => dc.name == msg.body.channelName) match { + p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { case Some(dc) => val hasPermission = for { replaceOrDeletePermission <- dc.replaceOrDeletePermission diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala index 227d539764..116f8a8805 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala @@ -1,9 +1,11 @@ package org.bigbluebutton.core.models -import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.annotation.{ JsonIgnoreProperties, JsonProperty } +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.{ JsonMappingException, ObjectMapper } import com.fasterxml.jackson.module.scala.DefaultScalaModule -import org.bigbluebutton.common2.util.JsonUtil import org.bigbluebutton.core.db.PluginDAO + import java.util case class RateLimiting( @@ -34,10 +36,10 @@ case class PluginManifestContent( requiredSdkVersion: String, name: String, javascriptEntrypointUrl: String, - localesBaseUrl: String, - eventPersistence: EventPersistence, - dataChannels: List[DataChannel], - remoteDataSources: List[RemoteDataSource] + localesBaseUrl: Option[String] = None, + eventPersistence: Option[EventPersistence] = None, + dataChannels: Option[List[DataChannel]] = None, + remoteDataSources: Option[List[RemoteDataSource]] = None ) case class PluginManifest( @@ -59,9 +61,18 @@ object PluginModel { instance.plugins } def createPluginModelFromJson(json: util.Map[String, AnyRef]): PluginModel = { - val jsonString = objectMapper.writeValueAsString(json) val instance = new PluginModel() - instance.plugins = JsonUtil.fromJson[Map[String, Plugin]](jsonString).getOrElse(Map()) + var pluginsMap: Map[String, Plugin] = Map.empty[String, Plugin] + json.forEach { case (pluginName, plugin) => + try { + val pluginObject = objectMapper.readValue(objectMapper.writeValueAsString(plugin), classOf[Plugin]) + pluginsMap = pluginsMap + (pluginName -> pluginObject) + } catch { + case err @ (_: JsonProcessingException | _: JsonMappingException) => println("Error while processing plugin " + + pluginName + ": ", err) + } + } + instance.plugins = pluginsMap instance } def persistPluginsForClient(instance: PluginModel, meetingId: String): Unit = { From 04a5a9da9f8dff5be36e8c7102e846286104c62a Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 9 Oct 2024 16:36:32 -0300 Subject: [PATCH 04/13] [plugin-sdk-issue-121] - Added checksum logic to both the manifest and the js bundled plugin --- .../org/bigbluebutton/core/db/PluginDAO.scala | 7 ++- .../bigbluebutton/core/models/Plugins.scala | 19 ++++---- .../java/org/bigbluebutton/api/ApiParams.java | 2 +- .../org/bigbluebutton/api/MeetingService.java | 35 +++++++++++---- .../api/ParamsProcessorUtil.java | 45 ++++++++++++------- .../org/bigbluebutton/api/domain/Meeting.java | 14 +++--- .../api/domain/PluginsManifest.java | 35 +++++++++++++++ bbb-graphql-server/bbb_schema.sql | 1 + .../BigBlueButton/tables/public_v_plugin.yaml | 2 + .../plugins-engine/loader/manager.tsx | 4 +- .../ui/components/plugins-engine/query.ts | 1 + .../ui/components/plugins-engine/types.ts | 2 +- 12 files changed, 122 insertions(+), 45 deletions(-) create mode 100644 bbb-common-web/src/main/java/org/bigbluebutton/api/domain/PluginsManifest.java diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala index 4549ea158a..6a05246942 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala @@ -6,23 +6,26 @@ case class PluginDbModel( meetingId: String, name: String, javascriptEntrypointUrl: String, + pluginJavascriptChecksum: String, ) class PluginDbTableDef(tag: Tag) extends Table[PluginDbModel](tag, None, "plugin") { val meetingId = column[String]("meetingId", O.PrimaryKey) val name = column[String]("name", O.PrimaryKey) val javascriptEntrypointUrl = column[String]("javascriptEntrypointUrl") - override def * = (meetingId, name, javascriptEntrypointUrl) <> (PluginDbModel.tupled, PluginDbModel.unapply) + val pluginJavascriptChecksum = column[String]("pluginJavascriptChecksum") + override def * = (meetingId, name, javascriptEntrypointUrl, pluginJavascriptChecksum) <> (PluginDbModel.tupled, PluginDbModel.unapply) } object PluginDAO { - def insert(meetingId: String, name: String, javascriptEntrypointUrl: String) = { + def insert(meetingId: String, name: String, javascriptEntrypointUrl: String, pluginJavascriptChecksum: String) = { DatabaseConnection.enqueue( TableQuery[PluginDbTableDef].forceInsert( PluginDbModel( meetingId = meetingId, name = name, javascriptEntrypointUrl = javascriptEntrypointUrl, + pluginJavascriptChecksum = pluginJavascriptChecksum, ) ) ) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala index 116f8a8805..dd0eaf2df5 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala @@ -33,13 +33,15 @@ case class RemoteDataSource( ) case class PluginManifestContent( - requiredSdkVersion: String, - name: String, - javascriptEntrypointUrl: String, - localesBaseUrl: Option[String] = None, - eventPersistence: Option[EventPersistence] = None, - dataChannels: Option[List[DataChannel]] = None, - remoteDataSources: Option[List[RemoteDataSource]] = None + requiredSdkVersion: String, + name: String, + javascriptEntrypointUrl: String, + checksum: Option[String] = None, + pluginJavascriptChecksum: Option[String] = None, + localesBaseUrl: Option[String] = None, + eventPersistence: Option[EventPersistence] = None, + dataChannels: Option[List[DataChannel]] = None, + remoteDataSources: Option[List[RemoteDataSource]] = None ) case class PluginManifest( @@ -77,7 +79,8 @@ object PluginModel { } def persistPluginsForClient(instance: PluginModel, meetingId: String): Unit = { instance.plugins.foreach { case (_, plugin) => - PluginDAO.insert(meetingId, plugin.manifest.content.name, plugin.manifest.content.javascriptEntrypointUrl) + PluginDAO.insert(meetingId, plugin.manifest.content.name, plugin.manifest.content.javascriptEntrypointUrl, + plugin.manifest.content.pluginJavascriptChecksum.getOrElse("")) } } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java index e4128661d2..b15ab435fc 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java @@ -73,7 +73,7 @@ public class ApiParams { public static final String ROLE = "role"; public static final String GROUPS = "groups"; public static final String DISABLED_FEATURES = "disabledFeatures"; - public static final String PLUGINS = "plugins"; + public static final String PLUGINS_MANIFESTS = "pluginsManifests"; public static final String DISABLED_FEATURES_EXCLUDE = "disabledFeaturesExclude"; public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn"; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 3c7014298e..154c911be1 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -19,10 +19,8 @@ package org.bigbluebutton.api; import java.io.File; -import java.math.BigInteger; import java.net.URI; import java.net.URL; -import java.security.MessageDigest; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.BlockingQueue; @@ -49,7 +47,6 @@ import org.bigbluebutton.api.messaging.messages.*; import org.bigbluebutton.api2.IBbbWebApiGWApp; import org.bigbluebutton.api2.domain.UploadedTrack; import org.bigbluebutton.common2.redis.RedisStorageService; -import org.bigbluebutton.common2.util.JsonUtil; import org.bigbluebutton.presentation.PresentationUrlDownloadService; import org.bigbluebutton.presentation.imp.SlidesGenerationProgressNotifier; import org.bigbluebutton.web.services.UserCleanupTimerTask; @@ -386,9 +383,11 @@ public class MeetingService implements MessageListener { Map metadata = m.getMetadata(); // Fetch content for each URL and store in the map - for (String manifestUrl : m.getPluginManifestUrls()) { + for (PluginsManifest pluginsManifest : m.getPluginsManifests()) { try { - URL url = new URL(manifestUrl); + + String urlString = pluginsManifest.getUrl(); + URL url = new URL(urlString); BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); StringBuilder content = new StringBuilder(); String inputLine; @@ -398,18 +397,36 @@ public class MeetingService implements MessageListener { } in.close(); - // Parse the JSON content and get the "name" field + // Parse the JSON content JsonNode jsonNode = objectMapper.readTree(content.toString()); + + // Validate checksum if any: + String checksum = pluginsManifest.getChecksum(); + if (jsonNode.has("checksum") || !StringUtils.isEmpty(checksum)) { + if (!checksum.equals(jsonNode.get("checksum").asText())) { + log.info("Plugin checksum mismatch for {}. Manifest.json returned {} and URL parameter input was {}", + pluginsManifest.getUrl(), + jsonNode.get("checksum").asText(), + checksum + ); + log.info("Plugin {} is not going to be loaded", + pluginsManifest.getUrl() + ); + continue; + } + } + + // Get the "name" field String name; if (jsonNode.has("name")) { name = jsonNode.get("name").asText(); } else { - throw new NoSuchFieldException("For url " + manifestUrl + "there is no name field configured."); + throw new NoSuchFieldException("For url " + urlString + "there is no name field configured."); } String pluginKey = name; HashMap manifestObject = new HashMap<>(); - manifestObject.put("url", manifestUrl); + manifestObject.put("url", urlString); String manifestContent = replaceMetaParametersIntoManifestTemplate(content.toString(), metadata); ObjectMapper mapper = new ObjectMapper(); @@ -423,7 +440,7 @@ public class MeetingService implements MessageListener { urlContents.put(pluginKey, manifestWrapper); } catch(Exception e) { log.error("Failed with the following plugin manifest URL: {}. Error: {} therefore this plugin will not load", - manifestUrl, e); + pluginsManifest.getUrl(), e); } } return urlContents; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index a4e3d7d883..aa663b8358 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -26,12 +26,12 @@ import java.nio.charset.StandardCharsets; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import org.bigbluebutton.api.domain.*; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.safety.Safelist; @@ -41,10 +41,6 @@ import org.jsoup.select.Elements; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; -import org.bigbluebutton.api.domain.BreakoutRoomsParams; -import org.bigbluebutton.api.domain.LockSettingsParams; -import org.bigbluebutton.api.domain.Meeting; -import org.bigbluebutton.api.domain.Group; import org.bigbluebutton.api.service.ServiceUtils; import org.bigbluebutton.api.util.ParamsUtil; import org.slf4j.Logger; @@ -434,6 +430,33 @@ public class ParamsProcessorUtil { return groups; } + private ArrayList processPluginsManifestsParams(Map params) { + ArrayList pluginsManifests = new ArrayList(); + String pluginsManifestParams = params.get(ApiParams.PLUGINS_MANIFESTS); + if (!StringUtils.isEmpty(pluginsManifestParams)) { + JsonElement pluginsManifestsJsonElement = new Gson().fromJson(pluginsManifestParams, JsonElement.class); + + if(pluginsManifestsJsonElement != null && pluginsManifestsJsonElement.isJsonArray()) { + JsonArray pluginsManifestsJson = pluginsManifestsJsonElement.getAsJsonArray(); + for (JsonElement pluginsManifestJson : pluginsManifestsJson) { + if(pluginsManifestJson.isJsonObject()) { + JsonObject pluginsManifestJsonObj = pluginsManifestJson.getAsJsonObject(); + if(pluginsManifestJsonObj.has("url")) { + String url = pluginsManifestJsonObj.get("url").getAsString(); + PluginsManifest newPlugin = new PluginsManifest(url); + if(pluginsManifestJsonObj.has("checksum")) { + newPlugin.setChecksum(pluginsManifestJsonObj.get("checksum").getAsString()); + } + pluginsManifests.add(newPlugin); + } + } + } + } + } + + return pluginsManifests; + } + public Meeting processCreateParams(Map params) { String meetingName = params.get(ApiParams.NAME); @@ -543,15 +566,7 @@ public class ParamsProcessorUtil { listOfDisabledFeatures = new ArrayList<>(new HashSet<>(listOfDisabledFeatures)); // Process Plugin Manifest Urls - ArrayList listOfPlugins = new ArrayList(); - if (!StringUtils.isEmpty(params.get(ApiParams.PLUGINS))) { - String pluginsParam = params.get(ApiParams.PLUGINS); - listOfPlugins.addAll(Arrays.stream(pluginsParam.split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .collect(Collectors.toList())); - - } + ArrayList listOfPluginsManifests = processPluginsManifestsParams(params); // Check Disabled Features Exclude list -- passed as a CREATE parameter to cancel the disabling (typically from bbb-web's properties file) ArrayList listOfDisabledFeaturesExclude = new ArrayList<>(); @@ -803,7 +818,7 @@ public class ParamsProcessorUtil { .withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins) .withLearningDashboardAccessToken(learningDashboardAccessToken) .withGroups(groups) - .withPluginManifestUrls(listOfPlugins) + .withPluginManifests(listOfPluginsManifests) .withDisabledFeatures(listOfDisabledFeatures) .withNotifyRecordingIsOn(notifyRecordingIsOn) .withPresentationUploadExternalDescription(presentationUploadExternalDescription) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java index fb501b89b4..d43c0cb0db 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java @@ -79,7 +79,7 @@ public class Meeting { private String defaultAvatarURL; private String defaultWebcamBackgroundURL; private Map plugins; - private ArrayList pluginManifestUrls; + private ArrayList pluginsManifests; private String guestPolicy = GuestPolicy.ASK_MODERATOR; private String guestLobbyMessage = ""; private Map usersWithGuestLobbyMessages; @@ -130,7 +130,7 @@ public class Meeting { extMeetingId = builder.externalId; intMeetingId = builder.internalId; disabledFeatures = builder.disabledFeatures; - pluginManifestUrls = builder.pluginManifestUrls; + pluginsManifests = builder.pluginsManifests; notifyRecordingIsOn = builder.notifyRecordingIsOn; presentationUploadExternalDescription = builder.presentationUploadExternalDescription; presentationUploadExternalUrl = builder.presentationUploadExternalUrl; @@ -448,8 +448,8 @@ public class Meeting { plugins = p; } - public ArrayList getPluginManifestUrls() { - return pluginManifestUrls; + public ArrayList getPluginsManifests() { + return pluginsManifests; } public Boolean getNotifyRecordingIsOn() { @@ -939,7 +939,7 @@ public class Meeting { private int learningDashboardCleanupDelayInMinutes; private String learningDashboardAccessToken; private ArrayList disabledFeatures; - private ArrayList pluginManifestUrls; + private ArrayList pluginsManifests; private Boolean notifyRecordingIsOn; private String presentationUploadExternalDescription; private String presentationUploadExternalUrl; @@ -1074,8 +1074,8 @@ public class Meeting { return this; } - public Builder withPluginManifestUrls(ArrayList map) { - this.pluginManifestUrls = map; + public Builder withPluginManifests(ArrayList map) { + this.pluginsManifests = map; return this; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/PluginsManifest.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/PluginsManifest.java new file mode 100644 index 0000000000..96d570ea6b --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/PluginsManifest.java @@ -0,0 +1,35 @@ +package org.bigbluebutton.api.domain; + +import java.util.Vector; + +public class PluginsManifest { + + private String url = ""; + private String checksum = ""; + public PluginsManifest( + String url, + String checksum) { + this.url = url; + this.checksum = checksum; + } + public PluginsManifest( + String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getChecksum() { + return checksum; + } + + public void setChecksum(String checksum) { + this.checksum = checksum; + } +} \ No newline at end of file diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index 86dcca20bb..932469724a 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1968,6 +1968,7 @@ create table "plugin" ( "meetingId" varchar(100), "name" varchar(100), "javascriptEntrypointUrl" varchar(500), + "pluginJavascriptChecksum" varchar(500), CONSTRAINT "plugin_pk" PRIMARY KEY ("meetingId","name"), FOREIGN KEY ("meetingId") REFERENCES "meeting"("meetingId") ON DELETE CASCADE ); diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml index 857e370880..341c5a15b8 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml @@ -10,6 +10,7 @@ select_permissions: - role: bbb_client permission: columns: + - pluginJavascriptChecksum - javascriptEntrypointUrl - name filter: @@ -19,6 +20,7 @@ select_permissions: - role: bbb_client_not_in_meeting permission: columns: + - pluginJavascriptChecksum - javascriptEntrypointUrl - name filter: diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx index 30c131be3e..f29fdaee02 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx @@ -40,8 +40,8 @@ const PluginLoaderManager = (props: PluginLoaderManagerProps) => { script.src = plugin.url; script.setAttribute('uuid', div.id); script.setAttribute('pluginName', plugin.name); - if (plugin.checksum) { - script.setAttribute('integrity', plugin.checksum); + if (plugin.pluginJavascriptChecksum) { + script.setAttribute('integrity', plugin.pluginJavascriptChecksum); } document.head.appendChild(script); }, [plugin, containerRef]); diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts index 8da1c0e52d..5718313ebd 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts @@ -4,6 +4,7 @@ const PLUGIN_CONFIGURATION_QUERY = gql`query PluginConfigurationQuery { plugin { name, javascriptEntrypointUrl, + pluginJavascriptChecksum, } }`; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts index fbec42eb72..6e872b511b 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts @@ -16,7 +16,7 @@ export interface PluginsEngineManagerProps { export interface PluginConfig { name: string; url: string; - checksum?: string; + pluginJavascriptChecksum?: string; } export interface EffectivePluginConfig extends PluginConfig { From f375d4d3aa5ca0ed52e60bea65d1b2f5367d218d Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 9 Oct 2024 17:40:41 -0300 Subject: [PATCH 05/13] [plugin-sdk-issue-121] - change naming for jsEntrypointchecksum --- .../org/bigbluebutton/core/db/PluginDAO.scala | 10 +++++----- .../bigbluebutton/core/models/Plugins.scala | 19 +++++++++--------- .../org/bigbluebutton/api/MeetingService.java | 20 +++++++++++-------- bbb-graphql-server/bbb_schema.sql | 2 +- .../BigBlueButton/tables/public_v_plugin.yaml | 4 ++-- .../plugins-engine/loader/manager.tsx | 4 ++-- .../ui/components/plugins-engine/query.ts | 2 +- .../ui/components/plugins-engine/types.ts | 2 +- 8 files changed, 33 insertions(+), 30 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala index 6a05246942..99493c100f 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala @@ -6,26 +6,26 @@ case class PluginDbModel( meetingId: String, name: String, javascriptEntrypointUrl: String, - pluginJavascriptChecksum: String, + javascriptEntrypointChecksum: String ) class PluginDbTableDef(tag: Tag) extends Table[PluginDbModel](tag, None, "plugin") { val meetingId = column[String]("meetingId", O.PrimaryKey) val name = column[String]("name", O.PrimaryKey) val javascriptEntrypointUrl = column[String]("javascriptEntrypointUrl") - val pluginJavascriptChecksum = column[String]("pluginJavascriptChecksum") - override def * = (meetingId, name, javascriptEntrypointUrl, pluginJavascriptChecksum) <> (PluginDbModel.tupled, PluginDbModel.unapply) + val javascriptEntrypointChecksum = column[String]("javascriptEntrypointChecksum") + override def * = (meetingId, name, javascriptEntrypointUrl, javascriptEntrypointChecksum) <> (PluginDbModel.tupled, PluginDbModel.unapply) } object PluginDAO { - def insert(meetingId: String, name: String, javascriptEntrypointUrl: String, pluginJavascriptChecksum: String) = { + def insert(meetingId: String, name: String, javascriptEntrypointUrl: String, javascriptEntrypointChecksum: String) = { DatabaseConnection.enqueue( TableQuery[PluginDbTableDef].forceInsert( PluginDbModel( meetingId = meetingId, name = name, javascriptEntrypointUrl = javascriptEntrypointUrl, - pluginJavascriptChecksum = pluginJavascriptChecksum, + javascriptEntrypointChecksum = javascriptEntrypointChecksum, ) ) ) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala index dd0eaf2df5..a233e3d9f2 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala @@ -33,15 +33,14 @@ case class RemoteDataSource( ) case class PluginManifestContent( - requiredSdkVersion: String, - name: String, - javascriptEntrypointUrl: String, - checksum: Option[String] = None, - pluginJavascriptChecksum: Option[String] = None, - localesBaseUrl: Option[String] = None, - eventPersistence: Option[EventPersistence] = None, - dataChannels: Option[List[DataChannel]] = None, - remoteDataSources: Option[List[RemoteDataSource]] = None + requiredSdkVersion: String, + name: String, + javascriptEntrypointUrl: String, + javascriptEntrypointChecksum: Option[String] = None, + localesBaseUrl: Option[String] = None, + eventPersistence: Option[EventPersistence] = None, + dataChannels: Option[List[DataChannel]] = None, + remoteDataSources: Option[List[RemoteDataSource]] = None ) case class PluginManifest( @@ -80,7 +79,7 @@ object PluginModel { def persistPluginsForClient(instance: PluginModel, meetingId: String): Unit = { instance.plugins.foreach { case (_, plugin) => PluginDAO.insert(meetingId, plugin.manifest.content.name, plugin.manifest.content.javascriptEntrypointUrl, - plugin.manifest.content.pluginJavascriptChecksum.getOrElse("")) + plugin.manifest.content.javascriptEntrypointChecksum.getOrElse("")) } } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 154c911be1..a28d4bb15e 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -19,8 +19,11 @@ package org.bigbluebutton.api; import java.io.File; +import java.io.InputStream; import java.net.URI; import java.net.URL; +import java.security.DigestInputStream; +import java.security.MessageDigest; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.BlockingQueue; @@ -34,6 +37,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.JsonObject; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.bigbluebutton.api.domain.*; @@ -401,13 +405,12 @@ public class MeetingService implements MessageListener { JsonNode jsonNode = objectMapper.readTree(content.toString()); // Validate checksum if any: - String checksum = pluginsManifest.getChecksum(); - if (jsonNode.has("checksum") || !StringUtils.isEmpty(checksum)) { - if (!checksum.equals(jsonNode.get("checksum").asText())) { - log.info("Plugin checksum mismatch for {}. Manifest.json returned {} and URL parameter input was {}", - pluginsManifest.getUrl(), - jsonNode.get("checksum").asText(), - checksum + String paramChecksum = pluginsManifest.getChecksum(); + if (!StringUtils.isEmpty(paramChecksum)) { + String hash = DigestUtils.sha1Hex(content.toString()); + if (!paramChecksum.equals(hash)) { + log.info("Plugin's manifest.json checksum mismatch with that of the URL parameter for {}.", + pluginsManifest.getUrl() ); log.info("Plugin {} is not going to be loaded", pluginsManifest.getUrl() @@ -439,8 +442,9 @@ public class MeetingService implements MessageListener { ); urlContents.put(pluginKey, manifestWrapper); } catch(Exception e) { - log.error("Failed with the following plugin manifest URL: {}. Error: {} therefore this plugin will not load", + log.info("Failed with the following plugin manifest URL: {}. Error: ", pluginsManifest.getUrl(), e); + log.info("Therefore this plugin will not be loaded"); } } return urlContents; diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index 932469724a..b26da4a089 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1968,7 +1968,7 @@ create table "plugin" ( "meetingId" varchar(100), "name" varchar(100), "javascriptEntrypointUrl" varchar(500), - "pluginJavascriptChecksum" varchar(500), + "javascriptEntrypointChecksum" varchar(500), CONSTRAINT "plugin_pk" PRIMARY KEY ("meetingId","name"), FOREIGN KEY ("meetingId") REFERENCES "meeting"("meetingId") ON DELETE CASCADE ); diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml index 341c5a15b8..25820baa9f 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml @@ -10,7 +10,7 @@ select_permissions: - role: bbb_client permission: columns: - - pluginJavascriptChecksum + - javascriptEntrypointChecksum - javascriptEntrypointUrl - name filter: @@ -20,7 +20,7 @@ select_permissions: - role: bbb_client_not_in_meeting permission: columns: - - pluginJavascriptChecksum + - javascriptEntrypointChecksum - javascriptEntrypointUrl - name filter: diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx index f29fdaee02..a2d5bf8359 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx @@ -40,8 +40,8 @@ const PluginLoaderManager = (props: PluginLoaderManagerProps) => { script.src = plugin.url; script.setAttribute('uuid', div.id); script.setAttribute('pluginName', plugin.name); - if (plugin.pluginJavascriptChecksum) { - script.setAttribute('integrity', plugin.pluginJavascriptChecksum); + if (plugin.javascriptEntrypointChecksum) { + script.setAttribute('integrity', plugin.javascriptEntrypointChecksum); } document.head.appendChild(script); }, [plugin, containerRef]); diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts index 5718313ebd..b347ec1c39 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts @@ -4,7 +4,7 @@ const PLUGIN_CONFIGURATION_QUERY = gql`query PluginConfigurationQuery { plugin { name, javascriptEntrypointUrl, - pluginJavascriptChecksum, + javascriptEntrypointChecksum, } }`; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts index 6e872b511b..b1001c2816 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts @@ -16,7 +16,7 @@ export interface PluginsEngineManagerProps { export interface PluginConfig { name: string; url: string; - pluginJavascriptChecksum?: string; + javascriptEntrypointChecksum?: string; } export interface EffectivePluginConfig extends PluginConfig { From a590d15f7cc817274a4c2ce0ded07078fd11d9cb Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 10 Oct 2024 15:49:16 -0300 Subject: [PATCH 06/13] [new-server-side-architecture] - replace checksum algorithm of manifest.json to be sha256 and renamed javascriptEntrypointChecksum to javascriptEntrypointIntegrity --- .../scala/org/bigbluebutton/core/db/PluginDAO.scala | 10 +++++----- .../java/org/bigbluebutton/api/MeetingService.java | 2 +- bbb-graphql-server/bbb_schema.sql | 2 +- .../BigBlueButton/tables/public_v_plugin.yaml | 4 ++-- .../ui/components/plugins-engine/loader/manager.tsx | 4 ++-- .../imports/ui/components/plugins-engine/query.ts | 2 +- .../imports/ui/components/plugins-engine/types.ts | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala index 99493c100f..dc7fa869f9 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala @@ -6,26 +6,26 @@ case class PluginDbModel( meetingId: String, name: String, javascriptEntrypointUrl: String, - javascriptEntrypointChecksum: String + javascriptEntrypointIntegrity: String ) class PluginDbTableDef(tag: Tag) extends Table[PluginDbModel](tag, None, "plugin") { val meetingId = column[String]("meetingId", O.PrimaryKey) val name = column[String]("name", O.PrimaryKey) val javascriptEntrypointUrl = column[String]("javascriptEntrypointUrl") - val javascriptEntrypointChecksum = column[String]("javascriptEntrypointChecksum") - override def * = (meetingId, name, javascriptEntrypointUrl, javascriptEntrypointChecksum) <> (PluginDbModel.tupled, PluginDbModel.unapply) + val javascriptEntrypointIntegrity = column[String]("javascriptEntrypointIntegrity") + override def * = (meetingId, name, javascriptEntrypointUrl, javascriptEntrypointIntegrity) <> (PluginDbModel.tupled, PluginDbModel.unapply) } object PluginDAO { - def insert(meetingId: String, name: String, javascriptEntrypointUrl: String, javascriptEntrypointChecksum: String) = { + def insert(meetingId: String, name: String, javascriptEntrypointUrl: String, javascriptEntrypointIntegrity: String) = { DatabaseConnection.enqueue( TableQuery[PluginDbTableDef].forceInsert( PluginDbModel( meetingId = meetingId, name = name, javascriptEntrypointUrl = javascriptEntrypointUrl, - javascriptEntrypointChecksum = javascriptEntrypointChecksum, + javascriptEntrypointIntegrity = javascriptEntrypointIntegrity, ) ) ) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index a28d4bb15e..86507920d1 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -407,7 +407,7 @@ public class MeetingService implements MessageListener { // Validate checksum if any: String paramChecksum = pluginsManifest.getChecksum(); if (!StringUtils.isEmpty(paramChecksum)) { - String hash = DigestUtils.sha1Hex(content.toString()); + String hash = DigestUtils.sha256Hex(content.toString()); if (!paramChecksum.equals(hash)) { log.info("Plugin's manifest.json checksum mismatch with that of the URL parameter for {}.", pluginsManifest.getUrl() diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index b26da4a089..d2baad5495 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1968,7 +1968,7 @@ create table "plugin" ( "meetingId" varchar(100), "name" varchar(100), "javascriptEntrypointUrl" varchar(500), - "javascriptEntrypointChecksum" varchar(500), + "javascriptEntrypointIntegrity" varchar(500), CONSTRAINT "plugin_pk" PRIMARY KEY ("meetingId","name"), FOREIGN KEY ("meetingId") REFERENCES "meeting"("meetingId") ON DELETE CASCADE ); diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml index 25820baa9f..2862b820dd 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml @@ -10,7 +10,7 @@ select_permissions: - role: bbb_client permission: columns: - - javascriptEntrypointChecksum + - javascriptEntrypointIntegrity - javascriptEntrypointUrl - name filter: @@ -20,7 +20,7 @@ select_permissions: - role: bbb_client_not_in_meeting permission: columns: - - javascriptEntrypointChecksum + - javascriptEntrypointIntegrity - javascriptEntrypointUrl - name filter: diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx index a2d5bf8359..8b49379485 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/loader/manager.tsx @@ -40,8 +40,8 @@ const PluginLoaderManager = (props: PluginLoaderManagerProps) => { script.src = plugin.url; script.setAttribute('uuid', div.id); script.setAttribute('pluginName', plugin.name); - if (plugin.javascriptEntrypointChecksum) { - script.setAttribute('integrity', plugin.javascriptEntrypointChecksum); + if (plugin.javascriptEntrypointIntegrity) { + script.setAttribute('integrity', plugin.javascriptEntrypointIntegrity); } document.head.appendChild(script); }, [plugin, containerRef]); diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts index b347ec1c39..8d383fbf56 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts @@ -4,7 +4,7 @@ const PLUGIN_CONFIGURATION_QUERY = gql`query PluginConfigurationQuery { plugin { name, javascriptEntrypointUrl, - javascriptEntrypointChecksum, + javascriptEntrypointIntegrity, } }`; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts index b1001c2816..8e3c032ca5 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts @@ -16,7 +16,7 @@ export interface PluginsEngineManagerProps { export interface PluginConfig { name: string; url: string; - javascriptEntrypointChecksum?: string; + javascriptEntrypointIntegrity?: string; } export interface EffectivePluginConfig extends PluginConfig { From 72adc94a9b973950ab60d5753c6fde8261232c20 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 10 Oct 2024 17:22:27 -0300 Subject: [PATCH 07/13] [new-server-side-architecture] - code rabbit suggestions --- .../bigbluebutton/core/models/Plugins.scala | 18 +++++++++--------- .../org/bigbluebutton/api/MeetingService.java | 18 ++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala index a233e3d9f2..b2431bc87e 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala @@ -33,14 +33,14 @@ case class RemoteDataSource( ) case class PluginManifestContent( - requiredSdkVersion: String, - name: String, - javascriptEntrypointUrl: String, - javascriptEntrypointChecksum: Option[String] = None, - localesBaseUrl: Option[String] = None, - eventPersistence: Option[EventPersistence] = None, - dataChannels: Option[List[DataChannel]] = None, - remoteDataSources: Option[List[RemoteDataSource]] = None + requiredSdkVersion: String, + name: String, + javascriptEntrypointUrl: String, + javascriptEntrypointIntegrity: Option[String] = None, + localesBaseUrl: Option[String] = None, + eventPersistence: Option[EventPersistence] = None, + dataChannels: Option[List[DataChannel]] = None, + remoteDataSources: Option[List[RemoteDataSource]] = None ) case class PluginManifest( @@ -79,7 +79,7 @@ object PluginModel { def persistPluginsForClient(instance: PluginModel, meetingId: String): Unit = { instance.plugins.foreach { case (_, plugin) => PluginDAO.insert(meetingId, plugin.manifest.content.name, plugin.manifest.content.javascriptEntrypointUrl, - plugin.manifest.content.javascriptEntrypointChecksum.getOrElse("")) + plugin.manifest.content.javascriptEntrypointIntegrity.getOrElse("")) } } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 86507920d1..360f73f7c6 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -392,14 +392,13 @@ public class MeetingService implements MessageListener { String urlString = pluginsManifest.getUrl(); URL url = new URL(urlString); - BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); StringBuilder content = new StringBuilder(); - String inputLine; - - while ((inputLine = in.readLine()) != null) { - content.append(inputLine).append("\n"); + try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()))) { + String inputLine; + while ((inputLine = in.readLine()) != null) { + content.append(inputLine).append("\n"); + } } - in.close(); // Parse the JSON content JsonNode jsonNode = objectMapper.readTree(content.toString()); @@ -432,8 +431,7 @@ public class MeetingService implements MessageListener { manifestObject.put("url", urlString); String manifestContent = replaceMetaParametersIntoManifestTemplate(content.toString(), metadata); - ObjectMapper mapper = new ObjectMapper(); - Map mappedManifestContent = mapper.readValue(manifestContent, new TypeReference<>() {}); + Map mappedManifestContent = objectMapper.readValue(manifestContent, new TypeReference<>() {}); manifestObject.put("content", mappedManifestContent); Map manifestWrapper = new HashMap(); @@ -442,9 +440,9 @@ public class MeetingService implements MessageListener { ); urlContents.put(pluginKey, manifestWrapper); } catch(Exception e) { - log.info("Failed with the following plugin manifest URL: {}. Error: ", + log.error("Failed with the following plugin manifest URL: {}. Error: ", pluginsManifest.getUrl(), e); - log.info("Therefore this plugin will not be loaded"); + log.error("Therefore this plugin will not be loaded"); } } return urlContents; From 3951cf6c2f082e0befc7da8de958f25556874d52 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 14 Oct 2024 09:03:37 -0300 Subject: [PATCH 08/13] [new-server-side-architecture] - sonar cloud feedback implementation - removed a possible bug --- .../core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala index edbdd0ee02..3ea288353b 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala @@ -54,8 +54,8 @@ trait PluginDataChannelReplaceEntryMsgHdlr extends HandlerHelpers { ) } case None => println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") - case None => println(s"Plugin '${msg.body.pluginName}' not found.") } + case None => println(s"Plugin '${msg.body.pluginName}' not found.") } } } From 1c3c11afde08882b364d1be837e8a238ba329c29 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 14 Oct 2024 10:42:34 -0300 Subject: [PATCH 09/13] [new-server-side-architecture] - sonar cloud feedback implementation - removed a possible bug --- .../PluginDataChannelDeleteEntryMsgHdlr.scala | 24 +++----------- .../PluginDataChannelPushEntryMsgHdlr.scala | 15 ++------- ...PluginDataChannelReplaceEntryMsgHdlr.scala | 23 ++----------- .../PluginDataChannelResetMsgHdlr.scala | 12 ++----- .../core/apps/plugin/PluginHdlrHelpers.scala | 27 +++++++++++++++ .../common2/msgs/PluginMsgs.scala | 14 ++++++-- .../api/ParamsProcessorUtil.java | 33 ++++++++++--------- 7 files changed, 67 insertions(+), 81 deletions(-) create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrHelpers.scala diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala index c321c6dbf5..9abc3592c0 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala @@ -1,6 +1,7 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.common2.msgs.PluginDataChannelDeleteEntryMsg +import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{ checkPermission, defaultCreatorCheck } import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.models.{ PluginModel, Roles, Users2x } @@ -20,26 +21,9 @@ trait PluginDataChannelDeleteEntryMsgHdlr extends HandlerHelpers { case Some(p) => p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { case Some(dc) => - val hasPermission = for { - replaceOrDeletePermission <- dc.replaceOrDeletePermission - } yield { - replaceOrDeletePermission.toLowerCase match { - case "all" => true - case "moderator" => user.role == Roles.MODERATOR_ROLE - case "presenter" => user.presenter - case "creator" => { - val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.body.entryId - ) - creatorUserId == msg.header.userId - } - case _ => false - } - } + val hasPermission = checkPermission(user, dc.replaceOrDeletePermission, defaultCreatorCheck( + meetingId, msg.body, msg.header.userId + )) if (!hasPermission.contains(true)) { println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") } else { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala index 5ac622881d..e3037a8ca9 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala @@ -1,9 +1,10 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.common2.msgs.PluginDataChannelPushEntryMsg +import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.checkPermission import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{ PluginModel, Roles, Users2x } +import org.bigbluebutton.core.models.{ PluginModel, Roles, UserState, Users2x } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers { @@ -20,17 +21,7 @@ trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers { case Some(p) => p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { case Some(dc) => - val hasPermission = for { - pushPermission <- dc.pushPermission - } yield { - pushPermission.toLowerCase match { - case "all" => true - case "moderator" => user.role == Roles.MODERATOR_ROLE - case "presenter" => user.presenter - case _ => false - } - } - + val hasPermission = checkPermission(user, dc.pushPermission) if (!hasPermission.contains(true)) { println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") } else { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala index 3ea288353b..1683d807f3 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala @@ -1,6 +1,7 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.common2.msgs.PluginDataChannelReplaceEntryMsg +import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{checkPermission, defaultCreatorCheck} import org.bigbluebutton.core.db.{JsonUtils, PluginDataChannelEntryDAO} import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.models.{PluginModel, Roles, Users2x} @@ -20,26 +21,8 @@ trait PluginDataChannelReplaceEntryMsgHdlr extends HandlerHelpers { case Some(p) => p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { case Some(dc) => - val hasPermission = for { - replaceOrDeletePermission <- dc.replaceOrDeletePermission - } yield { - replaceOrDeletePermission.toLowerCase match { - case "all" => true - case "moderator" => user.role == Roles.MODERATOR_ROLE - case "presenter" => user.presenter - case "creator" => { - val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.body.entryId - ) - creatorUserId == msg.header.userId - } - case _ => false - } - } + val hasPermission = checkPermission(user, dc.replaceOrDeletePermission, defaultCreatorCheck( + meetingId, msg.body, msg.header.userId)) if (!hasPermission.contains(true)) { println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala index 27e6a23d88..1f5030cf9a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala @@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.ClientSettings import org.bigbluebutton.common2.msgs.PluginDataChannelResetMsg +import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.checkPermission import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.models.{ PluginModel, Roles, Users2x } @@ -21,16 +22,7 @@ trait PluginDataChannelResetMsgHdlr extends HandlerHelpers { case Some(p) => p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { case Some(dc) => - val hasPermission = for { - replaceOrDeletePermission <- dc.replaceOrDeletePermission - } yield { - replaceOrDeletePermission.toLowerCase match { - case "all" => true - case "moderator" => user.role == Roles.MODERATOR_ROLE - case "presenter" => user.presenter - case _ => false - } - } + val hasPermission = checkPermission(user, dc.replaceOrDeletePermission) if (!hasPermission.contains(true)) { println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrHelpers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrHelpers.scala new file mode 100644 index 0000000000..469d9fae6a --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrHelpers.scala @@ -0,0 +1,27 @@ +package org.bigbluebutton.core.apps.plugin + +import org.bigbluebutton.common2.msgs.PluginDataChannelReplaceOrDeleteBaseBody +import org.bigbluebutton.core.db.PluginDataChannelEntryDAO +import org.bigbluebutton.core.models.{ Roles, UserState } + +object PluginHdlrHelpers { + def checkPermission(user: UserState, permissionType: List[String], creatorCheck: => Boolean = false): List[Boolean] = { + permissionType.map(_.toLowerCase).map { + case "all" => true + case "moderator" => user.role == Roles.MODERATOR_ROLE + case "presenter" => user.presenter + case "creator" => creatorCheck + case _ => false + } + } + def defaultCreatorCheck[T <: PluginDataChannelReplaceOrDeleteBaseBody](meetingId: String, msgBody: T, userId: String): Boolean = { + val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator( + meetingId, + msgBody.pluginName, + msgBody.channelName, + msgBody.subChannelName, + msgBody.entryId + ) + creatorUserId == userId + } +} diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PluginMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PluginMsgs.scala index 7fe61c9d8b..53c67031c4 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PluginMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PluginMsgs.scala @@ -7,6 +7,14 @@ import org.bigbluebutton.common2.domain.PluginLearningAnalyticsDashboardGenericD /** * Sent from graphql-actions to bbb-akka */ + +trait PluginDataChannelReplaceOrDeleteBaseBody{ + val pluginName: String + val channelName: String + val subChannelName: String + val entryId: String +} + object PluginDataChannelPushEntryMsg { val NAME = "PluginDataChannelPushEntryMsg" } case class PluginDataChannelPushEntryMsg(header: BbbClientMsgHeader, body: PluginDataChannelPushEntryMsgBody) extends StandardMsg case class PluginDataChannelPushEntryMsgBody( @@ -20,13 +28,13 @@ case class PluginDataChannelPushEntryMsgBody( object PluginDataChannelReplaceEntryMsg { val NAME = "PluginDataChannelReplaceEntryMsg" } case class PluginDataChannelReplaceEntryMsg(header: BbbClientMsgHeader, body: PluginDataChannelReplaceEntryMsgBody) extends StandardMsg -case class PluginDataChannelReplaceEntryMsgBody( +case class PluginDataChannelReplaceEntryMsgBody ( pluginName: String, channelName: String, subChannelName: String, payloadJson: Map[String, Any], entryId: String, - ) + ) extends PluginDataChannelReplaceOrDeleteBaseBody object PluginDataChannelDeleteEntryMsg { val NAME = "PluginDataChannelDeleteEntryMsg" } case class PluginDataChannelDeleteEntryMsg(header: BbbClientMsgHeader, body: PluginDataChannelDeleteEntryMsgBody) extends StandardMsg @@ -35,7 +43,7 @@ case class PluginDataChannelDeleteEntryMsgBody( subChannelName: String, channelName: String, entryId: String - ) + ) extends PluginDataChannelReplaceOrDeleteBaseBody object PluginDataChannelResetMsg { val NAME = "PluginDataChannelResetMsg" } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index aa663b8358..a2d4263d8e 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -27,10 +27,7 @@ import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.google.gson.*; import org.bigbluebutton.api.domain.*; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -434,23 +431,27 @@ public class ParamsProcessorUtil { ArrayList pluginsManifests = new ArrayList(); String pluginsManifestParams = params.get(ApiParams.PLUGINS_MANIFESTS); if (!StringUtils.isEmpty(pluginsManifestParams)) { - JsonElement pluginsManifestsJsonElement = new Gson().fromJson(pluginsManifestParams, JsonElement.class); + try { + JsonElement pluginsManifestsJsonElement = new Gson().fromJson(pluginsManifestParams, JsonElement.class); - if(pluginsManifestsJsonElement != null && pluginsManifestsJsonElement.isJsonArray()) { - JsonArray pluginsManifestsJson = pluginsManifestsJsonElement.getAsJsonArray(); - for (JsonElement pluginsManifestJson : pluginsManifestsJson) { - if(pluginsManifestJson.isJsonObject()) { - JsonObject pluginsManifestJsonObj = pluginsManifestJson.getAsJsonObject(); - if(pluginsManifestJsonObj.has("url")) { - String url = pluginsManifestJsonObj.get("url").getAsString(); - PluginsManifest newPlugin = new PluginsManifest(url); - if(pluginsManifestJsonObj.has("checksum")) { - newPlugin.setChecksum(pluginsManifestJsonObj.get("checksum").getAsString()); + if(pluginsManifestsJsonElement != null && pluginsManifestsJsonElement.isJsonArray()) { + JsonArray pluginsManifestsJson = pluginsManifestsJsonElement.getAsJsonArray(); + for (JsonElement pluginsManifestJson : pluginsManifestsJson) { + if(pluginsManifestJson.isJsonObject()) { + JsonObject pluginsManifestJsonObj = pluginsManifestJson.getAsJsonObject(); + if(pluginsManifestJsonObj.has("url")) { + String url = pluginsManifestJsonObj.get("url").getAsString(); + PluginsManifest newPlugin = new PluginsManifest(url); + if(pluginsManifestJsonObj.has("checksum")) { + newPlugin.setChecksum(pluginsManifestJsonObj.get("checksum").getAsString()); + } + pluginsManifests.add(newPlugin); } - pluginsManifests.add(newPlugin); } } } + } catch (JsonSyntaxException err) { + log.error("Error in pluginsManifests URL parameter's json structure."); } } From 96926391ecc35e910053243001801518168bed28 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 14 Oct 2024 11:43:48 -0300 Subject: [PATCH 10/13] [new-server-side-architecture] - Remove code duplication --- .../PluginDataChannelDeleteEntryMsgHdlr.scala | 47 ++++++------------ .../PluginDataChannelPushEntryMsgHdlr.scala | 49 +++++++------------ ...PluginDataChannelReplaceEntryMsgHdlr.scala | 46 ++++++----------- .../PluginDataChannelResetMsgHdlr.scala | 42 +++++----------- .../core/apps/plugin/PluginHdlrHelpers.scala | 25 +++++++++- 5 files changed, 86 insertions(+), 123 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala index 9abc3592c0..10803dd51e 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteEntryMsgHdlr.scala @@ -1,44 +1,29 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.common2.msgs.PluginDataChannelDeleteEntryMsg -import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{ checkPermission, defaultCreatorCheck } +import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{ checkPermission, dataChannelCheckingLogic, defaultCreatorCheck } import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{ PluginModel, Roles, Users2x } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } trait PluginDataChannelDeleteEntryMsgHdlr extends HandlerHelpers { def handle(msg: PluginDataChannelDeleteEntryMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") - val meetingId = liveMeeting.props.meetingProp.intId - - for { - _ <- if (!pluginsDisabled) Some(()) else None - user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) - } yield { - PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { - case Some(p) => - p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { - case Some(dc) => - val hasPermission = checkPermission(user, dc.replaceOrDeletePermission, defaultCreatorCheck( - meetingId, msg.body, msg.header.userId - )) - if (!hasPermission.contains(true)) { - println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") - } else { - PluginDataChannelEntryDAO.delete( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.body.entryId - ) - } - case None => println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") - } - case None => println(s"Plugin '${msg.body.pluginName}' not found.") + dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => { + val hasPermission = checkPermission(user, dc.replaceOrDeletePermission, defaultCreatorCheck( + meetingId, msg.body, msg.header.userId + )) + if (!hasPermission.contains(true)) { + println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") + } else { + PluginDataChannelEntryDAO.delete( + meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName, + msg.body.entryId + ) } - } + }) } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala index e3037a8ca9..e83b867412 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelPushEntryMsgHdlr.scala @@ -1,45 +1,30 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.common2.msgs.PluginDataChannelPushEntryMsg -import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.checkPermission +import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{ checkPermission, dataChannelCheckingLogic, defaultCreatorCheck } import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{ PluginModel, Roles, UserState, Users2x } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers { def handle(msg: PluginDataChannelPushEntryMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") - val meetingId = liveMeeting.props.meetingProp.intId - - for { - _ <- if (!pluginsDisabled) Some(()) else None - user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) - } yield { - PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { - case Some(p) => - p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { - case Some(dc) => - val hasPermission = checkPermission(user, dc.pushPermission) - if (!hasPermission.contains(true)) { - println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") - } else { - PluginDataChannelEntryDAO.insert( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.header.userId, - msg.body.payloadJson, - msg.body.toRoles, - msg.body.toUserIds - ) - } - case None => println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") - } - case None => println(s"Plugin '${msg.body.pluginName}' not found.") + dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => { + val hasPermission = checkPermission(user, dc.pushPermission) + if (!hasPermission.contains(true)) { + println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") + } else { + PluginDataChannelEntryDAO.insert( + meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName, + msg.header.userId, + msg.body.payloadJson, + msg.body.toRoles, + msg.body.toUserIds + ) } - } + }) } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala index 1683d807f3..b216353f55 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelReplaceEntryMsgHdlr.scala @@ -1,45 +1,31 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.common2.msgs.PluginDataChannelReplaceEntryMsg -import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{checkPermission, defaultCreatorCheck} +import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{checkPermission, dataChannelCheckingLogic, defaultCreatorCheck} import org.bigbluebutton.core.db.{JsonUtils, PluginDataChannelEntryDAO} import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{PluginModel, Roles, Users2x} import org.bigbluebutton.core.running.{HandlerHelpers, LiveMeeting} trait PluginDataChannelReplaceEntryMsgHdlr extends HandlerHelpers { def handle(msg: PluginDataChannelReplaceEntryMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") - val meetingId = liveMeeting.props.meetingProp.intId - for { - _ <- if (!pluginsDisabled) Some(()) else None - user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) - } yield { - PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { - case Some(p) => - p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { - case Some(dc) => - val hasPermission = checkPermission(user, dc.replaceOrDeletePermission, defaultCreatorCheck( - meetingId, msg.body, msg.header.userId)) + dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => { + val hasPermission = checkPermission(user, dc.replaceOrDeletePermission, defaultCreatorCheck( + meetingId, msg.body, msg.header.userId)) - if (!hasPermission.contains(true)) { - println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") - } else { - PluginDataChannelEntryDAO.replace( - msg.header.meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName, - msg.body.entryId, - JsonUtils.mapToJson(msg.body.payloadJson), - ) - } - case None => println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") - } - case None => println(s"Plugin '${msg.body.pluginName}' not found.") + if (!hasPermission.contains(true)) { + println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") + } else { + PluginDataChannelEntryDAO.replace( + msg.header.meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName, + msg.body.entryId, + JsonUtils.mapToJson(msg.body.payloadJson), + ) } - } + }) } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala index 1f5030cf9a..61dcfec982 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala @@ -1,43 +1,27 @@ package org.bigbluebutton.core.apps.plugin -import org.bigbluebutton.ClientSettings import org.bigbluebutton.common2.msgs.PluginDataChannelResetMsg -import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.checkPermission +import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{ checkPermission, dataChannelCheckingLogic } import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{ PluginModel, Roles, Users2x } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } trait PluginDataChannelResetMsgHdlr extends HandlerHelpers { def handle(msg: PluginDataChannelResetMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") - val meetingId = liveMeeting.props.meetingProp.intId + dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => { + val hasPermission = checkPermission(user, dc.replaceOrDeletePermission) - for { - _ <- if (!pluginsDisabled) Some(()) else None - user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) - } yield { - PluginModel.getPluginByName(liveMeeting.plugins, msg.body.pluginName) match { - case Some(p) => - p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == msg.body.channelName) match { - case Some(dc) => - val hasPermission = checkPermission(user, dc.replaceOrDeletePermission) - - if (!hasPermission.contains(true)) { - println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") - } else { - PluginDataChannelEntryDAO.reset( - meetingId, - msg.body.pluginName, - msg.body.channelName, - msg.body.subChannelName - ) - } - case None => println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.") - } - case None => println(s"Plugin '${msg.body.pluginName}' not found.") + if (!hasPermission.contains(true)) { + println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") + } else { + PluginDataChannelEntryDAO.reset( + meetingId, + msg.body.pluginName, + msg.body.channelName, + msg.body.subChannelName + ) } - } + }) } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrHelpers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrHelpers.scala index 469d9fae6a..68bed194e1 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrHelpers.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrHelpers.scala @@ -2,7 +2,8 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.common2.msgs.PluginDataChannelReplaceOrDeleteBaseBody import org.bigbluebutton.core.db.PluginDataChannelEntryDAO -import org.bigbluebutton.core.models.{ Roles, UserState } +import org.bigbluebutton.core.models.{ DataChannel, PluginModel, Roles, UserState, Users2x } +import org.bigbluebutton.core.running.LiveMeeting object PluginHdlrHelpers { def checkPermission(user: UserState, permissionType: List[String], creatorCheck: => Boolean = false): List[Boolean] = { @@ -24,4 +25,26 @@ object PluginHdlrHelpers { ) creatorUserId == userId } + + def dataChannelCheckingLogic(liveMeeting: LiveMeeting, userId: String, + pluginName: String, channelName: String, + caseSomeDataChannelAndPlugin: (UserState, DataChannel, String) => Unit): Option[Unit] = { + val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") + val meetingId = liveMeeting.props.meetingProp.intId + + for { + _ <- if (!pluginsDisabled) Some(()) else None + user <- Users2x.findWithIntId(liveMeeting.users2x, userId) + } yield { + PluginModel.getPluginByName(liveMeeting.plugins, pluginName) match { + case Some(p) => + p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == channelName) match { + case Some(dc) => + caseSomeDataChannelAndPlugin(user, dc, meetingId) + case None => println(s"Data channel '${channelName}' not found in plugin '${pluginName}'.") + } + case None => println(s"Plugin '${pluginName}' not found.") + } + } + } } From f3c03fbc4409c951f7cd9fe53c1d14c0df5e41e0 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Mon, 14 Oct 2024 14:34:40 -0300 Subject: [PATCH 11/13] Include more useful headers for endpoint /checkGraphqlAuthorization it is useful when the application that requires authorization wants to inject user information to the request it can be an integration with a third party applicaton and it will require to receive user-external-is and meeting-external-id it can also validate if the requester is moderator or presenter, so now it includes user-is-moderator and user-is-presenter headers --- .../web/controllers/ConnectionController.groovy | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy index 78ca7df618..8cf7e63f72 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy @@ -77,6 +77,12 @@ class ConnectionController { } response.addHeader("Meeting-Id", userSession.meetingID) + response.addHeader("Meeting-External-Id", userSession.externMeetingID) + response.addHeader("User-Id", userSession.internalUserId) + response.addHeader("User-External-Id", userSession.externUserID) + response.addHeader("User-Name", URLEncoder.encode(userSession.fullname, StandardCharsets.UTF_8.name())) + response.addHeader("User-Is-Moderator", u && u.isModerator() ? "true" : "false") + response.addHeader("User-Is-Presenter", u && u.isPresenter() ? "true" : "false") response.setStatus(200) withFormat { json { @@ -96,6 +102,12 @@ class ConnectionController { UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken) if(removedUserSession) { response.addHeader("Meeting-Id", removedUserSession.meetingId) + response.addHeader("Meeting-External-Id", userSession.externMeetingID) + response.addHeader("User-Id", userSession.internalUserId) + response.addHeader("User-External-Id", userSession.externUserID) + response.addHeader("User-Name", URLEncoder.encode(userSession.fullname, StandardCharsets.UTF_8.name())) + response.addHeader("User-Is-Moderator", u && u.isModerator() ? "true" : "false") + response.addHeader("User-Is-Presenter", u && u.isPresenter() ? "true" : "false") response.setStatus(200) withFormat { json { From 40c00f8739b45ffb581fc78b0fd99dd2365749b0 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Mon, 14 Oct 2024 14:37:32 -0300 Subject: [PATCH 12/13] Introduces a config `pluginsManifests` to bigbluebutton.properties. e.g pluginsManifests=[{url: "https://plugin_manifest.json"}] it will be merged with the parameters received through /create?pluginsManifests= --- .../api/ParamsProcessorUtil.java | 98 +++++++++++-------- .../grails-app/conf/bigbluebutton.properties | 4 + .../grails-app/conf/spring/resources.xml | 1 + 3 files changed, 61 insertions(+), 42 deletions(-) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index aa663b8358..848eb06743 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -102,6 +102,7 @@ public class ParamsProcessorUtil { private boolean defaultAllowModsToUnmuteUsers = false; private boolean defaultAllowModsToEjectCameras = false; private String defaultDisabledFeatures; + private String defaultPluginsManifests; private boolean defaultNotifyRecordingIsOn = false; private boolean defaultKeepEvents = false; private Boolean useDefaultLogo; @@ -430,25 +431,23 @@ public class ParamsProcessorUtil { return groups; } - private ArrayList processPluginsManifestsParams(Map params) { + private ArrayList processPluginsManifests(String pluginsManifestsParam) { ArrayList pluginsManifests = new ArrayList(); - String pluginsManifestParams = params.get(ApiParams.PLUGINS_MANIFESTS); - if (!StringUtils.isEmpty(pluginsManifestParams)) { - JsonElement pluginsManifestsJsonElement = new Gson().fromJson(pluginsManifestParams, JsonElement.class); - if(pluginsManifestsJsonElement != null && pluginsManifestsJsonElement.isJsonArray()) { - JsonArray pluginsManifestsJson = pluginsManifestsJsonElement.getAsJsonArray(); - for (JsonElement pluginsManifestJson : pluginsManifestsJson) { - if(pluginsManifestJson.isJsonObject()) { - JsonObject pluginsManifestJsonObj = pluginsManifestJson.getAsJsonObject(); - if(pluginsManifestJsonObj.has("url")) { - String url = pluginsManifestJsonObj.get("url").getAsString(); - PluginsManifest newPlugin = new PluginsManifest(url); - if(pluginsManifestJsonObj.has("checksum")) { - newPlugin.setChecksum(pluginsManifestJsonObj.get("checksum").getAsString()); - } - pluginsManifests.add(newPlugin); + JsonElement pluginsManifestsJsonElement = new Gson().fromJson(pluginsManifestsParam, JsonElement.class); + + if(pluginsManifestsJsonElement != null && pluginsManifestsJsonElement.isJsonArray()) { + JsonArray pluginsManifestsJson = pluginsManifestsJsonElement.getAsJsonArray(); + for (JsonElement pluginsManifestJson : pluginsManifestsJson) { + if(pluginsManifestJson.isJsonObject()) { + JsonObject pluginsManifestJsonObj = pluginsManifestJson.getAsJsonObject(); + if(pluginsManifestJsonObj.has("url")) { + String url = pluginsManifestJsonObj.get("url").getAsString(); + PluginsManifest newPlugin = new PluginsManifest(url); + if(pluginsManifestJsonObj.has("checksum")) { + newPlugin.setChecksum(pluginsManifestJsonObj.get("checksum").getAsString()); } + pluginsManifests.add(newPlugin); } } } @@ -565,9 +564,6 @@ public class ParamsProcessorUtil { listOfDisabledFeatures.replaceAll(String::trim); listOfDisabledFeatures = new ArrayList<>(new HashSet<>(listOfDisabledFeatures)); - // Process Plugin Manifest Urls - ArrayList listOfPluginsManifests = processPluginsManifestsParams(params); - // Check Disabled Features Exclude list -- passed as a CREATE parameter to cancel the disabling (typically from bbb-web's properties file) ArrayList listOfDisabledFeaturesExclude = new ArrayList<>(); if (!StringUtils.isEmpty(params.get(ApiParams.DISABLED_FEATURES_EXCLUDE))) { @@ -578,6 +574,20 @@ public class ParamsProcessorUtil { listOfDisabledFeatures.removeAll(Arrays.asList(disabledFeaturesExcludeParam.split(","))); } + // Parse Plugins Manifests from config and param + ArrayList listOfPluginsManifests = new ArrayList(); + //Process plugins from config + if(defaultPluginsManifests != null && !defaultPluginsManifests.isEmpty()) { + ArrayList pluginsManifestsFromConfig = processPluginsManifests(defaultPluginsManifests); + listOfPluginsManifests.addAll(pluginsManifestsFromConfig); + } + //Process plugins from /create param + String pluginsManifestsParam = params.get(ApiParams.PLUGINS_MANIFESTS); + if (!StringUtils.isEmpty(pluginsManifestsParam)) { + ArrayList pluginsManifestsFromParam = processPluginsManifests(pluginsManifestsParam); + listOfPluginsManifests.addAll(pluginsManifestsFromParam); + } + // Check if VirtualBackgrounds is disabled if (!StringUtils.isEmpty(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED))) { boolean virtualBackgroundsDisabled = Boolean.valueOf(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED)); @@ -1603,36 +1613,40 @@ public class ParamsProcessorUtil { this.defaultEndWhenNoModerator = val; } - public void setEndWhenNoModeratorDelayInMinutes(Integer value) { - this.defaultEndWhenNoModeratorDelayInMinutes = value; - } + public void setEndWhenNoModeratorDelayInMinutes(Integer value) { + this.defaultEndWhenNoModeratorDelayInMinutes = value; + } - public void setDisabledFeatures(String disabledFeatures) { - this.defaultDisabledFeatures = disabledFeatures; - } + public void setDisabledFeatures(String disabledFeatures) { + this.defaultDisabledFeatures = disabledFeatures; + } - public void setNotifyRecordingIsOn(Boolean notifyRecordingIsOn) { - this.defaultNotifyRecordingIsOn = notifyRecordingIsOn; - } + public void setPluginsManifests(String pluginsManifests) { + this.defaultPluginsManifests = pluginsManifests; + } - public void setPresentationUploadExternalDescription(String presentationUploadExternalDescription) { - this.defaultPresentationUploadExternalDescription = presentationUploadExternalDescription; - } + public void setNotifyRecordingIsOn(Boolean notifyRecordingIsOn) { + this.defaultNotifyRecordingIsOn = notifyRecordingIsOn; + } - public void setPresentationUploadExternalUrl(String presentationUploadExternalUrl) { - this.defaultPresentationUploadExternalUrl = presentationUploadExternalUrl; - } + public void setPresentationUploadExternalDescription(String presentationUploadExternalDescription) { + this.defaultPresentationUploadExternalDescription = presentationUploadExternalDescription; + } - public void setBbbVersion(String version) { + public void setPresentationUploadExternalUrl(String presentationUploadExternalUrl) { + this.defaultPresentationUploadExternalUrl = presentationUploadExternalUrl; + } + + public void setBbbVersion(String version) { this.bbbVersion = this.allowRevealOfBBBVersion ? version : ""; - } + } - public void setAllowRevealOfBBBVersion(Boolean allowVersion) { - this.allowRevealOfBBBVersion = allowVersion; - } + public void setAllowRevealOfBBBVersion(Boolean allowVersion) { + this.allowRevealOfBBBVersion = allowVersion; + } - public void setAllowOverrideClientSettingsOnCreateCall(Boolean allowOverrideClientSettingsOnCreateCall) { - this.allowOverrideClientSettingsOnCreateCall = allowOverrideClientSettingsOnCreateCall; - } + public void setAllowOverrideClientSettingsOnCreateCall(Boolean allowOverrideClientSettingsOnCreateCall) { + this.allowOverrideClientSettingsOnCreateCall = allowOverrideClientSettingsOnCreateCall; + } } diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index c66a5c181a..e08f124865 100644 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -474,3 +474,7 @@ breakoutRoomsEnabled=true # legacy, please use maxUserConcurrentAccesses instead allowDuplicateExtUserid=true + +# list of plugins manifests (json array) +# e.g: [{url: "https://plugin_manifest.json"}] +pluginsManifests= diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml index f16003fe1b..e20befdf55 100755 --- a/bigbluebutton-web/grails-app/conf/spring/resources.xml +++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml @@ -201,6 +201,7 @@ with BigBlueButton; if not, see . + From bc2a72bd3d72e33eb0f191b2d571497efe0c4f00 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Mon, 14 Oct 2024 16:13:28 -0300 Subject: [PATCH 13/13] Fix checkAuth Headers for removed users Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../web/controllers/ConnectionController.groovy | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy index 8cf7e63f72..c3ba847019 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy @@ -102,12 +102,12 @@ class ConnectionController { UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken) if(removedUserSession) { response.addHeader("Meeting-Id", removedUserSession.meetingId) - response.addHeader("Meeting-External-Id", userSession.externMeetingID) - response.addHeader("User-Id", userSession.internalUserId) - response.addHeader("User-External-Id", userSession.externUserID) - response.addHeader("User-Name", URLEncoder.encode(userSession.fullname, StandardCharsets.UTF_8.name())) - response.addHeader("User-Is-Moderator", u && u.isModerator() ? "true" : "false") - response.addHeader("User-Is-Presenter", u && u.isPresenter() ? "true" : "false") + response.addHeader("Meeting-External-Id", removedUserSession.externMeetingID) + response.addHeader("User-Id", removedUserSession.internalUserId) + response.addHeader("User-External-Id", removedUserSession.externUserID) + response.addHeader("User-Name", URLEncoder.encode(removedUserSession.fullname, StandardCharsets.UTF_8.name())) + response.addHeader("User-Is-Moderator", removedUserSession.isModerator() ? "true" : "false") + response.addHeader("User-Is-Presenter", "false") response.setStatus(200) withFormat { json {