Skip to content

Commit d757d35

Browse files
committed
Merge branch 'main' into issue-1255
2 parents b377711 + 4abf0e7 commit d757d35

File tree

14 files changed

+839
-389
lines changed

14 files changed

+839
-389
lines changed

.github/workflows/run-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
run: docker compose exec -T studip_db mysql -u studip_user --password=studip_password studip_db < ./oc.sql
5757

5858
- name: Trigger playlists migration to Opencast
59-
run: curl http://localhost/plugins.php/opencastv3/api/migrate_playlists -u root@studip:testing
59+
run: curl http://localhost/plugins.php/opencastv3/api/migrate_playlists -u root@studip:testing --fail-with-body
6060

6161
- name: Run tests
6262
run: npm run tests

app/controllers/redirect.php

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use Opencast\Models\VideosShares;
55
use Opencast\Models\LTI\LtiHelper;
66
use Opencast\Models\REST\ApiEventsClient;
7+
use Opencast\Errors\Error;
78

89
class RedirectController extends Opencast\Controller
910
{
@@ -87,15 +88,46 @@ public function download_action($token, $type, $index)
8788

8889
$publication = $video->publication? json_decode($video->publication, true) : null;
8990
if (!empty($publication) && isset($publication['downloads'][$type][$index]['url'])) {
90-
$url = $publication['downloads'][$type][$index]['url'];
9191

92-
$api_events = ApiEventsClient::getInstance($video->config_id);
93-
$response = $api_events->fileRequest($url);
92+
// Make sure the server configs are overwritten, in order to allow large file downloads.
93+
ignore_user_abort(true);
94+
set_time_limit(0);
95+
ini_set('memory_limit', '512M');
9496

95-
header('Content-Type: '. $response['mimetype']);
96-
97-
echo $response['body'];
98-
die;
97+
// Clean all output buffers.
98+
while (ob_get_level()) {
99+
ob_end_clean();
100+
}
101+
try {
102+
$url = $publication['downloads'][$type][$index]['url'];
103+
104+
$api_events = ApiEventsClient::getInstance($video->config_id);
105+
// Since we are using stream, we need to perform the get directly here! Doing it in another file and pass the body as a parameter won't work!
106+
$response = $api_events->ocRestClient->get($url, $api_events->getStreamDownloadConfig());
107+
$stream = $response->getBody();
108+
109+
// Set headers properly.
110+
header('Content-Type: ' . $response->getHeaderLine('Content-Type') ?: 'application/octet-stream');
111+
header('Content-Length: ' . $response->getHeaderLine('Content-Length'));
112+
header('Content-Disposition: attachment; filename*=UTF-8\'\'' . basename($url));
113+
header('Cache-Control: no-cache');
114+
header('Pragma: no-cache');
115+
116+
// Stream in chunks
117+
while (!$stream->eof()) {
118+
// A double check to make sure the connection is still open.
119+
if(connection_status() != CONNECTION_NORMAL){
120+
// If not leave the loop!
121+
break;
122+
}
123+
// 1MB per chunk.
124+
echo $stream->read(1024 * 1024);
125+
flush();
126+
}
127+
die;
128+
} catch (\Throwable $th) {
129+
throw new Error(_('Fehler beim Herunterladen der Datei').': ' . $th->getMessage(), 500);
130+
}
99131
}
100132
}
101133

assets/css/opencast.scss

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,13 @@ h2.oc--loadingbar, .oc--loadingbar-title {
723723
}
724724
}
725725

726+
&.oc--minimal-progress {
727+
height: 3px !important;
728+
.oc--progress-bar {
729+
height: 3px !important;
730+
}
731+
}
732+
726733
margin: 5px 0;
727734
}
728735

@@ -911,6 +918,134 @@ div.oc--dialog-possibilities {
911918
display: block;
912919
}
913920

