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..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,62 +1,29 @@ package org.bigbluebutton.core.apps.plugin -import org.bigbluebutton.ClientSettings import org.bigbluebutton.common2.msgs.PluginDataChannelDeleteEntryMsg +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.{ 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 { - 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}'.") + 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 { - 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 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 - ) - } + 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 fc6b8c4c45..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,55 +1,30 @@ package org.bigbluebutton.core.apps.plugin import org.bigbluebutton.common2.msgs.PluginDataChannelPushEntryMsg -import org.bigbluebutton.ClientSettings +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.{ Roles, 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 { - 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}'.") + 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 { - 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 - ) - } + 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 453f64587d..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,63 +1,31 @@ package org.bigbluebutton.core.apps.plugin -import org.bigbluebutton.ClientSettings import org.bigbluebutton.common2.msgs.PluginDataChannelReplaceEntryMsg +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.{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 { - val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile) + 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 (!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}'.") + if (!hasPermission.contains(true)) { + println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") } 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), - ) - } + 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..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,51 +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, dataChannelCheckingLogic } import org.bigbluebutton.core.db.PluginDataChannelEntryDAO import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{ 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 { - 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}'.") + if (!hasPermission.contains(true)) { + println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.") } 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 - ) - } + 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 new file mode 100644 index 0000000000..68bed194e1 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrHelpers.scala @@ -0,0 +1,50 @@ +package org.bigbluebutton.core.apps.plugin + +import org.bigbluebutton.common2.msgs.PluginDataChannelReplaceOrDeleteBaseBody +import org.bigbluebutton.core.db.PluginDataChannelEntryDAO +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] = { + 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 + } + + 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.") + } + } + } +} 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/db/PluginDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala new file mode 100644 index 0000000000..dc7fa869f9 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDAO.scala @@ -0,0 +1,33 @@ +package org.bigbluebutton.core.db + +import PostgresProfile.api._ + +case class PluginDbModel( + meetingId: String, + name: String, + javascriptEntrypointUrl: 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 javascriptEntrypointIntegrity = column[String]("javascriptEntrypointIntegrity") + override def * = (meetingId, name, javascriptEntrypointUrl, javascriptEntrypointIntegrity) <> (PluginDbModel.tupled, PluginDbModel.unapply) +} + +object PluginDAO { + def insert(meetingId: String, name: String, javascriptEntrypointUrl: String, javascriptEntrypointIntegrity: String) = { + DatabaseConnection.enqueue( + TableQuery[PluginDbTableDef].forceInsert( + PluginDbModel( + meetingId = meetingId, + name = name, + javascriptEntrypointUrl = javascriptEntrypointUrl, + javascriptEntrypointIntegrity = javascriptEntrypointIntegrity, + ) + ) + ) + } +} \ 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..b2431bc87e --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Plugins.scala @@ -0,0 +1,90 @@ +package org.bigbluebutton.core.models + +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.core.db.PluginDAO + +import java.util + +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, + 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( + url: String, + content: PluginManifestContent +) + +case class Plugin( + manifest: PluginManifest +) + +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(json: util.Map[String, AnyRef]): PluginModel = { + val instance = new PluginModel() + 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 = { + instance.plugins.foreach { case (_, plugin) => + PluginDAO.insert(meetingId, plugin.manifest.content.name, plugin.manifest.content.javascriptEntrypointUrl, + plugin.manifest.content.javascriptEntrypointIntegrity.getOrElse("")) + } + } +} + +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 0c3811adad..ad9b3b068c 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,7 +172,7 @@ class MeetingActor( outGW.send(msgEvent) //Insert meeting into the database - MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings) + 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 16bdd59e68..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 @@ -2,13 +2,11 @@ package org.bigbluebutton.core.running import org.apache.pekko.actor.ActorContext import org.bigbluebutton.ClientSettings -import org.bigbluebutton.ClientSettings.{getConfigPropertyValueByPathAsBooleanOrElse, getConfigPropertyValueByPathAsStringOrElse} 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.core2.MeetingStatus2x object RunningMeeting { @@ -19,9 +17,9 @@ object RunningMeeting { class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway, eventBus: InternalEventBus)(implicit val context: ActorContext) { - private val externalVideoModel = new ExternalVideoModel() private val chatModel = new ChatModel() + private val plugins = PluginModel.createPluginModelFromJson(props.pluginProp) private val layouts = new Layouts() private val pads = new Pads() private val wbModel = new WhiteboardModel() @@ -45,7 +43,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-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/ApiParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java index 2de1966241..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,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_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 386b176573..b49e2f24c2 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,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; @@ -29,7 +33,11 @@ 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.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.bigbluebutton.api.domain.*; @@ -57,6 +65,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 +107,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); @@ -352,6 +364,102 @@ 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.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"); + } + + 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, Matcher.quoteReplacement(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 (PluginsManifest pluginsManifest : m.getPluginsManifests()) { + try { + + String urlString = pluginsManifest.getUrl(); + URL url = new URL(urlString); + StringBuilder content = new StringBuilder(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()))) { + String inputLine; + while ((inputLine = in.readLine()) != null) { + content.append(inputLine).append("\n"); + } + } + + // Parse the JSON content + JsonNode jsonNode = objectMapper.readTree(content.toString()); + + // Validate checksum if any: + String paramChecksum = pluginsManifest.getChecksum(); + if (!StringUtils.isEmpty(paramChecksum)) { + 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() + ); + 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 " + urlString + "there is no name field configured."); + } + + String pluginKey = name; + HashMap manifestObject = new HashMap<>(); + manifestObject.put("url", urlString); + String manifestContent = replaceMetaParametersIntoManifestTemplate(content.toString(), metadata); + + Map mappedManifestContent = objectMapper.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: ", + pluginsManifest.getUrl(), e); + log.error("Therefore this plugin will not be loaded"); + } + } + return urlContents; + } public synchronized boolean createMeeting(Meeting m) { String internalMeetingId = paramsProcessorUtil.convertToInternalMeetingId(m.getExternalId()); Meeting existingId = getNotEndedMeetingWithId(internalMeetingId); @@ -359,6 +467,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; } @@ -444,7 +554,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..54d2d85a41 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,10 +26,9 @@ import java.nio.charset.StandardCharsets; 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; import org.jsoup.safety.Safelist; @@ -39,10 +38,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; @@ -104,6 +99,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; @@ -432,6 +428,33 @@ public class ParamsProcessorUtil { return groups; } + private ArrayList processPluginsManifests(String pluginsManifestsParam) { + ArrayList pluginsManifests = new ArrayList(); + JsonElement pluginsManifestsJsonElement = new Gson().fromJson(pluginsManifestsParam, JsonElement.class); + try { + 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); + } + } + } + } + } catch(JsonSyntaxException err){ + log.error("Error in pluginsManifests URL parameter's json structure."); + } + + return pluginsManifests; + } + public Meeting processCreateParams(Map params) { String meetingName = params.get(ApiParams.NAME); @@ -550,6 +573,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)); @@ -790,6 +827,7 @@ public class ParamsProcessorUtil { .withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins) .withLearningDashboardAccessToken(learningDashboardAccessToken) .withGroups(groups) + .withPluginManifests(listOfPluginsManifests) .withDisabledFeatures(listOfDisabledFeatures) .withNotifyRecordingIsOn(notifyRecordingIsOn) .withPresentationUploadExternalDescription(presentationUploadExternalDescription) @@ -1574,36 +1612,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/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 2c9bae2311..04007338a7 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 pluginsManifests; 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; + pluginsManifests = builder.pluginsManifests; notifyRecordingIsOn = builder.notifyRecordingIsOn; presentationUploadExternalDescription = builder.presentationUploadExternalDescription; presentationUploadExternalUrl = builder.presentationUploadExternalUrl; @@ -441,6 +444,17 @@ public class Meeting { public ArrayList getDisabledFeatures() { return disabledFeatures; } + public Map getPlugins() { + return plugins; + } + + public void setPlugins(Map p) { + plugins = p; + } + + public ArrayList getPluginsManifests() { + return pluginsManifests; + } public Boolean getNotifyRecordingIsOn() { return notifyRecordingIsOn; @@ -929,6 +943,7 @@ public class Meeting { private int learningDashboardCleanupDelayInMinutes; private String learningDashboardAccessToken; private ArrayList disabledFeatures; + private ArrayList pluginsManifests; private Boolean notifyRecordingIsOn; private String presentationUploadExternalDescription; private String presentationUploadExternalUrl; @@ -1063,6 +1078,11 @@ public class Meeting { return this; } + public Builder withPluginManifests(ArrayList map) { + this.pluginsManifests = map; + return this; + } + public Builder withNotifyRecordingIsOn(Boolean b) { this.notifyRecordingIsOn = b; 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-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java index c71d79ad8d..cb80263b8c 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 827d49be14..c05829d301 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 96dbcdcdb6..7a8ce56e3b 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -2063,6 +2063,18 @@ 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), + "javascriptEntrypointIntegrity" 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..2862b820dd --- /dev/null +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_plugin.yaml @@ -0,0 +1,29 @@ +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: + - javascriptEntrypointIntegrity + - javascriptEntrypointUrl + - name + filter: + meetingId: + _eq: X-Hasura-MeetingId + comment: "" + - role: bbb_client_not_in_meeting + permission: + columns: + - javascriptEntrypointIntegrity + - 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 4503ba1e3f..f23200670e 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml @@ -26,6 +26,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 f68e89a081..c0b6f2b0b8 100644 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -249,6 +249,7 @@ class App extends Component { presentationIsOpen, darkTheme, intl, + pluginConfig, genericMainContentId, } = this.props; @@ -260,7 +261,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..8b49379485 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', @@ -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.javascriptEntrypointIntegrity) { + script.setAttribute('integrity', plugin.javascriptEntrypointIntegrity); } document.head.appendChild(script); }, [plugin, containerRef]); 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..8d383fbf56 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/query.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +const PLUGIN_CONFIGURATION_QUERY = gql`query PluginConfigurationQuery { + plugin { + name, + javascriptEntrypointUrl, + javascriptEntrypointIntegrity, + } +}`; + +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..8e3c032ca5 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/types.ts @@ -4,10 +4,19 @@ 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; - checksum?: string; + javascriptEntrypointIntegrity?: string; } export interface EffectivePluginConfig extends PluginConfig { diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index a8cfad93c8..31cd6b1e77 100644 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -477,3 +477,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 638f0aca96..2f3b330463 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 . + 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 658b868aac..e5872b33a7 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 @@ -90,6 +90,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 { @@ -109,6 +115,12 @@ class ConnectionController { UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken) if(removedUserSession) { response.addHeader("Meeting-Id", removedUserSession.meetingId) + 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 {