pull/207/merge
David 2022-05-24 22:02:14 +00:00 committed by GitHub
commit cdb78e0786
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 291 additions and 25 deletions

View File

@ -206,7 +206,7 @@ class API {
if ($presentation !== null && $presentation->isValid()) {
/** @psalm-suppress InvalidArgument */
$createMeetingParams->addPresentation($presentation->getUrl(), null, $presentation->getFilename());
$createMeetingParams->addPresentation($presentation->generateUrl(), null, $presentation->getFilename());
}
if ($room->access === Room::ACCESS_WAITING_ROOM || $room->access === Room::ACCESS_WAITING_ROOM_ALL) {

View File

@ -2,14 +2,69 @@
namespace OCA\BigBlueButton\BigBlueButton;
use OCA\DAV\Db\Direct;
use OCA\DAV\Db\DirectMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\IRootFolder;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\IURLGenerator;
use OCP\Security\ISecureRandom;
class Presentation {
private $url;
private $userId;
private $filename;
/** @var File*/
private $file;
public function __construct(string $url, string $filename) {
$this->url = $url;
$this->filename = preg_replace('/[^\x20-\x7E]+/','#', $filename);
/** @var Folder */
private $userFolder;
/** @var DirectMapper */
private $mapper;
/** @var ISecureRandom */
private $random;
/** @var ITimeFactory */
private $timeFactory;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct(
string $path,
string $userId,
IRootFolder $iRootFolder,
DirectMapper $mapper,
ISecureRandom $random,
ITimeFactory $timeFactory,
IURLGenerator $urlGenerator
) {
$this->userFolder = $iRootFolder->getUserFolder($userId);
$this->file = $this->userFolder->get($path);
$this->mapper = $mapper;
$this->random = $random;
$this->timeFactory = $timeFactory;
$this->userId = $userId;
$this->urlGenerator = $urlGenerator;
}
public function generateUrl() {
$direct = new Direct();
$direct->setUserId($this->userId);
$direct->setFileId($this->file->getId());
$token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
$direct->setToken($token);
$direct->setExpiration($this->timeFactory->getTime() + (60 * 60 * 8));
$this->mapper->insert($direct);
$url = $this->urlGenerator->getAbsoluteURL('remote.php/direct/' . $token);
return $url;
}
public function getUrl(): string {
@ -17,10 +72,10 @@ class Presentation {
}
public function getFilename(): string {
return $this->filename;
return $this->file->getName();
}
public function isValid(): bool {
return !empty($this->url) && !empty($this->filename);
return !empty($this->file->getContent());
}
}

View File

@ -10,13 +10,17 @@ use OCA\BigBlueButton\NoPermissionException;
use OCA\BigBlueButton\NotFoundException;
use OCA\BigBlueButton\Permission;
use OCA\BigBlueButton\Service\RoomService;
use OCA\DAV\Db\DirectMapper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
class JoinController extends Controller {
/** @var string */
@ -43,6 +47,18 @@ class JoinController extends Controller {
/** @var IJobList */
private $jobList;
/** @var IRootFolder */
private $iRootFolder;
/** @var DirectMapper */
private $mapper;
/** @var ISecureRandom */
private $random;
/** @var ITimeFactory */
private $timeFactory;
public function __construct(
string $appName,
IRequest $request,
@ -51,7 +67,11 @@ class JoinController extends Controller {
IUserSession $userSession,
API $api,
Permission $permission,
IJobList $jobList
IJobList $jobList,
IRootFolder $iRootFolder,
DirectMapper $mapper,
ISecureRandom $random,
ITimeFactory $timeFactory
) {
parent::__construct($appName, $request);
@ -61,6 +81,10 @@ class JoinController extends Controller {
$this->api = $api;
$this->permission = $permission;
$this->jobList = $jobList;
$this->iRootFolder = $iRootFolder;
$this->mapper = $mapper;
$this->random = $random;
$this->timeFactory = $timeFactory;
}
public function setToken(string $token): void {
@ -107,8 +131,10 @@ class JoinController extends Controller {
throw new NoPermissionException();
}
if ($this->permission->isAdmin($room, $userId)) {
$presentation = new Presentation($u, $filename);
if ($this->permission->isAdmin($room, $userId) && !empty($filename)) {
$presentation = new Presentation($filename, $userId, $this->iRootFolder, $this->mapper, $this->random, $this->timeFactory, $this->urlGenerator);
} elseif (!$room->running && !empty($room->presentationPath)) {
$presentation = new Presentation($room->presentationPath, $room->presentationUserId, $this->iRootFolder, $this->mapper, $this->random, $this->timeFactory, $this->urlGenerator);
}
} elseif ($room->access === Room::ACCESS_INTERNAL || $room->access === Room::ACCESS_INTERNAL_RESTRICTED) {
return new RedirectResponse($this->getLoginUrl());
@ -138,7 +164,7 @@ class JoinController extends Controller {
$this->markAsRunning($room);
\OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url='.$joinUrl]);
\OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url=' . $joinUrl]);
return new TemplateResponse($this->appName, 'forward', [
'room' => $room->name,

View File

@ -14,6 +14,7 @@ use OCP\IRequest;
use OCP\IUserManager;
class RoomController extends Controller {
use Errors;
/** @var RoomService */
private $service;
@ -32,8 +33,6 @@ class RoomController extends Controller {
/** @var string */
private $userId;
use Errors;
public function __construct(
$appName,
IRequest $request,
@ -119,7 +118,9 @@ class RoomController extends Controller {
bool $listenOnly,
bool $mediaCheck,
bool $cleanLayout,
bool $joinMuted
bool $joinMuted,
string $presentationUserId,
string $presentationPath
): DataResponse {
$room = $this->service->find($id);
@ -142,8 +143,20 @@ class RoomController extends Controller {
return new DataResponse(['message' => 'Access type not allowed.'], Http::STATUS_BAD_REQUEST);
}
return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken, $listenOnly, $mediaCheck, $cleanLayout, $joinMuted) {
return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken, $listenOnly, $mediaCheck, $cleanLayout, $joinMuted);
if ($presentationUserId != '' && $presentationUserId != $room->getPresentationUserId()) {
return new DataResponse(['message' => 'Not allowed to change to another user.'], Http::STATUS_BAD_REQUEST);
}
if ($presentationUserId === '') {
$presentationUserId = $this->userId;
}
if ($presentationUserId != $this->userId && $presentationPath != $room->getPresentationPath()) {
return new DataResponse(['message' => 'Not allowed to choose path of another user.'], Http::STATUS_BAD_REQUEST);
}
return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken, $listenOnly, $mediaCheck, $cleanLayout, $joinMuted, $presentationUserId, $presentationPath) {
return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken, $listenOnly, $mediaCheck, $cleanLayout, $joinMuted, $presentationUserId, $presentationPath);
});
}

View File

@ -74,6 +74,8 @@ class Room extends Entity implements JsonSerializable {
public $cleanLayout;
public $joinMuted;
public $running;
public $presentationUserId;
public $presentationPath;
public function __construct() {
$this->addType('maxParticipants', 'integer');
@ -108,6 +110,8 @@ class Room extends Entity implements JsonSerializable {
'cleanLayout' => boolval($this->cleanLayout),
'joinMuted' => boolval($this->joinMuted),
'running' => boolval($this->running),
'presentationUserId' => $this->presentationUserId,
'presentationPath' => $this->presentationPath,
];
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Your name <your@email.com>
*
* @author Your name <your@email.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\BigBlueButton\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version000000Date20220413130357 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
$schema = $schemaClosure();
if ($schema->hasTable('bbb_rooms')) {
$table = $schema->getTable('bbb_rooms');
if (!$table->hasColumn('presentation_user_id')) {
$table->addColumn('presentation_user_id', 'string', [
'notnull' => true,
'length' => 200,
]);
}
if (!$table->hasColumn('presentation_path')) {
$table->addColumn('presentation_path', 'string', [
'notnull' => false,
]);
}
return $schema;
}
return null;
}
}

View File

@ -16,7 +16,6 @@ use OCP\IConfig;
use OCP\Security\ISecureRandom;
class RoomService {
/** @var RoomMapper */
private $mapper;
@ -33,7 +32,8 @@ class RoomService {
RoomMapper $mapper,
IConfig $config,
IEventDispatcher $eventDispatcher,
ISecureRandom $random) {
ISecureRandom $random
) {
$this->mapper = $mapper;
$this->config = $config;
$this->eventDispatcher = $eventDispatcher;
@ -103,6 +103,8 @@ class RoomService {
$room->setMediaCheck($mediaCheck);
$room->setCleanLayout(false);
$room->setJoinMuted(false);
$room->setPresentationUserId('');
$room->setPresentationPath('');
if ($access === Room::ACCESS_PASSWORD) {
$room->setPassword($this->humanReadableRandom(8));
@ -133,7 +135,10 @@ class RoomService {
bool $listenOnly,
bool $mediaCheck,
bool $cleanLayout,
bool $joinMuted) {
bool $joinMuted,
string $presentationUserId,
string $presentationPath
) {
try {
$room = $this->mapper->find($id);
@ -156,6 +161,8 @@ class RoomService {
$room->setMediaCheck($mediaCheck);
$room->setCleanLayout($cleanLayout);
$room->setJoinMuted($joinMuted);
$room->setPresentationUserId($presentationUserId);
$room->setPresentationPath($presentationPath);
return $this->mapper->update($room);
} catch (Exception $e) {

View File

@ -45,6 +45,8 @@ export interface Room {
cleanLayout: boolean,
joinMuted: boolean,
running: boolean,
presentationUserId: string,
presentationPath: string | string[]
}
export interface RoomShare {
@ -178,7 +180,6 @@ class Api {
public async updateRoom(room: Room) {
const response = await axios.put(this.getUrl(`rooms/${room.id}`), room);
return response.data;
}

View File

@ -67,6 +67,42 @@ pre {
}
}
.bbb-presentation-input {
display: flex;
flex-direction: column;
height: min-content;
width: fit-content;
margin-top: 10px;
align-content: flex-start;
.hidden {
display: none;
}
p {
display: flex;
justify-content: flex-start;
align-items: center;
button {
border: 0;
background-color: transparent;
padding: 0;
margin: 5px;
border-radius: 50%;
line-height: 0;
display: inline-block;
justify-self: flex-end;
}
}
img {
margin: 5px;
height: 32px;
width: auto;
}
}
#bbb-settings {
#bbb-warning,
#bbb-success {
@ -306,4 +342,4 @@ pre {
border: 5px solid #fff;
}
}
}
}

View File

@ -6,9 +6,10 @@ type Props = {
room: Room;
restriction?: Restriction;
updateProperty: (key: string, value: string | boolean | number | null) => Promise<void>;
updateRoom: (Room) => Promise<void>;
}
const EditRoom: React.FC<Props> = ({ room, restriction, updateProperty }) => {
const EditRoom: React.FC<Props> = ({ room, restriction, updateProperty, updateRoom }) => {
const [open, setOpen] = useState<boolean>(false);
return (
@ -18,7 +19,7 @@ const EditRoom: React.FC<Props> = ({ room, restriction, updateProperty }) => {
<span className="icon icon-settings-dark icon-visible"></span>
</button>
<EditRoomDialog room={room} restriction={restriction} updateProperty={updateProperty} open={open} setOpen={setOpen} />
<EditRoomDialog room={room} restriction={restriction} updateProperty={updateProperty} updateRoom={updateRoom} open={open} setOpen={setOpen} />
</>
);
};

View File

@ -6,6 +6,7 @@ import Dialog from './Dialog';
import ShareWith from './ShareWith';
import { SubmitInput } from './SubmitInput';
import { AccessOptions } from '../Common/Translation';
import SharedPresentationInput from './SharedPresentationInput';
const descriptions: { [key: string]: string } = {
name: t('bbb', 'Descriptive name of this room.'),
@ -29,11 +30,12 @@ type Props = {
room: Room;
restriction?: Restriction;
updateProperty: (key: string, value: string | boolean | number | null) => Promise<void>;
updateRoom: (Room) => Promise<void>;
open: boolean;
setOpen: (open: boolean) => void;
}
const EditRoomDialog: React.FC<Props> = ({ room, restriction, updateProperty, open, setOpen }) => {
const EditRoomDialog: React.FC<Props> = ({ room, restriction, updateProperty, open, setOpen, updateRoom }) => {
const [shares, setShares] = useState<RoomShare[]>();
const maxParticipantsLimit = (restriction?.maxParticipants || 0) < 0 ? undefined : restriction?.maxParticipants;
@ -226,6 +228,12 @@ const EditRoomDialog: React.FC<Props> = ({ room, restriction, updateProperty, op
</div>
<p><em>{descriptions.joinMuted}</em></p>
</div>
<div>
<div>
<label htmlFor={`bbb-presentation-${room.id}`}>{t('bbb', 'Default Presentation')}</label>
<SharedPresentationInput id={`bbb-presentation-${room.id}`} room={room} updateRoom={updateRoom}/>
</div>
</div>
</div>
</Dialog>
);

View File

@ -223,7 +223,7 @@ const RoomRow: React.FC<Props> = (props) => {
</td>
<td className="bbb-shrink"><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
<td className="edit icon-col">
<EditRoom room={props.room} restriction={props.restriction} updateProperty={updateRoom} />
<EditRoom room={props.room} restriction={props.restriction} updateRoom={props.updateRoom} updateProperty={updateRoom} />
</td>
<td className="remove icon-col">
<button className="action-item" onClick={deleteRow as any} title={t('bbb', 'Delete')}>

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Room } from '../Common/Api';
type Props = {
id: string
room: Room
updateRoom: (Room) => Promise<void>
}
const SharedPresentationInput: React.FC<Props> = ({ room, updateRoom, id }) => {
function filepicker() {
OC.dialogs.filepicker(t('bbb', 'Default Presentation'), file => {
updateRoom({...room, presentationUserId: '', presentationPath: file});
},
);
}
function removeFile() {
updateRoom({...room, presentationUserId: '', presentationPath: ''});
}
function getAvatarUrl() {
if (room.presentationUserId === null || room.presentationUserId === undefined) {
return ;
}
return (OC.generateUrl('/avatar/' + encodeURIComponent(room.presentationUserId) + '/' + 24, {
user: room.presentationUserId,
size: 24,
requesttoken: OC.requestToken,
}));
}
return(
<div className="bbb-presentation-input">
<input id={id} type="button" value={t('bbb', 'Choose a File')} onClick={filepicker} />
<p className={ room.presentationPath === '' ? 'hidden' : ''}>
<img src={getAvatarUrl()} alt={room.presentationUserId} className="bbb-avatar" height="100%" />
<em>{room.presentationPath}</em>
<button onClick={removeFile}><span className="icon icon-close icon-visible"></span></button>
</p>
</div>
);
};
export default SharedPresentationInput;