921+
/* * * * * * * * * * * */
922+
/* DOWNLOAD VIDEOS */
923+
/* * * * * * * * * * * */
924+
925+
.oc--download-list-container {
926+
display: flex;
927+
flex-direction: column;
928+
height: 100%;
929+
.oc--download-list {
930+
flex-grow: 1;
931+
}
932+
.oc--download-messages {
933+
flex-shrink: 0;
934+
}
935+
936+
.oc--download-item {
937+
position: relative;
938+
display: flex;
939+
flex-direction: column;
940+
justify-content: start;
941+
align-items: stretch;
942+
padding: 5px 0px;
943+
944+
button.button {
945+
margin: 0px !important;
946+
}
947+
948+
.oc--download-item-control-row {
949+
display: flex;
950+
flex-direction: row;
951+
justify-content: space-between;
952+
align-items: center;
953+
width: 100%;
954+
gap: 5px;
955+
956+
.oc--tooltip--copy {
957+
width: 40px;
958+
flex-shrink: 0;
959+
display: flex;
960+
justify-content: center;
961+
align-items: center;
962+
963+
.oc--tooltip--copy-success {
964+
top: 0;
965+
}
966+
}
967+
}
968+
969+
.oc--download-btn-container {
970+
position: relative;
971+
display: flex;
972+
flex-direction: row;
973+
justify-content: space-between;
974+
align-items: center;
975+
width: 100%;
976+
977+
.oc--download-btn {
978+
flex-grow: 1;
979+
position: relative;
980+
}
981+
982+
.oc--download-cancel {
983+
margin-left: 5px;
984+
flex-shrink: 0;
985+
display: flex;
986+
justify-content: center;
987+
align-items: center;
988+
989+
&.active {
990+
cursor: pointer;
991+
}
992+
993+
&.inactive {
994+
cursor: default;
995+
}
996+
}
997+
}
998+
999+
.oc--download-info-container {
1000+
display: flex;
1001+
flex-direction: row;
1002+
justify-content: start;
1003+
align-items: center;
1004+
.oc--download-info-status {
1005+
flex: 1;
1006+
margin-left: 5px;
1007+
font-size: 0.8em;
1008+
}
1009+
}
1010+
1011+
.oc--download-spinner-container {
1012+
z-index: 999;
1013+
position: absolute;
1014+
top: 50%;
1015+
translate: 0 -50%;
1016+
left: 2px;
1017+
margin-right: 5px;
1018+
flex-shrink: 0;
1019+
display: flex;
1020+
justify-content: center;
1021+
align-items: center;
1022+
width: 24px;
1023+
height: 24px;
1024+
1025+
.oc--spinner {
1026+
transform: scale(0.5);
1027+
}
1028+
.oc--spinner:after {
1029+
content: " ";
1030+
display: block;
1031+
width: 24px;
1032+
height: 24px;
1033+
border-radius: 50%;
1034+
border: 6px solid;
1035+
border-color: #28497c transparent #28497c transparent;
1036+
animation: oc-spinner 1.2s linear infinite;
1037+
}
1038+
@keyframes oc-spinner {
1039+
0% {
1040+
transform: rotate(0deg);
1041+
}
1042+
100% {
1043+
transform: rotate(360deg);
1044+
}
1045+
}
1046+
}
1047+
}
1048+
}
9141049

