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 (
+
+
+
+
+ |
+ {intl.formatMessage(intlMessages.filename)}
+ |
+
+
+
+ {files.map(item => this.renderItem(item))}
+
+
+
+ );
+ }
+
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}
+
+
+
+
+
+
+ );
+ }
+
+ renderFiles() {
+ const { intl } = this.props;
+ const { files } = this.state;
+
+ if (files.length === 0) return null;
+
+ return (
+
+
+
+
+
+ {intl.formatMessage(intlMessages.filename)}
+
+
+ {intl.formatMessage(intlMessages.options)}
+
+
+
+
+ {files.map(item => this.renderItem(item))}
+
+
+
+ );
+ }
+
+ render() {
+ const {
+ intl,
+ closeModal,
+ } = this.props;
+
+ const { files } = this.state;
+
+ return (
+
+
+
+
+ {intl.formatMessage(intlMessages.title)}
+
+
+
+ {intl.formatMessage(intlMessages.note)}
+ {this.renderFiles()}
+
+ type.extension)}
+ maxSize={this.maxSize}
+ disablepreview="true"
+ onDrop={this.handleOnDrop}
+ >
+
+
+ {intl.formatMessage(intlMessages.message)}
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default injectIntl(withModalMounter(MediaUpload));
diff --git a/bigbluebutton-html5/imports/ui/components/upload/media/container.jsx b/bigbluebutton-html5/imports/ui/components/upload/media/container.jsx
new file mode 100644
index 000000000000..95d063a6ed26
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/upload/media/container.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import { withModalMounter } from '/imports/ui/components/common/modal/service';
+import Service from './service';
+import MediaUpload from './component';
+
+const MediaUploadContainer = props => ;
+
+export default withModalMounter(withTracker(({ mountModal }) => ({
+ closeModal: () => mountModal(null),
+ isEnabled: Service.isEnabled(),
+}))(MediaUploadContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/upload/media/service.js b/bigbluebutton-html5/imports/ui/components/upload/media/service.js
new file mode 100644
index 000000000000..d7c261d40410
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/upload/media/service.js
@@ -0,0 +1,44 @@
+import { UploadedFile } from '/imports/api/upload';
+import Auth from '/imports/ui/services/auth';
+import UploadService from '../service';
+
+const MEDIA_UPLOAD = Meteor.settings.public.upload.media;
+const DOWNLOAD = Meteor.settings.public.download;
+
+const isEnabled = () => {
+ return MEDIA_UPLOAD.enabled;
+};
+
+const getSource = () => {
+ return MEDIA_UPLOAD.source;
+};
+
+const getMaxSize = () => {
+ return MEDIA_UPLOAD.maxSize;
+};
+
+const getMediaValidFiles = () => {
+ return MEDIA_UPLOAD.validFiles;
+};
+
+const getMediaFiles = () => {
+ return UploadedFile.find({
+ meetingId: Auth.meetingID,
+ source: MEDIA_UPLOAD.source,
+ }).fetch()
+};
+
+const getDownloadURL = uploadId => {
+ const { source } = MEDIA_UPLOAD;
+
+ return UploadService.buildDownloadURL(source, uploadId);
+};
+
+export default {
+ isEnabled,
+ getSource,
+ getMaxSize,
+ getMediaValidFiles,
+ getMediaFiles,
+ getDownloadURL,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/upload/media/styles.js b/bigbluebutton-html5/imports/ui/components/upload/media/styles.js
new file mode 100644
index 000000000000..530acf3979ec
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/upload/media/styles.js
@@ -0,0 +1,390 @@
+import styled from 'styled-components';
+import Modal from '/imports/ui/components/common/modal/simple/component';
+import Button from '/imports/ui/components/common/button/component';
+import Icon from '/imports/ui/components/common/icon/component';
+import Dropzone from 'react-dropzone';
+import {
+ fileLineWidth,
+ iconPaddingMd,
+ borderSizeLarge,
+ lgPaddingX,
+ statusIconSize,
+ toastMdMargin,
+ uploadListHeight,
+ smPaddingX,
+ smPaddingY,
+ borderSize,
+ borderRadius,
+ lgPaddingY,
+ mdPaddingY,
+ modalInnerWidth,
+ statusInfoHeight,
+ itemActionsWidth,
+ uploadIconSize,
+ iconLineHeight,
+} from '/imports/ui/stylesheets/styled-components/general';
+import {
+ headingsFontWeight,
+ fontSizeLarge,
+ modalTitleFw,
+} from '/imports/ui/stylesheets/styled-components/typography';
+import {
+ colorGrayLight,
+ colorGrayDark,
+ colorPrimary,
+ colorWhite,
+ colorDanger,
+ colorGray,
+ colorGrayLighter,
+ colorLink,
+ colorSuccess,
+ colorGrayLightest,
+ colorText,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
+import { ScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scrollable';
+
+const Header = styled.header`
+ margin: 0;
+ padding: 0;
+ border: none;
+ line-height: 2rem;
+`;
+
+const Content = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ padding: .5rem 0 2rem 0;
+ overflow: hidden;
+`;
+
+/*const Overlay = styled(Modal)`
+ @extend .overlay;
+`;*/
+
+const ModalStyle = styled(Modal)`
+ padding: 1.5rem;
+ min-height: 20rem;
+`;
+
+const UploaderModal = styled.div`
+ background-color: white;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1300;
+`;
+
+const ModalInner = styled.div`
+ margin-left: auto;
+ margin-right: auto;
+ width: ${modalInnerWidth};
+ max-height: 100%;
+ max-width: 100%;
+ padding-bottom: .75rem;
+ overflow-y: auto;
+
+ @media ${smallOnly} {
+ padding-left: ${statusInfoHeight};
+ padding-right: ${statusInfoHeight};
+ }
+`;
+
+const ModalHeader = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ border-bottom:${borderSize} solid ${colorGrayLighter};
+ margin-bottom: 2rem;
+ h1 {
+ font-weight: ${modalTitleFw};
+ }
+ div {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+`;
+
+const Title = styled.h3`
+ text-align: center;
+ font-weight: 400;
+ font-size: 1.3rem;
+ color: var(--color-background);
+ white-space: normal;
+
+ @include mq($small-only) {
+ font-size: 1rem;
+ padding: 0 1rem;
+ }
+`;
+
+const DropzoneWrapper = styled.div`
+ width: 100%;
+ display: flex;
+ margin-top: calc(var(--lg-padding-y) * 5);
+`;
+
+const UploaderDropzone = styled(Dropzone)`
+ flex: auto;
+ border: ${borderSize} dashed ${colorGray};
+ color: ${colorGray};
+ border-radius: ${borderRadius};
+ padding: calc(${lgPaddingY} * 2.5) ${lgPaddingX};
+ text-align: center;
+ font-size: ${fontSizeLarge};
+ cursor: pointer;
+ & .dropzoneActive {
+ background-color: ${colorGrayLighter};
+ }
+`;
+
+const DropzoneIcon = styled(Icon)`
+ font-size: calc(${fontSizeLarge} * 3);
+`;
+
+const DropzoneMessage = styled.p`
+ margin: ${mdPaddingY} 0;
+`;
+
+const DropzoneLink = styled.span`
+ color: ${colorLink};
+ text-decoration: underline;
+ font-size: 80%;
+ display: block;
+`;
+
+/*.dropzoneActive {
+ background-color: var(--color-gray-lighter);
+}*/
+
+/*.dropzoneIcon {
+ font-size: calc(var(--font-size-large) * 3);
+}*/
+
+/*
+const DropzoneMessage = styled.p`
+ margin: var(--md-padding-y) 0;
+`;
+*/
+
+const Hidden = styled.th`
+ 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();*/
+ overflow-y: auto;
+ max-height: 35vh;
+ width: 100%;
+ padding: .5rem 0;
+`;
+
+const Table = styled.table`
+ 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 Actions = styled.td`
+ width: 1%;
+ text-align: left;
+
+ [dir="rtl"] & {
+ text-align: right;
+ }
+`;
+
+/*.icon > i {
+ font-size: 1.35rem;
+}*/
+
+const Name = styled.th`
+ height: 1rem;
+ width: auto;
+ position: relative;
+
+ &:before {
+ content: "\00a0";
+ visibility: hidden;
+ }
+
+ > span {
+ /*@extend %text-elipsis;*/
+ /*text-overflow: ellipsis;*/
+ position: absolute;
+ left: 0;
+ right: 0;
+
+ [dir="rtl"] & {
+ right: 1rem;
+ }
+ }
+`;
+
+const Action = styled(Button)`
+ div > i {
+ margin-top: .25rem;
+ }
+ display: inline-block;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ font-size: 1.35rem;
+ color: var(--color-gray-light);
+ padding: 0;
+
+ :global(.animationsEnabled) & {
+ transition: all .25s;
+ }
+
+ :hover, :focus {
+ padding: unset !important;
+ }
+
+ background-color: transparent;
+ border: 0 !important;
+
+ & > i:focus,
+ & > i:hover {
+ color: var(--color-danger) !important;
+ background-color: transparent;
+ }
+
+ &[aria-disabled="true"] {
+ cursor: not-allowed;
+ opacity: .5;
+ box-shadow: none;
+ pointer-events: none;
+ }
+`;
+
+/*.action,
+.action > i {
+ display: inline-block;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ font-size: 1.35rem;
+ color: var(--color-gray-light);
+ padding: 0;
+
+ :global(.animationsEnabled) & {
+ transition: all .25s;
+ }
+
+ :hover, :focus {
+ padding: unset !important;
+ }
+}
+
+.remove {
+ background-color: transparent;
+ border: 0 !important;
+
+ & > i:focus,
+ & > i:hover {
+ color: var(--color-danger) !important;
+ background-color: transparent;
+ }
+
+ &[aria-disabled="true"] {
+ cursor: not-allowed;
+ opacity: .5;
+ box-shadow: none;
+ pointer-events: none;
+ }
+}*/
+
+const StyledButtons = styled.div`
+ margin-left: auto;
+ margin-right: 3px;
+
+
+ [dir="rtl"] & {
+ margin-right: auto;
+ margin-left: 3px;
+ }
+
+ :first-child {
+ margin-right: 3px;
+ /*margin-left: inherit;*/
+
+ [dir="rtl"] & {
+ margin-right: inherit;
+ margin-left: 3px;
+ }
+ }
+`;
+
+const Footer = styled.div`
+ display: flex;
+`;
+
+export default {
+ Header,
+ Content,
+ UploaderModal,
+ ModalStyle,
+ ModalInner,
+ ModalHeader,
+ Title,
+ DropzoneWrapper,
+ UploaderDropzone,
+ DropzoneIcon,
+ DropzoneMessage,
+ DropzoneLink,
+ Hidden,
+ List,
+ Table,
+ Actions,
+ Name,
+ Action,
+ StyledButtons,
+ Footer,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/upload/service.js b/bigbluebutton-html5/imports/ui/components/upload/service.js
new file mode 100644
index 000000000000..0c5e543ac369
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/upload/service.js
@@ -0,0 +1,138 @@
+import { defineMessages, injectIntl } from 'react-intl';
+import axios from 'axios';
+import { UploadRequest } from '/imports/api/upload';
+import Auth from '/imports/ui/services/auth';
+import { notify } from '/imports/ui/services/notification';
+import { makeCall } from '/imports/ui/services/api';
+
+const UPLOAD = Meteor.settings.public.upload;
+const DOWNLOAD = Meteor.settings.public.download;
+
+const intlMessages = defineMessages({
+ uploading: {
+ id: 'app.upload.toast.uploading',
+ description: 'Upload toast file uploading',
+ },
+ completed: {
+ id: 'app.upload.toast.completed',
+ description: 'Upload toast file completed',
+ },
+ 401: {
+ id: 'app.upload.toast.error.401',
+ description: 'Upload toast error unauthorized',
+ },
+ 408: {
+ id: 'app.upload.toast.error.408',
+ description: 'Upload toast error request timeout',
+ },
+ header: {
+ id: 'app.upload.notification.header',
+ description: 'Uploaded file notification header',
+ },
+ disclaimer: {
+ id: 'app.upload.notification.disclaimer',
+ description: 'Uploaded file notification disclaimer',
+ },
+});
+
+const requestUpload = (source, filename) => {
+ return new Promise((resolve, reject) => {
+ const timestamp = new Date().getTime();
+ makeCall('requestUpload', source, filename, timestamp);
+
+ let comp;
+ const timeout = setTimeout(() => {
+ if (comp) comp.stop();
+ reject(408);
+ }, UPLOAD.timeout);
+
+ Tracker.autorun(computation => {
+ comp = computation;
+
+ const subscription = Meteor.subscribe('upload-request', Auth.credentials, source, filename);
+ if (!subscription.ready()) return;
+
+ const {
+ meetingId,
+ requesterUserId: userId,
+ } = Auth.credentials;
+
+ const request = UploadRequest.findOne({
+ source,
+ meetingId,
+ userId,
+ filename,
+ timestamp,
+ });
+
+ if (!request) return;
+ clearTimeout(timeout);
+ computation.stop();
+
+ if (!request.success) {
+ reject(401);
+ } else {
+ resolve(request.token);
+ }
+ });
+ });
+};
+
+const upload = (source, files, intl) => {
+ files.forEach(file => {
+ requestUpload(source, file.filename).then(token => {
+ notify(intl.formatMessage(intlMessages.uploading, ({ 0: file.filename })), 'info', 'upload');
+ post(source, file, token, intl);
+ }).catch(code => {
+ notify(intl.formatMessage(intlMessages[code], ({ 0: file.filename })), 'error', 'upload');
+ });
+ });
+};
+
+const post = (source, file, token, intl) => {
+ const {
+ meetingId,
+ requesterUserId: userId,
+ } = Auth.credentials;
+
+ const url = `${UPLOAD.endpoint}/${source}/${token}`;
+
+ const data = new FormData();
+ data.append('file', file.file);
+
+ const config = {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ 'X-Meeting-ID': meetingId,
+ 'X-User-ID': userId,
+ 'X-Filename': encodeURI(file.filename),
+ },
+ };
+
+ axios.post(url, data, config).then(resp => {
+ notify(intl.formatMessage(intlMessages.completed, ({ 0: file.filename })), 'info', 'upload');
+ }).catch(error => {
+ console.log("Upload error", error.message);
+ notify('Upload error' + error.message, 'error', 'upload');
+ });
+};
+
+const buildDownloadURL = (source, uploadId) => {
+ return Auth.authenticateURL(`${DOWNLOAD.endpoint}/${source}/${uploadId}`);
+};
+
+const getNotification = ({ source, uploadId, filename }, intl) => {
+ const downloadURL = buildDownloadURL(source, uploadId);
+
+ const header = `${intl.formatMessage(intlMessages.header)}:`;
+ const link = `${filename}`;
+ const disclaimer = `${intl.formatMessage(intlMessages.disclaimer)}`;
+
+ return `${header}
${link}
${disclaimer}`;
+};
+
+export default {
+ upload,
+ buildDownloadURL,
+ getNotification,
+};
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 9686ba3b7192..d047f57a1476 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -498,7 +498,7 @@ public:
poll:
enabled: true
allowCustomResponseInput: true
- maxCustom: 5
+ maxCustom: 99
maxTypedAnswerLength: 45
chatMessage: true
captions:
@@ -698,6 +698,25 @@ public:
- danger
- critical
help: STATS_HELP_URL
+ upload:
+ endpoint: /bigbluebutton/upload
+ timeout: 5000
+ notification: false
+ media:
+ enabled: true
+ maxSize: 900000000
+ source: media
+ validFiles:
+ - extension: .mp3
+ type: audio/mp3
+ - extension: .ogg
+ type: audio/ogg
+ - extension: .mp4
+ type: video/mp4
+ - extension: .webm
+ type: video/webm
+ download:
+ endpoint: /bigbluebutton/download
presentation:
allowDownloadable: true
panZoomThrottle: 32
diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json
index cb8e447f392d..747ac7db5fbc 100755
--- a/bigbluebutton-html5/public/locales/en.json
+++ b/bigbluebutton-html5/public/locales/en.json
@@ -80,6 +80,20 @@
"app.confirmation.virtualBackground.title": "Start new virtual background",
"app.confirmation.virtualBackground.description": "{0} will be added as virtual background. Continue?",
"app.confirmationModal.yesLabel": "Yes",
+ "app.upload.toast.uploading": "Uploading {0}",
+ "app.upload.toast.completed": "Completed {0}",
+ "app.upload.toast.error.401": "Unauthorized {0}",
+ "app.upload.toast.error.408": "Request timeout {0}",
+ "app.upload.notification.header": "A file was uploaded",
+ "app.upload.notification.disclaimer": "Note that your browser might not support this type of file.",
+ "app.upload.media.title": "Media upload",
+ "app.upload.media.note": "Uploaded media can be shared from the external video sharing modal",
+ "app.upload.media.message": "Drag or select media files to be uploaded",
+ "app.upload.media.filename": "Media filename",
+ "app.upload.media.options": "Media options",
+ "app.upload.media.remove": "Remove",
+ "app.upload.media.upload": "Upload",
+ "app.upload.media.cancel": "Cancel",
"app.textInput.sendLabel": "Send",
"app.title.defaultViewLabel": "Default presentation view",
"app.notes.title": "Shared Notes",
@@ -574,6 +588,8 @@
"app.actionsBar.actionsDropdown.desktopShareLabel": "Share your screen",
"app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Stop sharing your screen",
"app.actionsBar.actionsDropdown.presentationDesc": "Upload your presentation",
+ "app.actionsBar.actionsDropdown.uploadMediaLabel": "Upload a movie/audio file temporarily",
+ "app.actionsBar.actionsDropdown.uploadMediaDesc": "Upload your local media file",
"app.actionsBar.actionsDropdown.initPollDesc": "Initiate a poll",
"app.actionsBar.actionsDropdown.desktopShareDesc": "Share your screen with others",
"app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with",
@@ -1140,6 +1156,9 @@
"app.createBreakoutRoom.modalDesc": "Tip: You can drag-and-drop a user's name to assign them to a specific breakout room.",
"app.createBreakoutRoom.roomTime": "{0} minutes",
"app.createBreakoutRoom.numberOfRoomsError": "The number of rooms is invalid.",
+ "app.externalLinks.title": "External links within slide",
+ "app.externalLinks.videotitle": "Video",
+ "app.externalLinks.urltitle": "Website",
"app.createBreakoutRoom.duplicatedRoomNameError": "Room name can't be duplicated.",
"app.createBreakoutRoom.emptyRoomNameError": "Room name can't be empty.",
"app.createBreakoutRoom.setTimeInMinutes": "Set duration to (minutes)",
@@ -1161,6 +1180,7 @@
"app.externalVideo.urlInput": "Add Video URL",
"app.externalVideo.urlError": "This video URL isn't supported",
"app.externalVideo.close": "Close",
+ "app.externalVideo.filename": "Media filename",
"app.externalVideo.autoPlayWarning": "Play the video to enable media synchronization",
"app.externalVideo.refreshLabel": "Refresh Video Player",
"app.externalVideo.fullscreenLabel": "Video Player",
diff --git a/bigbluebutton-html5/public/locales/ja.json b/bigbluebutton-html5/public/locales/ja.json
index 6fadc0153d1b..439d001d332f 100644
--- a/bigbluebutton-html5/public/locales/ja.json
+++ b/bigbluebutton-html5/public/locales/ja.json
@@ -80,6 +80,20 @@
"app.confirmation.virtualBackground.title": "新しいバーチャル背景を設定",
"app.confirmation.virtualBackground.description": "{0}個がバーチャル背景として追加されます。続けますか?",
"app.confirmationModal.yesLabel": "はい",
+ "app.upload.toast.uploading": "アップロード中 {0}",
+ "app.upload.toast.completed": "完了 {0}",
+ "app.upload.toast.error.401": "許可がありません {0}",
+ "app.upload.toast.error.408": "要求がタイムアウトしました {0}",
+ "app.upload.notification.header": "ファイルがアップロードされました",
+ "app.upload.notification.disclaimer": "ブラウザが、このファイルタイプをサポートしていない可能性もあります。注意してください",
+ "app.upload.media.title": "メディアアップロード",
+ "app.upload.media.note": "mp4、mp3、ogg、webm形式の動画、音声ファイルを1000MBまでアップロード可能です。アップロードされたファイルは「インターネット上の動画を共有」から共有できます。",
+ "app.upload.media.message": "アップロードするメディアファイルをドラッグまたは選択してください",
+ "app.upload.media.filename": "メディアファイル名",
+ "app.upload.media.options": "メディアオプション",
+ "app.upload.media.remove": "削除",
+ "app.upload.media.upload": "アップロード",
+ "app.upload.media.cancel": "キャンセル",
"app.textInput.sendLabel": "送る",
"app.title.defaultViewLabel": "既定のプレゼンテーションビュー",
"app.notes.title": "共有ノート",
@@ -574,6 +588,8 @@
"app.actionsBar.actionsDropdown.desktopShareLabel": "画面を共有",
"app.actionsBar.actionsDropdown.stopDesktopShareLabel": "画面共有をやめる",
"app.actionsBar.actionsDropdown.presentationDesc": "プレゼンテーションをアップロード",
+ "app.actionsBar.actionsDropdown.uploadMediaLabel": "動画や音声ファイルを一時的にアップロード",
+ "app.actionsBar.actionsDropdown.uploadMediaDesc": "ローカルのメディアファイルをアップロード",
"app.actionsBar.actionsDropdown.initPollDesc": "投票を初期化",
"app.actionsBar.actionsDropdown.desktopShareDesc": "他の人と画面を共有する",
"app.actionsBar.actionsDropdown.stopDesktopShareDesc": "画面共有をやめる:",
@@ -1140,6 +1156,9 @@
"app.createBreakoutRoom.modalDesc": "やり方:ユーザーの名前をドラッグ&ドロップして、特定の小会議室に割りふってください。",
"app.createBreakoutRoom.roomTime": "{0} 分",
"app.createBreakoutRoom.numberOfRoomsError": "会議室の数が正しく設定されていません。",
+ "app.externalLinks.title": "スライド内の外部リンク",
+ "app.externalLinks.videotitle": "ビデオ",
+ "app.externalLinks.urltitle": "Webサイト",
"app.createBreakoutRoom.duplicatedRoomNameError": "会議室名を重複してつけることはできません。",
"app.createBreakoutRoom.emptyRoomNameError": "会議室名を空白にはできません。",
"app.createBreakoutRoom.setTimeInMinutes": "会議時間を(minutes)分にセットする",
@@ -1161,6 +1180,7 @@
"app.externalVideo.urlInput": "動画URLを追加",
"app.externalVideo.urlError": "この動画URLは再生できませんでした",
"app.externalVideo.close": "閉じる",
+ "app.externalVideo.filename": "メディアファイル名",
"app.externalVideo.autoPlayWarning": "音声同期するには動画を再生してください",
"app.externalVideo.refreshLabel": "ビデオプレイヤーをリフレッシュ",
"app.externalVideo.fullscreenLabel": "ビデオプレイヤー",
diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js
index 770e85d499a6..0201803f7f03 100755
--- a/bigbluebutton-html5/server/main.js
+++ b/bigbluebutton-html5/server/main.js
@@ -24,6 +24,7 @@ import '/imports/api/users-infos/server';
import '/imports/api/users-persistent-data/server';
import '/imports/api/connection-status/server';
import '/imports/api/audio-captions/server';
+import '/imports/api/upload/server';
import '/imports/api/external-videos/server';
import '/imports/api/pads/server';
import '/imports/api/guest-users/server';
diff --git a/bigbluebutton-web/bbb-web.nginx b/bigbluebutton-web/bbb-web.nginx
index 5db9ea4b668f..1269454d3d16 100755
--- a/bigbluebutton-web/bbb-web.nginx
+++ b/bigbluebutton-web/bbb-web.nginx
@@ -148,7 +148,75 @@
proxy_set_header X-textTrack-track $textTrack;
proxy_set_header X-Original-URI $request_uri;
}
+
+ # For the implementation, you need to add these directly to /etc/bigbluebutton/nginx/web(.nginx) or to /usr/share/bigbluebutton/nginx/web.nginx
+ location ~ "^\/bigbluebutton\/upload\/(?[a-z]+)\/(?[a-zA-Z0-9]{32})$" {
+ proxy_pass http://127.0.0.1:8090;
+ proxy_redirect default;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Source $source;
+
+ # Allow 30M uploaded files
+ client_max_body_size 900m;
+ client_body_buffer_size 128k;
+
+ proxy_connect_timeout 90;
+ proxy_send_timeout 90;
+ proxy_read_timeout 90;
+
+ proxy_buffer_size 4k;
+ proxy_buffers 4 32k;
+ proxy_busy_buffers_size 64k;
+ proxy_temp_file_write_size 64k;
+
+ include fastcgi_params;
+
+ proxy_request_buffering off;
+
+ # Send a sub-request bbb-web to refuse before loading
+ auth_request /bigbluebutton/upload/check;
+ }
+ location = /bigbluebutton/upload/check {
+ internal;
+ proxy_pass http://127.0.0.1:8090;
+ proxy_redirect default;
+ proxy_set_header Content-Length "";
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Original-URI $request_uri;
+ proxy_set_header X-Content-Length $http_content_length;
+ proxy_set_header X-Source $source;
+ proxy_set_header X-Token $token;
+
+ # Allow 30M uploaded files
+ client_max_body_size 900m;
+ client_body_buffer_size 128k;
+
+ proxy_pass_request_body off;
+ proxy_request_buffering off;
+ }
+
+ location ~ "^\/bigbluebutton\/download\/(?[a-z]+)\/(?[a-zA-Z0-9-]+)$" {
+ proxy_pass http://127.0.0.1:8090;
+ proxy_redirect default;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Original-URI $request_uri;
+ proxy_set_header X-Source $source;
+ proxy_set_header X-Upload-ID $id;
+
+ # Send a sub-request bbb-web to refuse before loading
+ auth_request /bigbluebutton/download/check;
+ }
+
+ location = /bigbluebutton/download/check {
+ internal;
+ proxy_pass http://127.0.0.1:8090;
+ proxy_pass_request_body off;
+ proxy_set_header Content-Length "";
+ proxy_set_header X-Original-URI $request_uri;
+ proxy_set_header X-Source $source;
+ proxy_set_header X-Upload-ID $id;
+ }
}
location @error403 {
diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
index 3118079c6ad1..b60c3a556dd0 100644
--- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
+++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
@@ -63,6 +63,13 @@ maxNumPages=200
#----------------------------------------------------
# Maximum file size for an uploaded presentation (default 30MB).
maxFileSizeUpload=30000000
+#----------------------------------------------------
+# Maximum file size for an generic upload (default 30MB).
+maxUploadSize=900000000
+
+#----------------------------------------------------
+# Directory where BigBlueButton stores uploaded files.
+uploadDir=/var/bigbluebutton
#----------------------------------------------------
# Maximum allowed number of place object tags in generated svg, if exceeded the conversion will fallback to full BMP (default 800)
diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml
index a6de7921f729..f0d24ff65c33 100755
--- a/bigbluebutton-web/grails-app/conf/spring/resources.xml
+++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml
@@ -179,6 +179,8 @@ with BigBlueButton; if not, see .
+
+
diff --git a/bigbluebutton-web/grails-app/conf/spring/turn-stun-servers.xml b/bigbluebutton-web/grails-app/conf/spring/turn-stun-servers.xml
index 90913545e05c..06d4fc7b416c 100755
--- a/bigbluebutton-web/grails-app/conf/spring/turn-stun-servers.xml
+++ b/bigbluebutton-web/grails-app/conf/spring/turn-stun-servers.xml
@@ -1,5 +1,6 @@