diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/upload/FileUploadedSysMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/upload/FileUploadedSysMsgHdlr.scala new file mode 100644 index 000000000000..b95c26c7fa8e --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/upload/FileUploadedSysMsgHdlr.scala @@ -0,0 +1,36 @@ +package org.bigbluebutton.core.apps.upload + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting + +trait FileUploadedSysMsgHdlr { + this: UploadApp2x => + + def handle( + msg: FileUploadedSysMsg, + liveMeeting: LiveMeeting, + bus: MessageBus + ): Unit = { + + val meetingId = liveMeeting.props.meetingProp.intId + val userId = msg.header.userId + + def broadcastFileUploadedEvtMsg(msg: FileUploadedSysMsg): Unit = { + val uploadId = msg.body.uploadId + val source = msg.body.source + val filename = msg.body.filename + + val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId) + val envelope = BbbCoreEnvelope(FileUploadedEvtMsg.NAME, routing) + val header = BbbClientMsgHeader(FileUploadedEvtMsg.NAME, meetingId, userId) + val body = FileUploadedEvtMsgBody(uploadId, source, filename) + val event = FileUploadedEvtMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + + bus.outGW.send(msgEvent) + } + + broadcastFileUploadedEvtMsg(msg) + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/upload/UploadApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/upload/UploadApp2x.scala new file mode 100644 index 000000000000..e2c29e2cc1c8 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/upload/UploadApp2x.scala @@ -0,0 +1,11 @@ +package org.bigbluebutton.core.apps.upload + +import akka.actor.ActorContext +import akka.event.Logging + +class UploadApp2x(implicit val context: ActorContext) + extends UploadRequestReqMsgHdlr + with FileUploadedSysMsgHdlr { + + val log = Logging(context.system, getClass) +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/upload/UploadRequestReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/upload/UploadRequestReqMsgHdlr.scala new file mode 100644 index 000000000000..ac1494f8bf3f --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/upload/UploadRequestReqMsgHdlr.scala @@ -0,0 +1,65 @@ +package org.bigbluebutton.core.apps.upload + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } +import org.bigbluebutton.core.util.RandomStringGenerator + +trait UploadRequestReqMsgHdlr extends RightsManagementTrait { + this: UploadApp2x => + + def handle( + msg: UploadRequestReqMsg, + liveMeeting: LiveMeeting, + bus: MessageBus + ): Unit = { + + val meetingId = liveMeeting.props.meetingProp.intId + val userId = msg.header.userId + val source = msg.body.source + val filename = msg.body.filename + val timestamp = msg.body.timestamp + + // To system + def broadcastUploadRequestSysMsg( + msg: UploadRequestReqMsg, + token: String + ): Unit = { + + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(UploadRequestSysMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId(UploadRequestSysMsg.NAME, meetingId) + val body = UploadRequestSysMsgBody(source, filename, userId, token) + val event = UploadRequestSysMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + + bus.outGW.send(msgEvent) + } + + // To client + def broadcastUploadRequestRespMsg( + msg: UploadRequestReqMsg, + success: Boolean = false, + token: String = null + ): Unit = { + + val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId) + val envelope = BbbCoreEnvelope(UploadRequestRespMsg.NAME, routing) + val header = BbbClientMsgHeader(UploadRequestRespMsg.NAME, meetingId, userId) + val body = UploadRequestRespMsgBody(source, filename, userId, success, timestamp, token) + val event = UploadRequestRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + + bus.outGW.send(msgEvent) + } + + if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.MOD_LEVEL, liveMeeting.users2x, userId)) { + broadcastUploadRequestRespMsg(msg) + } else { + val token = RandomStringGenerator.randomAlphanumericString(32) + broadcastUploadRequestSysMsg(msg, token) + broadcastUploadRequestRespMsg(msg, true, token) + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index 98808f3e900e..aa6128f1bd5b 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -363,6 +363,12 @@ class ReceivedJsonMsgHandlerActor( case GetScreenshareStatusReqMsg.NAME => routeGenericMsg[GetScreenshareStatusReqMsg](envelope, jsonNode) + // Upload + case UploadRequestReqMsg.NAME => + routeGenericMsg[UploadRequestReqMsg](envelope, jsonNode) + case FileUploadedSysMsg.NAME => + routeGenericMsg[FileUploadedSysMsg](envelope, jsonNode) + // Lock settings case LockUserInMeetingCmdMsg.NAME => routeGenericMsg[LockUserInMeetingCmdMsg](envelope, jsonNode) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractUploadRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractUploadRecordEvent.scala new file mode 100644 index 000000000000..122a3e708fe7 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractUploadRecordEvent.scala @@ -0,0 +1,24 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +trait AbstractUploadRecordEvent extends RecordEvent { + setModule("UPLOAD") +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/FileUploadedRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/FileUploadedRecordEvent.scala new file mode 100644 index 000000000000..7f506a1d67b0 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/FileUploadedRecordEvent.scala @@ -0,0 +1,49 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +class FileUploadedRecordEvent extends AbstractUploadRecordEvent { + import FileUploadedRecordEvent._ + + setEvent("FileUploadedEvent") + + def setUserId(userId: String) { + eventMap.put(USER_ID, userId) + } + + def setUploadId(uploadId: String) { + eventMap.put(UPLOAD_ID, uploadId) + } + + def setSource(source: String) { + eventMap.put(SOURCE, source) + } + + def setFilename(filename: String) { + eventMap.put(FILENAME, filename) + } +} + +object FileUploadedRecordEvent { + protected final val USER_ID = "userId" + protected final val UPLOAD_ID = "uploadId" + protected final val SOURCE = "source" + protected final val FILENAME = "filename" +} 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 3a01f2d1454d..89131c21a5a3 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 @@ -6,6 +6,7 @@ import akka.actor.SupervisorStrategy.Resume import org.bigbluebutton.SystemConfiguration import org.bigbluebutton.core.apps.groupchats.GroupChatHdlrs import org.bigbluebutton.core.apps.presentationpod._ +import org.bigbluebutton.core.apps.upload.UploadApp2x import org.bigbluebutton.core.apps.users._ import org.bigbluebutton.core.apps.whiteboard.ClientToServerLatencyTracerMsgHdlr import org.bigbluebutton.core.domain._ @@ -133,6 +134,7 @@ class MeetingActor( val usersApp = new UsersApp(liveMeeting, outGW, eventBus) val groupChatApp = new GroupChatHdlrs val presentationPodsApp = new PresentationPodHdlrs + val uploadApp = new UploadApp2x val pollApp = new PollApp2x val webcamApp2x = new WebcamApp2x val wbApp = new WhiteboardApp2x @@ -538,6 +540,10 @@ class MeetingActor( case m: UpdateCaptionOwnerPubMsg => captionApp2x.handle(m, liveMeeting, msgBus) case m: SendCaptionHistoryReqMsg => captionApp2x.handle(m, liveMeeting, msgBus) + // Upload + case m: UploadRequestReqMsg => uploadApp.handle(m, liveMeeting, msgBus) + case m: FileUploadedSysMsg => uploadApp.handle(m, liveMeeting, msgBus) + // Guests case m: GetGuestsWaitingApprovalReqMsg => handleGetGuestsWaitingApprovalReqMsg(m) case m: SetGuestPolicyCmdMsg => handleSetGuestPolicyMsg(m) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala index 594919973885..1b846c37763c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala @@ -159,6 +159,11 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging { case m: SetPrivateGuestLobbyMessageCmdMsg => logMessage(msg) case m: PrivateGuestLobbyMsgChangedEvtMsg => logMessage(msg) + // Upload + case m: UploadRequestReqMsg => logMessage(msg) + case m: UploadRequestRespMsg => logMessage(msg) + case m: FileUploadedEvtMsg => logMessage(msg) + // System case m: ClientToServerLatencyTracerMsg => traceMessage(msg) case m: ServerToClientLatencyTracerMsg => traceMessage(msg) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index e634a89007b4..c6abb09e926c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -121,6 +121,9 @@ class RedisRecorderActor( case m: MeetingEndingEvtMsg => handleEndAndKickAllSysMsg(m) case m: MeetingCreatedEvtMsg => handleStarterConfigurations(m) + // Upload + case m: FileUploadedEvtMsg => handleFileUploadedEvtMsg(m) + // Recording case m: RecordingChapterBreakSysMsg => handleRecordingChapterBreakSysMsg(m) @@ -579,6 +582,16 @@ class RedisRecorderActor( record(msg.header.meetingId, ev.toMap.asJava) } + private def handleFileUploadedEvtMsg(msg: FileUploadedEvtMsg): Unit = { + val ev = new FileUploadedRecordEvent() + ev.setUserId(msg.header.userId) + ev.setUploadId(msg.body.uploadId) + ev.setSource(msg.body.source) + ev.setFilename(msg.body.filename) + + record(msg.header.meetingId, ev.toMap.asJava) + } + private def handleRecordingChapterBreakSysMsg(msg: RecordingChapterBreakSysMsg): Unit = { val ev = new RecordChapterBreakRecordEvent() ev.setMeetingId(msg.header.meetingId) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UploadMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UploadMsgs.scala new file mode 100644 index 000000000000..5c973f1e8a58 --- /dev/null +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UploadMsgs.scala @@ -0,0 +1,25 @@ +package org.bigbluebutton.common2.msgs + +// client to akka-apps +object UploadRequestReqMsg { val NAME = "UploadRequestReqMsg" } +case class UploadRequestReqMsg(header: BbbClientMsgHeader, body: UploadRequestReqMsgBody) extends StandardMsg +case class UploadRequestReqMsgBody(source: String, filename: String, timestamp: Long) + +// akka-apps to client +object UploadRequestRespMsg { val NAME = "UploadRequestRespMsg" } +case class UploadRequestRespMsg(header: BbbClientMsgHeader, body: UploadRequestRespMsgBody) extends StandardMsg +case class UploadRequestRespMsgBody(source: String, filename: String, userId: String, success: Boolean, timestamp: Long, token: String = null) + +object FileUploadedEvtMsg { val NAME = "FileUploadedEvtMsg" } +case class FileUploadedEvtMsg(header: BbbClientMsgHeader, body: FileUploadedEvtMsgBody) extends StandardMsg +case class FileUploadedEvtMsgBody(uploadId: String, source: String, filename: String) + +// akka-apps to bbb-web +object UploadRequestSysMsg { val NAME = "UploadRequestSysMsg" } +case class UploadRequestSysMsg(header: BbbCoreHeaderWithMeetingId, body: UploadRequestSysMsgBody) extends BbbCoreMsg +case class UploadRequestSysMsgBody(source: String, filename: String, userId: String, token: String) + +// bbb-web to akka-apps +object FileUploadedSysMsg { val NAME = "FileUploadedSysMsg" } +case class FileUploadedSysMsg(header: BbbClientMsgHeader, body: FileUploadedSysMsgBody) extends StandardMsg +case class FileUploadedSysMsgBody(uploadId: String, source: String, filename: String, contentType: String) 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 bf918aa9493b..247ff1156188 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 @@ -38,6 +38,7 @@ import org.bigbluebutton.api.domain.RegisteredUser; import org.bigbluebutton.api.domain.User; import org.bigbluebutton.api.domain.UserSession; +import org.bigbluebutton.api.domain.UploadedFile; import org.bigbluebutton.api.messaging.MessageListener; import org.bigbluebutton.api.messaging.converters.messages.DestroyMeetingMessage; import org.bigbluebutton.api.messaging.converters.messages.EndMeetingMessage; @@ -272,6 +273,33 @@ private void kickOffProcessingOfRecording(Meeting m) { } } + public Boolean isUploadRequestValid(String meetingId, String source, String filename, String userId, String token) { + Meeting m = getMeeting(meetingId); + if (m != null) { + return m.isUploadRequestValid(source, filename, userId, token); + } else { + return false; + } + } + + public Boolean isDownloadRequestValid(String meetingId, String source, String uploadId) { + Meeting m = getMeeting(meetingId); + if (m != null) { + return m.hasUploadedFile(source, uploadId); + } else { + return false; + } + } + + public UploadedFile getUploadedFile(String meetingId, String uploadId) { + Meeting m = getMeeting(meetingId); + if (m != null) { + return m.getUploadedFile(uploadId); + } else { + return null; + } + } + public Boolean authzTokenIsValid(String authzToken) { // Note we DO NOT expire the token return uploadAuthzTokens.containsKey(authzToken); } @@ -617,6 +645,22 @@ public void processRecording(Meeting m) { } } + public void fileUploaded( + String uploadId, + String source, + String filename, + String contentType, + String extension, + String userId, + String meetingId + ) { + Meeting m = getMeeting(meetingId); + if (m != null) { + m.addUploadedFile(source, filename, contentType, extension, uploadId); + gw.fileUploaded(uploadId, source, filename, contentType, userId, meetingId); + } + } + public void endMeeting(String meetingId) { handle(new EndMeeting(meetingId)); } @@ -734,6 +778,13 @@ private void processGuestStatusChangedEventMsg(GuestStatusChangedEventMsg messag } + private void processUploadRequest(UploadRequest message) { + Meeting m = getMeeting(message.meetingId); + if (m != null) { + m.addUploadRequest(message.source, message.filename, message.userId, message.token); + } + } + private void processPresentationUploadToken(PresentationUploadToken message) { uploadAuthzTokens.put(message.authzToken, message); } @@ -1157,6 +1208,8 @@ public void run() { processRegisterUser((RegisterUser) message); } else if (message instanceof CreateBreakoutRoom) { processCreateBreakoutRoom((CreateBreakoutRoom) message); + } else if (message instanceof UploadRequest) { + processUploadRequest((UploadRequest) message); } else if (message instanceof PresentationUploadToken) { processPresentationUploadToken((PresentationUploadToken) message); } else if (message instanceof PositionInWaitingQueueUpdated) { 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 10668b4329f0..d7104a0923c2 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 @@ -128,6 +128,8 @@ public class ParamsProcessorUtil { private boolean defaultLockSettingsHideViewersCursor; private Long maxPresentationFileUpload = 30000000L; // 30MB + private Long maxUploadSize = 900000000L; // 900MB - but probably doesn't matter + private String uploadDir; private Integer clientLogoutTimerInMinutes = 0; private Integer defaultMeetingExpireIfNoUserJoinedInMinutes = 5; @@ -1331,6 +1333,22 @@ public void setMaxPresentationFileUpload(Long maxFileSize) { public Long getMaxPresentationFileUpload() { return maxPresentationFileUpload; } + + public void setMaxUploadSize(Long maxUploadSize) { + this.maxUploadSize = maxUploadSize; + } + + public Long getMaxUploadSize() { + return maxUploadSize; + } + + public void setUploadDir(String uploadDir) { + this.uploadDir = uploadDir; + } + + public String getUploadDir() { + return uploadDir; + } public void setMuteOnStart(Boolean mute) { defaultMuteOnStart = mute; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/Util.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/Util.java index c76d0b8d1bd5..311a6860d42a 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/Util.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/Util.java @@ -123,4 +123,27 @@ public static void makePresentationDownloadable( downloadMarker.createNewFile(); } } + + public static String generateUploadId(String filename) { + long timestamp = System.currentTimeMillis(); + return DigestUtils.sha1Hex(filename) + "-" + timestamp; + } + + public static File createUploadDir(String rootDir, String source, String meetingId, String uploadId) { + String meetingsPath = rootDir + File.separatorChar + "meetings"; + String sourcePath = meetingsPath + File.separatorChar + meetingId + File.separatorChar + source; + String uploadPath = sourcePath + File.separatorChar + uploadId; + File uploadDir = new File(uploadPath); + if (uploadDir.mkdirs()) { + return uploadDir; + } + return null; + } + + public static String getDownloadPath(String rootDir, String source, String meetingId, String uploadId) { + String meetingsPath = rootDir + File.separatorChar + "meetings"; + String sourcePath = meetingsPath + File.separatorChar + meetingId + File.separatorChar + source; + String downloadPath = sourcePath + File.separatorChar + uploadId; + return downloadPath; + } } 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 1a45f0acf68b..a8d186a130db 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 @@ -104,6 +104,9 @@ public class Meeting { private String presentationUploadExternalDescription; private String presentationUploadExternalUrl; + private HashMap uploadRequests = new HashMap(); + private HashMap uploadedFiles = new HashMap(); + private Integer meetingExpireIfNoUserJoinedInMinutes = 5; private Integer meetingExpireWhenLastUserLeftInMinutes = 1; private Integer userInactivityInspectTimerInMinutes = 120; @@ -192,6 +195,16 @@ public List getBreakoutRooms() { return breakoutRooms; } + public void addUploadRequest(String source, String filename, String userId, String token) { + UploadRequest uploadRequest = new UploadRequest(source, filename, userId); + uploadRequests.put(token, uploadRequest); + } + + public void addUploadedFile(String source, String filename, String contentType, String extension, String uploadId) { + UploadedFile uploadedFile = new UploadedFile(source, filename, contentType, extension); + uploadedFiles.put(uploadId, uploadedFile); + } + public Map getMetadata() { return metadata; } @@ -266,6 +279,29 @@ public void setGuestStatusWithId(String userId, String guestStatus) { } + public Boolean isUploadRequestValid(String source, String filename, String userId, String token) { + UploadRequest uploadRequest = uploadRequests.get(token); + if (uploadRequest != null) { + return uploadRequest.isValid(source, filename, userId); + } else { + return false; + } + } + + public Boolean hasUploadedFile(String source, String uploadId) { + UploadedFile uploadedFile = uploadedFiles.get(uploadId); + if (uploadedFile != null) { + return source.equals(uploadedFile.source); + } else { + return false; + } + } + + public UploadedFile getUploadedFile(String uploadId) { + UploadedFile uploadedFile = uploadedFiles.get(uploadId); + return uploadedFile; + } + public RegisteredUser getRegisteredUserWithAuthToken(String authToken) { for (RegisteredUser ruser : registeredUsers.values()) { if (ruser.authToken.equals(authToken)) { diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/UploadRequest.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/UploadRequest.java new file mode 100644 index 000000000000..2863c18ed51a --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/UploadRequest.java @@ -0,0 +1,28 @@ +package org.bigbluebutton.api.domain; + +public class UploadRequest { + public final String source; + public final String filename; + public final String userId; + private Boolean valid; + + public UploadRequest(String source, String filename, String userId) { + this.source = source; + this.filename = filename; + this.userId = userId; + this.valid = true; + } + + public Boolean isValid(String source, String filename, String userId) { + if (this.valid) { + // Invalidate this upload request after the first check + this.valid = false; + Boolean validSource = source != null && source.equals(this.source); + Boolean validFilename = filename != null && filename.equals(this.filename); + Boolean validUserId = userId != null && userId.equals(this.userId); + return validSource && validFilename && validUserId; + } else { + return false; + } + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/UploadedFile.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/UploadedFile.java new file mode 100644 index 000000000000..477a754d38a3 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/UploadedFile.java @@ -0,0 +1,15 @@ +package org.bigbluebutton.api.domain; + +public class UploadedFile { + public final String source; + public final String filename; + public final String contentType; + public final String extension; + + public UploadedFile(String source, String filename, String contentType, String extension) { + this.source = source; + this.filename = filename; + this.contentType = contentType; + this.extension = extension; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/UploadRequest.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/UploadRequest.java new file mode 100644 index 000000000000..2eec4c72e150 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/UploadRequest.java @@ -0,0 +1,17 @@ +package org.bigbluebutton.api.messaging.messages; + +public class UploadRequest implements IMessage { + public final String meetingId; + public final String source; + public final String filename; + public final String userId; + public final String token; + + public UploadRequest(String meetingId, String source, String filename, String userId, String token) { + this.meetingId = meetingId; + this.source = source; + this.filename = filename; + this.userId = userId; + this.token = token; + } +} 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 cfb28e33d8ab..eef16046da15 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 @@ -55,6 +55,14 @@ void registerUser(String meetingID, String internalUserId, String fullname, Stri void destroyMeeting(DestroyMeetingMessage msg); void endMeeting(EndMeetingMessage msg); + void fileUploaded( + String uploadId, + String source, + String filename, + String contentType, + String userId, + String meetingId + ); void sendKeepAlive(String system, Long bbbWebTimestamp, Long akkaAppsTimestamp); void publishedRecording(PublishedRecordingMessage msg); void unpublishedRecording(UnpublishedRecordingMessage msg); 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 22ec7e5245f5..a3b91c64b484 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 @@ -293,6 +293,18 @@ class BbbWebApiGWApp( msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) } + def fileUploaded( + uploadId: String, + source: String, + filename: String, + contentType: String, + userId: String, + meetingId: String + ): Unit = { + val event = MsgBuilder.buildFileUploadedSysMsg(uploadId, source, filename, contentType, userId, meetingId) + msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) + } + def publishedRecording(msg: PublishedRecordingMessage): Unit = { val event = MsgBuilder.buildPublishedRecordingSysMsg(msg) // Probably violating something here, but a new event bus looks just too much for this diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala index 09532942bd66..562f50785d66 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala @@ -104,6 +104,22 @@ object MsgBuilder { BbbCommonEnvCoreMsg(envelope, req) } + def buildFileUploadedSysMsg( + uploadId: String, + source: String, + filename: String, + contentType: String, + userId: String, + meetingId: String + ): BbbCommonEnvCoreMsg = { + val routing = collection.immutable.HashMap("sender" -> "bbb-web") + val envelope = BbbCoreEnvelope(FileUploadedSysMsg.NAME, routing) + val header = BbbClientMsgHeader(FileUploadedSysMsg.NAME, meetingId, userId) + val body = FileUploadedSysMsgBody(uploadId, source, filename, contentType) + val req = FileUploadedSysMsg(header, body) + BbbCommonEnvCoreMsg(envelope, req) + } + def buildPresentationPageGeneratedPubMsg(msg: DocPageGeneratedProgress): BbbCommonEnvCoreMsg = { val routing = collection.immutable.HashMap("sender" -> "bbb-web") val envelope = BbbCoreEnvelope(PresentationPageGeneratedSysPubMsg.NAME, routing) diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala index a7f9d8f5ed83..1f9a624ab466 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala @@ -88,6 +88,8 @@ class ReceivedJsonMsgHdlrActor(val msgFromAkkaAppsEventBus: MsgFromAkkaAppsEvent route[UserRoleChangedEvtMsg](envelope, jsonNode) case CreateBreakoutRoomSysCmdMsg.NAME => route[CreateBreakoutRoomSysCmdMsg](envelope, jsonNode) + case UploadRequestSysMsg.NAME => + route[UploadRequestSysMsg](envelope, jsonNode) case PresentationUploadTokenSysPubMsg.NAME => route[PresentationUploadTokenSysPubMsg](envelope, jsonNode) case GuestsWaitingApprovedEvtMsg.NAME => diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala index 6e4092228d7a..c6d396db0101 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala @@ -36,6 +36,7 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW) case m: UserBroadcastCamStartedEvtMsg => handleUserBroadcastCamStartedEvtMsg(m) case m: UserBroadcastCamStoppedEvtMsg => handleUserBroadcastCamStoppedEvtMsg(m) case m: CreateBreakoutRoomSysCmdMsg => handleCreateBreakoutRoomSysCmdMsg(m) + case m: UploadRequestSysMsg => handleUploadRequestSysMsg(m) case m: PresentationUploadTokenSysPubMsg => handlePresentationUploadTokenSysPubMsg(m) case m: GuestsWaitingApprovedEvtMsg => handleGuestsWaitingApprovedEvtMsg(m) case m: PosInWaitingQueueUpdatedRespMsg => handlePosInWaitingQueueUpdatedRespMsg(m) @@ -168,6 +169,10 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW) olgMsgGW.handle(new UserRoleChanged(msg.header.meetingId, msg.body.userId, msg.body.role)) } + def handleUploadRequestSysMsg(msg: UploadRequestSysMsg): Unit = { + olgMsgGW.handle(new UploadRequest(msg.header.meetingId, msg.body.source, msg.body.filename, msg.body.userId, msg.body.token)) + } + def handlePresentationUploadTokenSysPubMsg(msg: PresentationUploadTokenSysPubMsg): Unit = { olgMsgGW.handle(new PresentationUploadToken(msg.body.podId, msg.body.authzToken, msg.body.filename, msg.body.meetingId)) } diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js index e851e3036d2f..e11831b84cdc 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js @@ -21,6 +21,8 @@ import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserIn import clearConnectionStatus from '/imports/api/connection-status/server/modifiers/clearConnectionStatus'; import clearScreenshare from '/imports/api/screenshare/server/modifiers/clearScreenshare'; import clearAudioCaptions from '/imports/api/audio-captions/server/modifiers/clearAudioCaptions'; +import clearUploadRequest from '/imports/api/upload/server/modifiers/clearUploadRequest'; +import clearUploadedFile from '/imports/api/upload/server/modifiers/clearUploadedFile'; import clearMeetingTimeRemaining from '/imports/api/meetings/server/modifiers/clearMeetingTimeRemaining'; import clearLocalSettings from '/imports/api/local-settings/server/modifiers/clearLocalSettings'; import clearRecordMeeting from './clearRecordMeeting'; @@ -56,6 +58,8 @@ export default async function meetingHasEnded(meetingId) { clearVoiceUsers(meetingId), clearUserInfo(meetingId), clearConnectionStatus(meetingId), + clearUploadRequest(meetingId), + clearUploadedFile(meetingId), clearAudioCaptions(meetingId), clearLocalSettings(meetingId), clearMeetingTimeRemaining(meetingId), diff --git a/bigbluebutton-html5/imports/api/upload/index.js b/bigbluebutton-html5/imports/api/upload/index.js new file mode 100644 index 000000000000..7ad70ab6a365 --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/index.js @@ -0,0 +1,14 @@ +import { Meteor } from 'meteor/meteor'; + +const UploadRequest = new Mongo.Collection('upload-request'); +const UploadedFile = new Mongo.Collection('uploaded-file'); + +if (Meteor.isServer) { + UploadRequest._ensureIndex({ meetingId: 1 }); + UploadedFile._ensureIndex({ meetingId: 1 }); +} + +export { + UploadRequest, + UploadedFile, +}; diff --git a/bigbluebutton-html5/imports/api/upload/server/eventHandlers.js b/bigbluebutton-html5/imports/api/upload/server/eventHandlers.js new file mode 100644 index 000000000000..d0e5f7171880 --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/eventHandlers.js @@ -0,0 +1,6 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import handleUploadRequestResp from './handlers/uploadRequestResp'; +import handleFileUploadedEvt from './handlers/fileUploadedEvt'; + +RedisPubSub.on('UploadRequestRespMsg', handleUploadRequestResp); +RedisPubSub.on('FileUploadedEvtMsg', handleFileUploadedEvt); diff --git a/bigbluebutton-html5/imports/api/upload/server/handlers/fileUploadedEvt.js b/bigbluebutton-html5/imports/api/upload/server/handlers/fileUploadedEvt.js new file mode 100644 index 000000000000..14e627f2cbea --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/handlers/fileUploadedEvt.js @@ -0,0 +1,24 @@ +import { check } from 'meteor/check'; +import { isNotificationEnabled } from '../helpers'; +import addNotification from '../modifiers/addNotification'; +import addUploadedFile from '../modifiers/addUploadedFile'; + +export default function handleFileUploadedEvt({ header, body }, meetingId) { + check(body, Object); + check(header, Object); + check(meetingId, String); + + const { userId } = header; + + const { + uploadId, + source, + filename, + } = body; + + if (isNotificationEnabled()) { + addNotification(meetingId, userId, uploadId, source, filename); + } + + return addUploadedFile(meetingId, userId, uploadId, source, filename); +} diff --git a/bigbluebutton-html5/imports/api/upload/server/handlers/uploadRequestResp.js b/bigbluebutton-html5/imports/api/upload/server/handlers/uploadRequestResp.js new file mode 100644 index 000000000000..f4f0c110ecd2 --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/handlers/uploadRequestResp.js @@ -0,0 +1,18 @@ +import { check } from 'meteor/check'; +import addUploadRequest from '../modifiers/addUploadRequest'; + +export default function handleUploadRequestResp({ body }, meetingId) { + check(body, Object); + check(meetingId, String); + + const { + source, + filename, + userId, + success, + timestamp, + token, + } = body; + + return addUploadRequest(meetingId, source, filename, userId, success, timestamp, token); +} diff --git a/bigbluebutton-html5/imports/api/upload/server/helpers.js b/bigbluebutton-html5/imports/api/upload/server/helpers.js new file mode 100644 index 000000000000..e4d5df9e94d8 --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/helpers.js @@ -0,0 +1,9 @@ +import { Meteor } from 'meteor/meteor'; + +const UPLOAD = Meteor.settings.public.upload; + +const isNotificationEnabled = () => UPLOAD.notification; + +export { + isNotificationEnabled, +}; diff --git a/bigbluebutton-html5/imports/api/upload/server/index.js b/bigbluebutton-html5/imports/api/upload/server/index.js new file mode 100644 index 000000000000..92451ac76bf2 --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/index.js @@ -0,0 +1,3 @@ +import './eventHandlers'; +import './methods'; +import './publishers'; diff --git a/bigbluebutton-html5/imports/api/upload/server/methods.js b/bigbluebutton-html5/imports/api/upload/server/methods.js new file mode 100644 index 000000000000..a410dd7a218c --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/methods.js @@ -0,0 +1,6 @@ +import { Meteor } from 'meteor/meteor'; +import requestUpload from './methods/requestUpload'; + +Meteor.methods({ + requestUpload, +}); diff --git a/bigbluebutton-html5/imports/api/upload/server/methods/requestUpload.js b/bigbluebutton-html5/imports/api/upload/server/methods/requestUpload.js new file mode 100644 index 000000000000..4cd5b1ffd47b --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/methods/requestUpload.js @@ -0,0 +1,27 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +//export default function requestUpload(credentials, source, filename, timestamp) { +export default function requestUpload(source, filename, timestamp) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'UploadRequestReqMsg'; + + //const { meetingId, requesterUserId } = credentials; +const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + check(source, String); + check(filename, String); + check(timestamp, Number) + + const payload = { + source, + filename, + timestamp, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} diff --git a/bigbluebutton-html5/imports/api/upload/server/modifiers/addNotification.js b/bigbluebutton-html5/imports/api/upload/server/modifiers/addNotification.js new file mode 100644 index 000000000000..df8d7cad705c --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/modifiers/addNotification.js @@ -0,0 +1,55 @@ +import flat from 'flat'; +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import { GroupChatMsg } from '/imports/api/group-chat-msg'; + +const CHAT = Meteor.settings.public.chat; +const SYSTEM_ID = CHAT.type_system; +const CHAT_ID = 'MAIN-PUBLIC-GROUP-CHAT'; +const CHAT_CONFIG = Meteor.settings.public.chat; +const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system; + +export default function addUploadedFileMsg(meetingId, userId, uploadId, source, filename) { + check(meetingId, String); + check(userId, String); + check(uploadId, String); + check(source, String); + check(filename, String); + + const now = Date.now(); + + const upload = { + uploadId, + source, + filename, + }; + + const selector = { + meetingId, + chatId: CHAT_ID, + id: `${SYSTEM_CHAT_TYPE}-upload-msg-${now}`, + }; + + const modifier = { + $set: { + meetingId, + chatId: CHAT_ID, + message: '', + upload, + sender: SYSTEM_ID, + timestamp: now, + }, + }; + + try { + const { insertedId, numberAffected } = GroupChatMsg.upsert(selector, modifier); + + if (insertedId) { + Logger.info(`Added group-chat-msg uploaded file meetingId=${meetingId}`); + } else if (numberAffected){ + Logger.info(`Upserted group-chat-msg uploaded file meetingId=${meetingId}`); + } + } catch (err) { + Logger.error(`Adding group-chat-msg uploaded file to collection: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/upload/server/modifiers/addUploadRequest.js b/bigbluebutton-html5/imports/api/upload/server/modifiers/addUploadRequest.js new file mode 100644 index 000000000000..312bb0d7ef06 --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/modifiers/addUploadRequest.js @@ -0,0 +1,42 @@ +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import { UploadRequest } from '/imports/api/upload'; + +export default function addUploadRequest(meetingId, source, filename, userId, success, timestamp, token) { + check(source, String); + check(filename, String); + check(userId, String); + check(success, Boolean); + check(timestamp, Number); + + if (success) { + check(token, String); + } + + const selector = { + meetingId, + source, + userId, + filename, + timestamp, + }; + + const modifier = { + meetingId, + source, + userId, + filename, + success, + timestamp, + token, + }; + + try { + const { numberAffected } = UploadRequest.upsert(selector, modifier); + if (numberAffected) { + Logger.debug(`Upserting upload request filename=${filename} user=${userId} meeting=${meetingId}`); + } + } catch (err) { + Logger.error(`Upserting upload request: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/upload/server/modifiers/addUploadedFile.js b/bigbluebutton-html5/imports/api/upload/server/modifiers/addUploadedFile.js new file mode 100644 index 000000000000..1999517158bc --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/modifiers/addUploadedFile.js @@ -0,0 +1,33 @@ +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import { UploadedFile } from '/imports/api/upload'; + +export default function addUploadedFile(meetingId, userId, uploadId, source, filename) { + check(meetingId, String); + check(userId, String); + check(uploadId, String); + check(source, String); + check(filename, String); + + const selector = { + meetingId, + uploadId, + }; + + const modifier = { + meetingId, + userId, + uploadId, + source, + filename, + }; + + try { + const { numberAffected } = UploadedFile.upsert(selector, modifier); + if (numberAffected) { + Logger.debug(`Upserting uploaded file filename=${filename} meeting=${meetingId} source=${source}`); + } + } catch (err) { + Logger.error(`Upserting upload file: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/upload/server/modifiers/clearUploadRequest.js b/bigbluebutton-html5/imports/api/upload/server/modifiers/clearUploadRequest.js new file mode 100644 index 000000000000..b8b1bf3ee50b --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/modifiers/clearUploadRequest.js @@ -0,0 +1,15 @@ +import { UploadRequest } from '/imports/api/upload'; +import Logger from '/imports/startup/server/logger'; + +export default function clearUploadRequest(meetingId) { + if (meetingId) { + return UploadRequest.remove({ meetingId }, () => { + Logger.info(`Cleared UploadRequest (${meetingId})`); + }); + } + + // clearing upload requests for the whole server + return UploadRequest.remove({}, () => { + Logger.info('Cleared UploadRequest (all)'); + }); +} diff --git a/bigbluebutton-html5/imports/api/upload/server/modifiers/clearUploadedFile.js b/bigbluebutton-html5/imports/api/upload/server/modifiers/clearUploadedFile.js new file mode 100644 index 000000000000..98fec2dffc4f --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/modifiers/clearUploadedFile.js @@ -0,0 +1,15 @@ +import { UploadedFile } from '/imports/api/upload'; +import Logger from '/imports/startup/server/logger'; + +export default function clearUploadedFile(meetingId) { + if (meetingId) { + return UploadedFile.remove({ meetingId }, () => { + Logger.info(`Cleared UploadedFile (${meetingId})`); + }); + } + + // clearing uploaded files for the whole server + return UploadedFile.remove({}, () => { + Logger.info('Cleared UploadedFile (all)'); + }); +} diff --git a/bigbluebutton-html5/imports/api/upload/server/publishers.js b/bigbluebutton-html5/imports/api/upload/server/publishers.js new file mode 100644 index 000000000000..01e450147b57 --- /dev/null +++ b/bigbluebutton-html5/imports/api/upload/server/publishers.js @@ -0,0 +1,54 @@ +import { Meteor } from 'meteor/meteor'; +//import { check } from 'meteor/check'; +import { + UploadRequest, + UploadedFile, +} from '/imports/api/upload'; +import Logger from '/imports/startup/server/logger'; +//Suga +import { extractCredentials } from '/imports/api/common/server/helpers'; + +// Somebody still sends credentials. This will be a security issue... +function uploadRequest(credentials, source, filename) { + if (!this.userId) { + return UploadRequest.find({ meetingId: ''}); + } + //const { meetingId, requesterUserId } = credentials; // this also works + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + const selector = { + meetingId, + userId: requesterUserId, + source, + filename, + }; + + Logger.debug(`Publishing upload request for ${meetingId} ${requesterUserId}`); + + return UploadRequest.find(selector); +} + +function publishRequest(...args) { + const boundUploadRequest = uploadRequest.bind(this); + return boundUploadRequest(...args); +} + +Meteor.publish('upload-request', publishRequest); + +function uploadedFile() { + if (!this.userId) { + return UploadedFile.find({ meetingId: ''}); + } + const { meetingId } = extractCredentials(this.userId); + + Logger.debug(`Publishing uploaded file for ${meetingId}`); + + return UploadedFile.find({ meetingId }); +} + +function publishUpload(...args) { + const boundUploadedFile = uploadedFile.bind(this); + return boundUploadedFile(...args); +} + +Meteor.publish('uploaded-file', publishUpload); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index d87edbf71acb..7926ee59631b 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -2,6 +2,8 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { defineMessages } from 'react-intl'; +//import Button from '/imports/ui/components/button/component'; +import MediaUploadContainer from '/imports/ui/components/upload/media/container'; import { withModalMounter } from '/imports/ui/components/common/modal/service'; import withShortcutHelper from '/imports/ui/components/shortcut-help/service'; import ExternalVideoModal from '/imports/ui/components/external-video-player/modal/container'; @@ -24,6 +26,7 @@ const propTypes = { shortcuts: PropTypes.string, handleTakePresenter: PropTypes.func.isRequired, allowExternalVideo: PropTypes.bool.isRequired, + isMediaUploadEnabled: PropTypes.bool.isRequired, stopExternalVideoShare: PropTypes.func.isRequired, isMobile: PropTypes.bool.isRequired, setMeetingLayout: PropTypes.func.isRequired, @@ -49,6 +52,14 @@ const intlMessages = defineMessages({ id: 'app.actionsBar.actionsDropdown.presentationDesc', description: 'adds context to upload presentation option', }, + uploadMediaLabel: { + id: 'app.actionsBar.actionsDropdown.uploadMediaLabel', + description: 'Upload media option label', + }, + uploadMediaDesc: { + id: 'app.actionsBar.actionsDropdown.uploadMediaDesc', + description: 'adds context to upload media option', + }, desktopShareDesc: { id: 'app.actionsBar.actionsDropdown.desktopShareDesc', description: 'adds context to desktop share option', @@ -106,12 +117,14 @@ class ActionsDropdown extends PureComponent { super(props); this.presentationItemId = _.uniqueId('action-item-'); + this.uploadMediaId = _.uniqueId('action-item-'); this.pollId = _.uniqueId('action-item-'); this.takePresenterId = _.uniqueId('action-item-'); this.selectUserRandId = _.uniqueId('action-item-'); this.handleExternalVideoClick = this.handleExternalVideoClick.bind(this); this.makePresentationItems = this.makePresentationItems.bind(this); + this.handleUploadMediaClick = this.handleUploadMediaClick.bind(this); } componentDidUpdate(prevProps) { @@ -132,6 +145,7 @@ class ActionsDropdown extends PureComponent { intl, amIPresenter, allowExternalVideo, + isMediaUploadEnabled, handleTakePresenter, isSharingVideo, isPollingEnabled, @@ -149,6 +163,8 @@ class ActionsDropdown extends PureComponent { pollBtnLabel, presentationLabel, takePresenter, + uploadMediaLabel, + uploadMediaDesc, } = intlMessages; const { @@ -241,6 +257,15 @@ class ActionsDropdown extends PureComponent { }); } + if (amIPresenter && isMediaUploadEnabled) { + actions.push({ + icon: "upload", + label: formatMessage(uploadMediaLabel), + key: this.uploadMediaId, + onClick: this.handleUploadMediaClick, + }) + } + return actions; } @@ -281,6 +306,11 @@ class ActionsDropdown extends PureComponent { return presentationItemElements; } + handleUploadMediaClick() { + const { mountModal } = this.props; + mountModal(); + } + render() { const { intl, diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index d0e1d262ff8a..85bcb26e597d 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -32,6 +32,7 @@ class ActionsBar extends PureComponent { isRaiseHandButtonEnabled, isThereCurrentPresentation, allowExternalVideo, + isMediaUploadEnabled, setEmojiStatus, currentUser, layoutContextDispatch, @@ -58,6 +59,7 @@ class ActionsBar extends PureComponent { isPollingEnabled, isSelectRandomUserEnabled, allowExternalVideo, + isMediaUploadEnabled, handleTakePresenter, intl, isSharingVideo, diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index 52b2ca3e3bb5..7289972d9865 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -11,6 +11,7 @@ import ActionsBar from './component'; import Service from './service'; import UserListService from '/imports/ui/components/user-list/service'; import ExternalVideoService from '/imports/ui/components/external-video-player/service'; +import MediaUploadService from '/imports/ui/components/upload/media/service'; import CaptionsService from '/imports/ui/components/captions/service'; import { layoutSelectOutput, layoutDispatch } from '../layout/context'; import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service'; @@ -66,6 +67,7 @@ export default withTracker(() => ({ isRaiseHandButtonEnabled: RAISE_HAND_BUTTON_ENABLED, isThereCurrentPresentation: Presentations.findOne({ meetingId: Auth.meetingID, current: true }, { fields: {} }), + isMediaUploadEnabled: MediaUploadService.isEnabled(), allowExternalVideo: isExternalVideoEnabled(), setEmojiStatus: UserListService.setEmojiStatus, }))(injectIntl(ActionsBarContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx index f6fd92281949..05b75b4debcd 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx @@ -123,7 +123,7 @@ const QuickPollDropdown = (props) => { itemLabel = options.join('/').replace(/[\n.)]/g, ''); if (type === _pollTypes.Custom) { for (let i = 0; i < options.length; i += 1) { - const letterOption = options[i]?.replace(/[\r.)]/g, '').toUpperCase(); + const letterOption = options[i]?.replace(/[\r.)]/g, ''); if (letterAnswers.length < MAX_CUSTOM_FIELDS) { letterAnswers.push(letterOption); } else { @@ -131,18 +131,19 @@ const QuickPollDropdown = (props) => { } } } + itemLabel = options.map(function(item){ return item.slice(0,1); }).slice(0,MAX_CUSTOM_FIELDS).join('/'); } // removes any whitespace from the label itemLabel = itemLabel?.replace(/\s+/g, '').toUpperCase(); - const numChars = { - 1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E', - }; - itemLabel = itemLabel.split('').map((c) => { - if (numChars[c]) return numChars[c]; - return c; - }).join(''); + //const numChars = { + // 1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E', 6: 'F', 7: 'G', 8: 'H', 9: 'I', + //}; + //itemLabel = itemLabel.split('').map((c) => { + // if (numChars[c]) return numChars[c]; + // return c; + //}).join(''); return ( { const sizes = []; return pollItemElements.filter((el) => { const { label } = el.props; - if (label.length === sizes[sizes.length - 1]) return false; + //if (label.length === sizes[sizes.length - 1]) return false; sizes.push(label.length); return el; }); @@ -215,7 +216,8 @@ const QuickPollDropdown = (props) => { startPoll( pollTypes.Custom, currentSlide.id, - optionsWithLabels, + //optionsWithLabels, + answers, pollQuestion, multiResponse, ); diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js index 8b45a3c5658c..f9230ddfe9d9 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/service.js +++ b/bigbluebutton-html5/imports/ui/components/chat/service.js @@ -104,11 +104,18 @@ const reduceGroupMessages = (previous, current) => { const lastMessage = previous[previous.length - 1]; const currentMessage = current; currentMessage.content = [{ +// const content = { id: current.id, text: current.message, time: current.timestamp, color: current.color, }]; +// }; +// // I (Pedro) do not like this + //-> [21.05.05 for BBB2.3] It is transferring "upload" under curreneMessage.content, + // which is done now by ui/component/components-data/chat-context/context.jsx +// if (current.upload) content['upload'] = current.upload; +// currentMessage.content = [content]; if (!lastMessage) { return previous.concat(currentMessage); } diff --git a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/component.jsx index d05eb220720d..db375d8b6c09 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/component.jsx @@ -4,6 +4,7 @@ import { FormattedTime, defineMessages, injectIntl } from 'react-intl'; import _ from 'lodash'; import UserAvatar from '/imports/ui/components/user-avatar/component'; import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger'; +import UploadService from '/imports/ui/components/upload/service'; import PollService from '/imports/ui/components/poll/service'; import Styled from './styles'; @@ -115,15 +116,15 @@ class TimeWindowChatItem extends PureComponent { ref={element => this.itemRef = element} > {messages.map((message) => ( - message.text !== '' + message.text !== '' || message.upload ? ( ; + } else { + return null; + } + } + render() { const { videoUrl, @@ -702,6 +714,7 @@ class VideoPlayer extends Component { ] : null } + {this.renderExternalVideoClose()} ); diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/externalvideo-close-button/component.jsx b/bigbluebutton-html5/imports/ui/components/external-video-player/externalvideo-close-button/component.jsx new file mode 100644 index 000000000000..2bdafb29bce3 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/externalvideo-close-button/component.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +//import Button from '/imports/ui/components/button/component'; +//import { styles } from './styles'; +import Styled from '../styles'; +import ExternalVideoService from '/imports/ui/components/external-video-player/service'; + +const intlMessages = defineMessages({ + closeExternalVideoLabel: { + id: 'app.actionsBar.actionsDropdown.stopShareExternalVideo', + description: 'Close external video label', + }, +}); + +const CloseExternalVideoComponent = ({ intl }) => ( + + +); + +export default injectIntl(CloseExternalVideoComponent); diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/modal/component.jsx b/bigbluebutton-html5/imports/ui/components/external-video-player/modal/component.jsx index 6681aae7b407..004a80753980 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/modal/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/modal/component.jsx @@ -1,5 +1,9 @@ import React, { Component } from 'react'; import { withModalMounter } from '/imports/ui/components/common/modal/service'; +import Icon from '/imports/ui/components/common/icon/component'; +//import Modal from '/imports/ui/components/modal/simple/component'; +//import Button from '/imports/ui/components/button/component'; +import UploadMediaService from '/imports/ui/components/upload/media/service'; import { defineMessages, injectIntl } from 'react-intl'; import { isUrlValid } from '../service'; import Settings from '/imports/ui/services/settings'; @@ -30,6 +34,10 @@ const intlMessages = defineMessages({ id: 'app.externalVideo.close', description: 'Close', }, + filename: { + id: 'app.externalVideo.filename', + description: 'Media filename', + }, note: { id: 'app.externalVideo.noteLabel', description: 'provides hint about Shared External videos', @@ -51,6 +59,7 @@ class ExternalVideoModal extends Component { this.updateVideoUrlHandler = this.updateVideoUrlHandler.bind(this); this.renderUrlError = this.renderUrlError.bind(this); this.updateVideoUrlHandler = this.updateVideoUrlHandler.bind(this); + this.onMediaFileClick = this.onMediaFileClick.bind(this); } startWatchingHandler() { @@ -69,6 +78,18 @@ class ExternalVideoModal extends Component { this.setState({ url: ev.target.value }); } + onMediaFileClick(id) { + const { + startWatching, + closeModal, + } = this.props; + + const url = UploadMediaService.getDownloadURL(id); + + startWatching(url.trim()); + closeModal(); + } + renderUrlError() { const { intl } = this.props; const { url } = this.state; @@ -87,6 +108,52 @@ class ExternalVideoModal extends Component { ); } + renderItem(item) { + const { intl } = this.props; + + return ( + this.onMediaFileClick(item.uploadId)} + > + + + + + + + {item.filename} + + + ); + } + + renderFiles() { + const { + intl, + files, + } = this.props; + + if (files.length === 0) return null; + + return ( +
+ + + + + + + + {files.map(item => this.renderItem(item))} + +
+ {intl.formatMessage(intlMessages.filename)} +
+
+ ); + } + render() { const { intl, closeModal } = this.props; const { url, sharing } = this.state; @@ -126,6 +193,7 @@ class ExternalVideoModal extends Component { {this.renderUrlError()} + {this.renderFiles()} ; @@ -11,5 +12,6 @@ export default withModalMounter(withTracker(({ mountModal }) => ({ mountModal(null); }, startWatching, + files: UploadMediaService.getMediaFiles(), videoUrl: getVideoUrl(), }))(ExternalVideoModalContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/modal/styles.js b/bigbluebutton-html5/imports/ui/components/external-video-player/modal/styles.js index 3bc986cd64cc..50d0fc0235d6 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/modal/styles.js +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/modal/styles.js @@ -18,13 +18,93 @@ import Button from '/imports/ui/components/common/button/component'; const UrlError = styled.div` color: red; - padding: 1em 0 2.5em 0; + padding: 1em 0; ${({ animations }) => animations && ` transition: 1s; `} `; +const Icon = styled.div` + width: 1%; + & > i { + font-size: 1.35rem; + } +`; + +const Item = styled.tr` + cursor: pointer; +`; + +const Name = styled.th` + height: 1rem; + width: auto; + position: relative; + &:before { + content: "\00a0"; + visibility: hidden; + } + & > span { + @extend %text-elipsis; + position: absolute; + left: 0; + right: 0; + [dir="rtl"] & { + right: 1rem; + } + } +`; + +const Hidden = styled.div` + position: absolute; + overflow: hidden; + clip: rect(0 0 0 0); + height: 1px; width: 1px; + margin: -1px; padding: 0; border: 0; +`; + +const List = styled.div` + /*@include scrollbox-vertical();*/ + max-height: 35vh; + width: 100%; + padding: .5rem 0; +`; + +const Table = styled.div` + width: 100%; + border-spacing: 0; + border-collapse: collapse; + & > thead { + } + & > tbody { + text-align: left; + [dir="rtl"] & { + text-align: right; + } + & > tr { + border-bottom: 1px solid var(--color-gray-light); + &:last-child { + border-bottom: 0; + } + &:hover, + &:focus { + background-color: transparentize(#8B9AA8, .85); + } + th, + td { + padding: calc(var(--sm-padding-y) * 2) calc(var(--sm-padding-x) / 2); + white-space: nowrap; + } + th { + font-weight: bold; + color: var(--color-gray-dark); + } + td { + } + } + } +`; + const ExternalVideoModal = styled(Modal)` padding: 1rem; min-height: 23rem; @@ -107,4 +187,7 @@ export default { VideoUrl, ExternalVideoNote, StartButton, + Item, + Icon, + Name, }; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/styles.js b/bigbluebutton-html5/imports/ui/components/external-video-player/styles.js index fd056d091b4d..0f3c907a940e 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/styles.js +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/styles.js @@ -1,5 +1,6 @@ import styled from 'styled-components'; import ReactPlayer from 'react-player'; +import Button from '/imports/ui/components/common/button/component'; const VideoPlayerWrapper = styled.div` position: relative; @@ -45,6 +46,20 @@ const VideoPlayer = styled(ReactPlayer)` } `; +const ExternalVideoCloseButton = styled(Button)` + z-index: 1; + position: absolute; + top: 0; + right: 0; + left: auto; + cursor: pointer; + + [dir="rtl"] & { + right: auto; + left :0; + } +`; + const MobileControlsOverlay = styled.span` position: absolute; top:0; @@ -116,4 +131,5 @@ export default { Loaded, Played, ButtonsWrapper, + ExternalVideoCloseButton, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index 32a7301453bf..e4874bc58ded 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -11,6 +11,7 @@ import { import Styled from './styles'; import ZoomTool from './zoom-tool/component'; import SmartMediaShareContainer from './smart-video-share/container'; +import QuickLinksDropdown from './quick-links-dropdown/component'; import TooltipContainer from '/imports/ui/components/common/tooltip/container'; import KEY_CODES from '/imports/utils/keyCodes'; @@ -269,6 +270,12 @@ class PresentationToolbar extends PureComponent { slidePosition, multiUserSize, multiUser, + allowExternalVideo, + screenSharingCheck, + fullscreenElementId, + isFullscreen, + fullscreenRef, + toolbarWidth, } = this.props; const { isMobile } = deviceInfo; @@ -305,7 +312,23 @@ class PresentationToolbar extends PureComponent { /> ) : null} - + { +
+ { + + } +
+ } { currentSlidHasContent: PresentationService.currentSlidHasContent(), parseCurrentSlideContent: PresentationService.parseCurrentSlideContent, startPoll, + allowExternalVideo: Meteor.settings.public.externalVideoPlayer.enabled, }; })(PresentationToolbarContainer); @@ -83,6 +84,7 @@ PresentationToolbarContainer.propTypes = { nextSlide: PropTypes.func.isRequired, previousSlide: PropTypes.func.isRequired, skipToSlide: PropTypes.func.isRequired, + allowExternalVideo: PropTypes.bool.isRequired, layoutSwapped: PropTypes.bool, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/quick-links-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/quick-links-dropdown/component.jsx new file mode 100644 index 000000000000..b9de9f0d012c --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/quick-links-dropdown/component.jsx @@ -0,0 +1,190 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages } from 'react-intl'; +import _ from 'lodash'; +import { makeCall } from '/imports/ui/services/api'; +//import browser from 'browser-detect'; +//import Button from '/imports/ui/components/button/component'; +import Styled from '../styles'; +import Dropdown from '/imports/ui/components/dropdown/component'; +import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; +import DropdownContent from '/imports/ui/components/dropdown/content/component'; +import DropdownList from '/imports/ui/components/dropdown/list/component'; +import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; +import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component'; +import DropdownListTitle from '/imports/ui/components/dropdown/list/title/component'; +import FullscreenService from '/imports/ui/components/common/fullscreen-button/service'; +import Panopto from '/imports/ui/components/external-video-player/custom-players/panopto'; +import Auth from '/imports/ui/services/auth'; + +const intlMessages = defineMessages({ + quickLinksLabel: { + id: 'app.externalLinks.title', + description: 'Quick external links title', + }, + quickLinksVideoLabel: { + id: 'app.externalLinks.videotitle', + description: 'Quick external links title for videos', + }, + quickLinksUrlLabel: { + id: 'app.externalLinks.urltitle', + description: 'Quick external links title for URLs', + }, + trueOptionLabel: { + id: 'app.poll.t', + description: 'Poll true option value', + }, + falseOptionLabel: { + id: 'app.poll.f', + description: 'Poll false option value', + }, + yesOptionLabel: { + id: 'app.poll.y', + description: 'Poll yes option value', + }, + noOptionLabel: { + id: 'app.poll.n', + description: 'Poll no option value', + }, + abstentionOptionLabel: { + id: 'app.poll.abstention', + description: 'Poll Abstention option value', + }, +}); + +//const BROWSER_RESULTS = browser(); +//const isMobileBrowser = (BROWSER_RESULTS ? BROWSER_RESULTS.mobile : false) +// || (BROWSER_RESULTS && BROWSER_RESULTS.os +// ? BROWSER_RESULTS.os.includes('Android') // mobile flag doesn't always work +// : false); + +const propTypes = { + parseCurrentSlideContent: PropTypes.func.isRequired, + amIPresenter: PropTypes.bool.isRequired, + allowExternalVideo: PropTypes.bool.isRequired, + fullscreenRef: PropTypes.instanceOf(Element), + isFullscreen: PropTypes.bool.isRequired, +}; + +const sendGroupMessage = (message) => { + const CHAT_CONFIG = Meteor.settings.public.chat; + const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid; + const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; + const payload = { + color: '0', + correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`, + sender: { + id: Auth.userID, + name: '', + }, + message, + }; + + return makeCall('sendGroupChatMsg', PUBLIC_GROUP_CHAT_ID, payload); +}; + +const handleClickQuickVideo = (videoUrl, isFullscreen, fullscreenRef) => { + if (isFullscreen) { + FullscreenService.toggleFullScreen(fullscreenRef); + } + sendGroupMessage(videoUrl); + + let externalVideoUrl = videoUrl; + if (Panopto.canPlay(videoUrl)) { + externalVideoUrl = Panopto.getSocialUrl(videoUrl); + } + makeCall('startWatchingExternalVideo', externalVideoUrl); +}; + +const handleClickQuickUrl = (url, isFullscreen, fullscreenRef) => { + if (isFullscreen) { + // may not be necessary; presentation automatically becomes small when the slide is moved on (but depending on browser..) + FullscreenService.toggleFullScreen(fullscreenRef); + } + sendGroupMessage(url); + window.open(url, null, 'menubar,toolbar,location,resizable'); +}; + +function getAvailableLinks(slideId, videoUrls, urls, videoLabel, urlLabel, isFullscreen, fullscreenRef, allowEV){ + const linkItems = []; + if (allowEV && videoUrls && videoUrls.length ) { + linkItems.push({videoLabel}); + videoUrls.forEach(url => { + linkItems.push( + handleClickQuickVideo(url, isFullscreen, fullscreenRef)} + key={url} + />); + }); + } + + if (urls && urls.length ) { + if (videoUrls && videoUrls.length) { + linkItems.push(); + } + linkItems.push({urlLabel}); + urls.forEach(url => { + linkItems.push( + handleClickQuickUrl(url, isFullscreen, fullscreenRef)} + key={url} + />); + }); + } + + if (linkItems.length == 0) { + linkItems.push( + ); + } + return(linkItems); +} + +const QuickLinksDropdown = (props) => { + const { amIPresenter, intl, parseCurrentSlideContent, allowExternalVideo, screenSharingCheck, isFullscreen, fullscreenRef } = props; + //This is called twice (in actions-bar/quick-poll-dropdown/component.jsx as well), + // we could move this to upper component and pass via props in the future. + const parsedSlide = parseCurrentSlideContent( + intl.formatMessage(intlMessages.yesOptionLabel), + intl.formatMessage(intlMessages.noOptionLabel), + intl.formatMessage(intlMessages.abstentionOptionLabel), + intl.formatMessage(intlMessages.trueOptionLabel), + intl.formatMessage(intlMessages.falseOptionLabel), + ); + + const { slideId, videoUrls, urls } = parsedSlide; + +// This seems useless. +// const shouldAllowScreensharing = screenSharingCheck +// && !isMobileBrowser +// && amIPresenter; + + return amIPresenter ? ( + + + null} + size="md" + aria-disabled={ (!videoUrls || (videoUrls && videoUrls.length == 0)) && (!urls || (urls && urls.length == 0)) ? true : false } + style={{ pointerEvents: (!videoUrls || (videoUrls && videoUrls.length == 0)) && (!urls || (urls && urls.length == 0)) ? 'none' : 'unset' }} + /> + + + + {getAvailableLinks(slideId, videoUrls, urls, intl.formatMessage(intlMessages.quickLinksVideoLabel), intl.formatMessage(intlMessages.quickLinksUrlLabel), isFullscreen, fullscreenRef, allowExternalVideo)} + + + + ) : null; +}; + +QuickLinksDropdown.propTypes = propTypes; + +export default QuickLinksDropdown; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js index 9e4719052d70..60626ef5ae43 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js @@ -29,7 +29,7 @@ const PresentationToolbarWrapper = styled.div` width: 100%; bottom: 0px; display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr 1fr; padding: 2px; select { @@ -169,6 +169,36 @@ const PresentationZoomControls = styled.div` } `; +const QuickLinksButton = styled(Button)` + border: none !important; + + & > i { + font-size: 1.2rem; + + [dir="rtl"] & { + -webkit-transform: scale(-1, 1); + -moz-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + -o-transform: scale(-1, 1); + transform: scale(-1, 1); + } + } + margin-left: ${whiteboardToolbarMargin}; + margin-right: ${whiteboardToolbarMargin}; + + position: relative; + color: ${toolbarButtonColor}; + background-color: ${colorOffWhite}; + border-radius: 0; + box-shadow: none !important; + border: 0; + + &:focus { + background-color: ${colorOffWhite}; + border: 0; + } +`; + const FitToWidthButton = styled(Button)` border: none !important; @@ -281,6 +311,7 @@ export default { NextSlideButton, SkipSlideSelect, PresentationZoomControls, + QuickLinksButton, FitToWidthButton, MultiUserTool, WBAccessButton, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/service.js b/bigbluebutton-html5/imports/ui/components/presentation/service.js index d693fab8daec..dada2c462f80 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/service.js @@ -1,8 +1,10 @@ import Presentations from '/imports/api/presentations'; import { Slides, SlidePositions } from '/imports/api/slides'; +import ReactPlayer from 'react-player'; import PollService from '/imports/ui/components/poll/service'; import { safeMatch } from '/imports/utils/string-utils'; +const isUrlValid = url => ReactPlayer.canPlay(url); const POLL_SETTINGS = Meteor.settings.public.poll; const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom; @@ -91,6 +93,18 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, content, } = currentSlide; + const urlRegex = /((http|https):\/\/[a-zA-Z0-9\-.:]+(\/\S*)?)/g; + const optionsUrls = content.match(urlRegex) || []; + const videoUrls = optionsUrls.filter(value => isUrlValid(value)); + const urls = optionsUrls.filter(i => videoUrls.indexOf(i) == -1); + content = content.replace(new RegExp(urlRegex), ''); + + const pollRegex = /\b(\d{1,2}|[A-Za-z])[.)].*/g; //from (#16622) + #16650 + let optionsPoll = content.match(pollRegex) || []; + let optionsPollStrings = []; + if (optionsPoll) optionsPollStrings = optionsPoll.map(opt => `${opt.replace(/^[^.)]{1,2}[.)]/,'').replace(/^\s+/, '')}`); + if (optionsPoll) optionsPoll = optionsPoll.map(opt => `\r${opt.replace(/[.)].*/,'')}.`); + const questionRegex = /^[\s\S]+\?\s*$/gm; const question = safeMatch(questionRegex, content, ''); @@ -110,23 +124,6 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, const trueFalsePatt = createPattern([trueValue, falseValue]); const hasTF = safeMatch(trueFalsePatt, content, false); - const pollRegex = /\b[1-9A-Ia-i][.)] .*/g; - let optionsPoll = safeMatch(pollRegex, content, []); - const optionsWithLabels = []; - - if (hasYN) { - optionsPoll = ['yes', 'no']; - } - - if (optionsPoll) { - optionsPoll = optionsPoll.map((opt) => { - const MAX_CHAR_LIMIT = 30; - const formattedOpt = opt.substring(0, MAX_CHAR_LIMIT); - optionsWithLabels.push(formattedOpt); - return `\r${opt[0]}.`; - }); - } - optionsPoll.reduce((acc, currentValue) => { const lastElement = acc[acc.length - 1]; @@ -147,12 +144,22 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, const isCurrentValueInteger = !!parseInt(currentValue.charAt(1), 10); if (isLastOptionInteger === isCurrentValueInteger) { - if (currentValue.toLowerCase().charCodeAt(1) > lastOption.toLowerCase().charCodeAt(1)) { - options.push(currentValue); + if (isCurrentValueInteger){ + if (parseInt(currentValue.replace(/[\r.]g/,'')) == parseInt(lastOption.replace(/[\r.]g/,'')) + 1) { + options.push(currentValue); + } else { + acc.push({ + options: [currentValue], + }); + } } else { - acc.push({ - options: [currentValue], - }); + if (currentValue.toLowerCase().charCodeAt(1) == lastOption.toLowerCase().charCodeAt(1) + 1) { + options.push(currentValue); + } else { + acc.push({ + options: [currentValue], + }); + } } } else { acc.push({ @@ -160,25 +167,21 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, }); } return acc; - }, []).filter(({ + }, []).map(poll => { + for (let i = 0 ; i < poll.options.length ; i++) { + poll.options.shift(); + poll.options.push(optionsPollStrings.shift()); + } + return poll; + }).filter(({ options, - }) => options.length > 1 && options.length < 10).forEach((p) => { + }) => options.length > 1 && options.length < MAX_CUSTOM_FIELDS).forEach((p) => { const poll = p; if (doubleQuestion) poll.multiResp = true; - if (poll.options.length <= 5 || MAX_CUSTOM_FIELDS <= 5) { - const maxAnswer = poll.options.length > MAX_CUSTOM_FIELDS - ? MAX_CUSTOM_FIELDS - : poll.options.length; - quickPollOptions.push({ - type: `${pollTypes.Letter}${maxAnswer}`, - poll, - }); - } else { quickPollOptions.push({ type: pollTypes.Custom, poll, }); - } }); if (question.length > 0 && optionsPoll.length === 0 && !doubleQuestion && !hasYN && !hasTF) { @@ -218,8 +221,9 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, return { slideId: currentSlide.id, quickPollOptions, - optionsWithLabels, pollQuestion, + videoUrls, + urls, }; }; diff --git a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx index 60d20e1d7010..35dc08bb3c96 100755 --- a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx @@ -22,7 +22,7 @@ const SUBSCRIPTIONS = [ 'presentation-pods', 'users-settings', 'guestUser', 'users-infos', 'meeting-time-remaining', 'local-settings', 'users-typing', 'record-meetings', 'video-streams', 'connection-status', 'voice-call-states', 'external-video-meetings', 'breakouts', 'breakouts-history', - 'pads', 'pads-sessions', 'pads-updates', 'notifications', 'audio-captions', + 'pads', 'pads-sessions', 'pads-updates', 'uploaded-file', 'notifications', 'audio-captions', 'layout-meetings', ]; const { diff --git a/bigbluebutton-html5/imports/ui/components/upload/media/component.jsx b/bigbluebutton-html5/imports/ui/components/upload/media/component.jsx new file mode 100644 index 000000000000..9db989c950ed --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/upload/media/component.jsx @@ -0,0 +1,205 @@ +import React, { Component } from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import cx from 'classnames'; +import _ from 'lodash'; +//import Dropzone from 'react-dropzone'; +import Icon from '/imports/ui/components/common/icon/component'; +import { withModalMounter } from '/imports/ui/components/common/modal/service'; +//import Modal from '/imports/ui/components/common/modal/simple/component'; +import Button from '/imports/ui/components/common/button/component'; +import Service from './service'; +import UploadService from '../service'; +import Styled from './styles'; + +const intlMessages = defineMessages({ + title: { + id: 'app.upload.media.title', + description: 'Media upload modal title', + }, + note: { + id: 'app.upload.media.note', + description: 'Media upload modal note', + }, + message: { + id: 'app.upload.media.message', + description: 'Media upload modal message', + }, + filename: { + id: 'app.upload.media.filename', + description: 'Media upload modal media filename', + }, + options: { + id: 'app.upload.media.options', + description: 'Media upload modal media options', + }, + remove: { + id: 'app.upload.media.remove', + description: 'Media upload modal remove media', + }, + upload: { + id: 'app.upload.media.upload', + description: 'Media upload modal upload button', + }, + cancel: { + id: 'app.upload.media.cancel', + description: 'Media upload modal cancel button', + }, +}); + +class MediaUpload extends Component { + constructor(props) { + super(props); + + this.state = { files: [] }; + + this.source = Service.getSource(); + this.maxSize = Service.getMaxSize(); + this.validFiles = Service.getMediaValidFiles(); + + this.handleOnDrop = this.handleOnDrop.bind(this); + } + + handleOnDrop(acceptedFiles, rejectedFiles) { + const filesToUpload = acceptedFiles.map(file => { + const id = _.uniqueId(file.name); + + return { + file, + id, + filename: file.name, + } + }); + + this.setState(({ files }) => ({ files: files.concat(filesToUpload) })); + } + + handleRemove(item) { + const { files } = this.state; + const index = files.indexOf(item); + + files.splice(index, 1); + + this.setState({ files }); + } + + handleUpload(files) { + const { + closeModal, + intl, + } = this.props; + + UploadService.upload(this.source, files, intl); + closeModal(); + } + + renderItem(item) { + const { intl } = this.props; + + return ( + + + + + + {item.filename} + + + +