9151050
/* * * * * * * * * * * * * * * * * * */
9161051
/* U P L O A D E L E M E N T S */
@@ -1327,7 +1462,6 @@ label.oc--file-upload {
13271462
/* E M B E D D I N G C O D E */
13281463
/* * * * * * * * * * * * * * * * * */
13291464

1330-
.oc--embedding-code-text {
1331-
width: 100%;
1332-
box-sizing: border-box;
1333-
}
1465+
.oc--video-actions-embedding {
1466+
margin-bottom: 1em;
1467+
}

lib/Models/REST/Config.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ public static function retrieveRESTservices($components, $service_url)
6565
if (($service_url['host'] == "localhost" || !preg_match('#https?://localhost.*#', $service->host))
6666
&& mb_strpos($service->host, $service_url['scheme']) === 0
6767
) {
68-
// check if service is wanted, active and online
68+
// check if service is wanted, active, online and not in maintenance
6969
if (isset($oc_services[$service->type])
70-
&& $service->online && $service->active
70+
&& $service->online && $service->active && !$service->maintenance
7171
) {
7272
// TODO: check duplicate entries for same service-type
7373
$services[$service->host . $service->path]

lib/Models/REST/RestClient.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@ public function __construct($config)
8282
$this->ocRestClient = new OcRestClient($oc_config);
8383
}
8484

85+
/**
86+
* Get the Guzzle client options suitable for stream downloading files.
87+
*
88+
* @return array
89+
*/
90+
public function getStreamDownloadConfig() {
91+
return [
92+
'auth' => [$this->username, $this->password],
93+
'timeout' => 0,
94+
'connect_timeout' => 0,
95+
'stream' => true,
96+
];
97+
}
98+
8599
public function fileRequest($file_url)
86100
{
87101
$response = $this->ocRestClient->get($file_url, [

lib/Routes/Opencast/UserRoles.php

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,19 @@ public function __invoke(Request $request, Response $response, $args)
102102
} else {
103103
// Handle video roles
104104

105-
// get all videos the user has permissions on
106-
foreach (VideosUserPerms::findByUser_id($user_id) as $vperm) {
107-
if (!$vperm->video->episode) continue;
108-
109-
if ($vperm->perm == 'owner' || $vperm->perm == 'write') {
110-
if ($episode_id_role_access) {
111-
$roles['ROLE_EPISODE_' . $vperm->video->episode . '_READ'] = 'ROLE_EPISODE_' . $vperm->video->episode . '_READ';
112-
$roles['ROLE_EPISODE_' . $vperm->video->episode . '_WRITE'] = 'ROLE_EPISODE_' . $vperm->video->episode . '_WRITE';
113-
} else {
114-
$roles[$vperm->video->episode . '_write'] = $vperm->video->episode . '_write';
115-
}
116-
} else {
117-
if ($episode_id_role_access) {
118-
$roles['ROLE_EPISODE_' . $vperm->video->episode . '_READ'] = 'ROLE_EPISODE_' . $vperm->video->episode . '_READ';
119-
} else {
120-
$roles[$vperm->video->episode . '_read'] = $vperm->video->episode . '_read';
121-
}
122-
}
123-
}
124-
125-
// get all videos in courseware blocks in courses and add them to the permission list as well
105+
// get all videos of courseware blocks in courses and add them to the permission list as well, ignore if user has permission through a course role
126106
$stmt_courseware = \DBManager::get()->prepare("SELECT episode FROM oc_video_cw_blocks
127107
LEFT JOIN oc_video USING (token)
128108
LEFT JOIN seminar_user USING (seminar_id)
129-
WHERE seminar_user.user_id = :user_id");#
109+
WHERE seminar_user.user_id = :user_id
110+
AND NOT EXISTS (
111+
SELECT 1
112+
FROM oc_playlist_video
113+
JOIN oc_playlist_seminar USING (playlist_id)
114+
WHERE video_id = oc_video.id
115+
AND oc_video_cw_blocks.seminar_id = oc_playlist_seminar.seminar_id
116+
)
117+
");
130118

131119
$stmt_courseware->execute([':user_id' => $user_id]);
132120

@@ -180,6 +168,37 @@ public function __invoke(Request $request, Response $response, $args)
180168
$roles[$course_id . '_Learner'] = $course_id . '_Learner';
181169
}
182170

171+
// Get all videos permissions of the user except those for videos to which the user has already write
172+
// permissions through course memberships and course playlists, in order to reduce the number of roles.
173+
$vperms = VideosUserPerms::findBySQL("
174+
user_id = :user_id
175+
AND NOT EXISTS (
176+
SELECT 1
177+
FROM oc_playlist_video
178+
JOIN oc_playlist_seminar USING (playlist_id)
179+
WHERE video_id = oc_video_user_perms.video_id AND seminar_id IN ( :course_ids )
180+
)
181+
", [':user_id' => $user_id, ':course_ids' => $courses_write]);
182+
183+
foreach ($vperms as $vperm) {
184+
if (!$vperm->video->episode) continue;
185+
186+
if ($vperm->perm == 'owner' || $vperm->perm == 'write') {
187+
if ($episode_id_role_access) {
188+
$roles['ROLE_EPISODE_' . $vperm->video->episode . '_READ'] = 'ROLE_EPISODE_' . $vperm->video->episode . '_READ';
189+
$roles['ROLE_EPISODE_' . $vperm->video->episode . '_WRITE'] = 'ROLE_EPISODE_' . $vperm->video->episode . '_WRITE';
190+
} else {
191+
$roles[$vperm->video->episode . '_write'] = $vperm->video->episode . '_write';
192+
}
193+
} else {
194+
if ($episode_id_role_access) {
195+
$roles['ROLE_EPISODE_' . $vperm->video->episode . '_READ'] = 'ROLE_EPISODE_' . $vperm->video->episode . '_READ';
196+
} else {
197+
$roles[$vperm->video->episode . '_read'] = $vperm->video->episode . '_read';
198+
}
199+
}
200+
}
201+
183202
// Handle playlist roles
184203

185204
if (PlaylistMigration::isConverted()) {

lib/Routes/Video/VideoWorldwideShareUpdate.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ public function __invoke(Request $request, Response $response, $args)
2929
throw new Error(_('Das Video kann nicht gefunden werden'), 404);
3030
}
3131

32+
// Take care of running videos by returning a proper response not throwing an exception!
33+
if ($video->state === 'running') {
34+
return $this->createResponse(
35+
_('Das Video befindet sich aktuell in der Bearbeitung. Bitte versuchen Sie es später erneut.'),
36+
$response->withStatus(409)
37+
);
38+
}
39+
3240
if (!$video->havePerm('write')
3341
|| !\Config::get()->OPENCAST_ALLOW_PUBLIC_SHARING
3442
) {
@@ -38,16 +46,21 @@ public function __invoke(Request $request, Response $response, $args)
3846
$json = $this->getRequestData($request);
3947
$visibility = $json['visibility'];
4048

41-
if ($video->setWorldVisibility($visibility) !== true) {
42-
return $this->createResponse(
43-
_('Beim Übertragen der Änderungen zum Videoserver ist ein Fehler aufgetreten.')
44-
, $response->withStatus(500));
45-
} else {
46-
return $this->createResponse(
47-
$visibility === 'public'
49+
$response_code = 500;
50+
$response_message = _('Beim Übertragen der Änderungen zum Videoserver ist ein Fehler aufgetreten.');
51+
52+
try {
53+
$result = $video->setWorldVisibility($visibility);
54+
if ($result === true) {
55+
$response_code = 200;
56+
$response_message = $visibility === 'public'
4857
? _('Das Video wurde auf weltweit zugreifbar gestellt.')
49-
: _('Das Video wurde nur berechtigten Personen zugreifbar gemacht.')
50-
, $response->withStatus(200));
58+
: _('Das Video wurde nur berechtigten Personen zugreifbar gemacht.');
59+
}
60+
} catch (\Throwable $th) {
61+
$response_message .= ' (' . $th->getMessage() . ')';
5162
}
63+
64+
return $this->createResponse($response_message, $response->withStatus($response_code));
5265
}
5366
}

0 commit comments

Comments
 (0)