Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Option in room file management to always use system-wide default presentation ([#2662], [#2746])
- Option in room file management to set system-wide default presentation as default ([#2662], [#2746])

### Changed

- Internal improvements to room authentication flow ([#1409], [#2726])
Expand Down Expand Up @@ -674,10 +679,12 @@ You can find the changelog for older versions there [here](https://github.com/TH
[#2613]: https://github.com/THM-Health/PILOS/pull/2613
[#2616]: https://github.com/THM-Health/PILOS/pull/2616
[#2660]: https://github.com/THM-Health/PILOS/issues/2660
[#2662]: https://github.com/THM-Health/PILOS/issues/2662
[#2686]: https://github.com/THM-Health/PILOS/pull/2686
[#2726]: https://github.com/THM-Health/PILOS/pull/2726
[#2728]: https://github.com/THM-Health/PILOS/pull/2728
[#2742]: https://github.com/THM-Health/PILOS/pull/2742
[#2746]: https://github.com/THM-Health/PILOS/pull/2746
[#2751]: https://github.com/THM-Health/PILOS/issues/2751
[#2752]: https://github.com/THM-Health/PILOS/pull/2752
[#2765]: https://github.com/THM-Health/PILOS/issues/2765
Expand Down
60 changes: 49 additions & 11 deletions app/Http/Controllers/api/v1/RoomFileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreRoomFile;
use App\Http\Requests\UpdateRoomFile;
use App\Http\Requests\UpdateRoomSystemDefaultPresentation;
use App\Http\Resources\PrivateRoomFile;
use App\Models\Room;
use App\Models\RoomFile;
use App\Settings\BigBlueButtonSettings;
use App\Settings\GeneralSettings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
Expand Down Expand Up @@ -67,6 +69,11 @@ public function index(Room $room, Request $request)
// If user is allowed to view all files, return PrivateRoomFile resource to show additional information
if (Gate::allows('viewAllFiles', $room)) {
$additional['default'] = $room->files()->where('default', true)->first();
$additional['system_default'] = [
'file' => app(BigBlueButtonSettings::class)->default_presentation,
'use_in_meeting' => $room->use_system_default_presentation_in_meeting,
'use_as_default' => $room->use_system_default_presentation_as_default,
];

return PrivateRoomFile::collection($resource->paginate(app(GeneralSettings::class)->pagination_page_size))->additional($additional);
}
Expand Down Expand Up @@ -103,22 +110,26 @@ public function store(Room $room, StoreRoomFile $request)
public function update(UpdateRoomFile $request, Room $room, RoomFile $file)
{
if ($request->has('use_in_meeting')) {
$file->use_in_meeting = $request->use_in_meeting;
// If no default file for this room is set, set this file as default
if (! $room->files()->where('default', true)->exists()) {
$file->default = true;
}
$file->use_in_meeting = $request->boolean('use_in_meeting');
}

if ($request->has('download')) {
$file->download = $request->download;
$file->download = $request->boolean('download');
}

if ($request->has('default') && $request->default === true) {
// Make other files not the default
$room->files()->update(['default' => false]);
// Set this file as default
$file->default = true;
if ($request->has('default')) {
if ($request->boolean('default') === false) {
$file->default = false;
} else {
// Make other files not the default
$room->files()->whereNot('id', $file->id)->update(['default' => false]);
// Set this file as default
$file->default = true;

// If a file is set as default, the system default presentation must not be used as default
$room->use_system_default_presentation_as_default = false;
$room->save();
}
}

$file->save();
Expand All @@ -130,6 +141,33 @@ public function update(UpdateRoomFile $request, Room $room, RoomFile $file)
return response()->noContent();
}

public function updateSystemDefault(UpdateRoomSystemDefaultPresentation $request, Room $room)
{
if ($request->has('use_in_meeting')) {
$room->use_system_default_presentation_in_meeting = $request->use_in_meeting;
}

if ($request->has('default')) {
// Make other files not the default
if ($request->default === true) {
$room->files()->update(['default' => false]);

// If system default is set as default, it must also be used in the next meeting
$room->use_system_default_presentation_in_meeting = true;
}

$room->use_system_default_presentation_as_default = $request->default;
}

$room->save();

Log::info('Changed system default presentation settings in room {room}', ['room' => $room->getLogLabel()]);

$room->updateDefaultFile();

return response()->noContent();
}
Comment on lines +144 to +169
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Two issues in updateSystemDefault().

1. Inconsistent boolean access — Lines 147 and 159 use $request->use_in_meeting and $request->default (raw property access), while update() at lines 113/117 uses $request->boolean(...). Raw access can pass through strings like "1" or "0" rather than actual booleans. Use $request->boolean() for consistency.

2. Setting use_in_meeting = false doesn't clear as_default — If the user disables the system presentation for the meeting (use_in_meeting = false) without explicitly sending default = false, the room ends up with use_system_default_presentation_as_default = true while use_system_default_presentation_in_meeting = false. This is an inconsistent state — the presentation can't be the default if it's not included in the meeting.

Proposed fix
     public function updateSystemDefault(UpdateRoomSystemDefaultPresentation $request, Room $room)
     {
         if ($request->has('use_in_meeting')) {
-            $room->use_system_default_presentation_in_meeting = $request->use_in_meeting;
+            $room->use_system_default_presentation_in_meeting = $request->boolean('use_in_meeting');
+
+            // If system default is removed from the meeting, it cannot remain the default
+            if (!$request->boolean('use_in_meeting')) {
+                $room->use_system_default_presentation_as_default = false;
+            }
         }
 
         if ($request->has('default')) {
             // Make other files not the default
-            if ($request->default === true) {
+            if ($request->boolean('default')) {
                 $room->files()->update(['default' => false]);
 
                 // If system default is set as default, it must also be used in the next meeting
                 $room->use_system_default_presentation_in_meeting = true;
             }
 
-            $room->use_system_default_presentation_as_default = $request->default;
+            $room->use_system_default_presentation_as_default = $request->boolean('default');
         }
 
         $room->save();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Controllers/api/v1/RoomFileController.php` around lines 144 - 169,
The updateSystemDefault method currently reads booleans directly from $request
and can leave an inconsistent state when use_in_meeting is set to false without
default being provided; change all raw accesses ($request->use_in_meeting and
$request->default) to use $request->boolean('use_in_meeting') and
$request->boolean('default') for consistent boolean coercion, and when you
detect use_in_meeting is explicitly false while the request did not include an
explicit default=false, also clear use_system_default_presentation_as_default
(set it to false) so the room cannot have a default presentation that is not
included in the meeting; keep the existing behavior in the block where default
=== true (files()->update(...) and setting
use_system_default_presentation_in_meeting = true) and then save and call
updateDefaultFile() as before.


/**
* Remove the specified file from storage and database.
*
Expand Down
16 changes: 16 additions & 0 deletions app/Http/Requests/UpdateRoomSystemDefaultPresentation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateRoomSystemDefaultPresentation extends FormRequest
{
public function rules()
{
return [
'use_in_meeting' => ['required', 'boolean'],
'default' => ['required', 'boolean'],
];
}
}
16 changes: 15 additions & 1 deletion app/Models/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Enums\RoomUserRole;
use App\Enums\RoomVisibility;
use App\Observers\RoomObserver;
use App\Settings\BigBlueButtonSettings;
use App\Settings\GeneralSettings;
use App\Traits\AddsModelNameTrait;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
Expand All @@ -28,6 +29,8 @@ protected function casts()
{
$casts = [
'expert_mode' => 'boolean',
'use_system_default_presentation_in_meeting' => 'boolean',
'use_system_default_presentation_as_default' => 'boolean',
'delete_inactive' => 'datetime',
];

Expand Down Expand Up @@ -201,7 +204,18 @@ public function updateDefaultFile()
$currentDefault->default = false;
$currentDefault->save();
}
// If any other files are found that are used in the next meeting, select the first one to become new default

// If system has a default presentation and the system presentation is always available
// use system default presentation as default presentation for the next meeting
if (app(BigBlueButtonSettings::class)->default_presentation && $this->use_system_default_presentation_in_meeting) {
$this->use_system_default_presentation_as_default = true;
$this->save();

return;
}

// If no default file is explicitly set or the system default is not enabled
// look for the first file that is set to be used in the meeting and set it as default
$newDefaultFile = $this->files()->firstWhere('use_in_meeting', true);
if ($newDefaultFile != null) {
$newDefaultFile->default = true;
Expand Down
15 changes: 13 additions & 2 deletions app/Services/MeetingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,13 @@ public function start(): ?\BigBlueButton\Responses\CreateMeetingResponse
$meetingParams->addMeta('bbb-origin', 'PILOS');
$meetingParams->addMeta('pilos-sub-spool-dir', config('recording.spool-sub-directory'));

// get files that should be used in this meeting and add links to the files
// Use system default presentation as default, if explicitly set
$useSystemDefaultFileAsDefault = app(BigBlueButtonSettings::class)->default_presentation && $this->meeting->room->use_system_default_presentation_as_default;
if ($useSystemDefaultFileAsDefault) {
$meetingParams->addPresentation(app(BigBlueButtonSettings::class)->default_presentation);
}

// Get files that should be used in this meeting and add links to the files
$files = $this->meeting->room->files()->where('use_in_meeting', true)->orderBy('default', 'desc')->get();
foreach ($files as $file) {
// Create file download url
Expand All @@ -117,7 +123,12 @@ public function start(): ?\BigBlueButton\Responses\CreateMeetingResponse
$meetingParams->addPresentation($fileUrl, null, preg_replace("/[^A-Za-z0-9.-_\(\)]/", '', $file->filename));
}

if (empty($meetingParams->getPresentations()) && app(BigBlueButtonSettings::class)->default_presentation) {
// Add system default presentation
// Only add it, if not already added as default file
// and if no other files are present or the room is set to use the system default in meetings
if (! $useSystemDefaultFileAsDefault && app(BigBlueButtonSettings::class)->default_presentation && (
empty($files->toArray()) || $this->meeting->room->use_system_default_presentation_in_meeting
)) {
$meetingParams->addPresentation(app(BigBlueButtonSettings::class)->default_presentation);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

use App\Models\RoomFile;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('rooms', function (Blueprint $table) {
$table->boolean('use_system_default_presentation_in_meeting')->default(true);
$table->boolean('use_system_default_presentation_as_default')->default(true);
});

RoomFile::where('default', true)
->where('use_in_meeting', true)
->each(function (RoomFile $file) {
$file->room->use_system_default_presentation_as_default = false;
$file->room->save();
});
Comment on lines +20 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Data migration triggers N+1 queries and risks model/observer side effects.

Two concerns with using Eloquent models here:

  1. N+1 queries: Each iteration loads $file->room individually. For a large number of rooms with default files, this could be slow. A single query would be more efficient.

  2. Model coupling: If Room's observer (RoomObserver) performs side-effects on save (e.g., logging, cache invalidation), those will fire for every room during migration. Migrations should ideally be decoupled from application-layer concerns.

A raw DB update avoids both issues:

Proposed fix
-        RoomFile::where('default', true)
-            ->where('use_in_meeting', true)
-            ->each(function (RoomFile $file) {
-                $file->room->use_system_default_presentation_as_default = false;
-                $file->room->save();
-            });
+        DB::table('rooms')
+            ->whereIn('id', function ($query) {
+                $query->select('room_id')
+                    ->from('room_files')
+                    ->where('default', true)
+                    ->where('use_in_meeting', true);
+            })
+            ->update(['use_system_default_presentation_as_default' => false]);

You'll need to add use Illuminate\Support\Facades\DB; at the top.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RoomFile::where('default', true)
->where('use_in_meeting', true)
->each(function (RoomFile $file) {
$file->room->use_system_default_presentation_as_default = false;
$file->room->save();
});
DB::table('rooms')
->whereIn('id', function ($query) {
$query->select('room_id')
->from('room_files')
->where('default', true)
->where('use_in_meeting', true);
})
->update(['use_system_default_presentation_as_default' => false]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@database/migrations/2026_01_19_105554_add_use_system_default_presentation_in_next_meeting_to_rooms_table.php`
around lines 20 - 25, Replace the per-model loop that loads Room models via
RoomFile::each (the RoomFile model and its $file->room access) with a single raw
DB update to avoid N+1 and model observer side-effects: compute the set of room
IDs that have a default, use_in_meeting RoomFile and then run a
DB::table('rooms')->whereIn('id',
$ids)->update(['use_system_default_presentation_as_default' => false]); also add
use Illuminate\Support\Facades\DB; to the file header; do not call Room::save()
or rely on Room observers in the migration.


}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('rooms', function (Blueprint $table) {
$table->dropColumn('use_system_default_presentation_in_meeting');
$table->dropColumn('use_system_default_presentation_as_default');
});
}
};
17 changes: 11 additions & 6 deletions lang/en/rooms.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,14 @@
'feature_disabled_system' => ':name is disabled. Please contact the administrator.',
'files' => [
'confirm_delete' => 'Do you want to delete this file :filename?',
'default' => 'Default',
'default' => 'Default presentation',
'delete' => 'Delete file',
'download_hidden' => 'Download hidden',
'download_visible' => 'Download visible',
'download_allowed' => 'Download allowed',
'download_not_allowed' => 'Download not allowed',
'downloadable' => 'Downloadable',
'edit' => 'Edit file',
'configure' => 'Configure file',
Comment on lines +112 to +115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for dangling references to removed translation keys
rg -n --type=vue --type=js --type=php \
  -e "files\.download_hidden" \
  -e "files\.download_visible" \
  -e "files\.edit['\"]" \
  -e "use_in_next_meeting" \
  -e "download_hidden" \
  -e "download_visible" \
  .

Repository: THM-Health/PILOS

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Also search in JSON files and any files that might reference these keys
rg -n "download_hidden|download_visible|use_in_next_meeting" \
  --type=json --type=ts --type=jsx .

Repository: THM-Health/PILOS

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Check for other locale files that may need updates
fd -e php -path "*/lang/*" | head -20

Repository: THM-Health/PILOS

Length of output: 228


🏁 Script executed:

#!/bin/bash
# Read the actual file to understand the context of changes
cat -n lang/en/rooms.php | sed -n '100,150p'

Repository: THM-Health/PILOS

Length of output: 2927


🏁 Script executed:

#!/bin/bash
# Search for dangling references to removed keys with simpler patterns
echo "=== Checking for references to removed keys ==="
rg "download_hidden|download_visible" -n --no-heading
rg "files\.edit" -n --no-heading
rg "use_in_next_meeting" -n --no-heading

Repository: THM-Health/PILOS

Length of output: 7710


🏁 Script executed:

#!/bin/bash
# Find all locale files
echo "=== Finding all locale translation files ==="
find . -path "*/lang/*/rooms.php" -type f

Repository: THM-Health/PILOS

Length of output: 231


🏁 Script executed:

#!/bin/bash
# Check for files that reference the old keys
rg "download_hidden" . 2>/dev/null | head -20

Repository: THM-Health/PILOS

Length of output: 1063


🏁 Script executed:

#!/bin/bash
# Look for Vue component files that might reference these translation keys
find . -name "*.vue" -o -name "*.js" | head -20 | xargs rg -l "download_hidden|download_visible|use_in_next_meeting" 2>/dev/null || echo "No matches found"

Repository: THM-Health/PILOS

Length of output: 76


Critical: Translation keys were changed in English but not propagated to other locales or E2E tests.

The removed keys (files.download_hidden, files.download_visible, files.edit, files.use_in_next_meeting, files.use_in_next_meeting_disabled) are still referenced in:

  • E2E tests: ~50+ assertions across RoomsViewFiles.cy.js and RoomsViewFilesFileActions.cy.js expect these old keys to appear in the UI
  • Other language files: de, de-gender, fr, fa, ar all still define the old keys while the English file uses new ones

This creates a critical inconsistency where E2E tests will fail and non-English locales will have mismatched translation keys. All language files must be updated together with the same keys, and E2E tests must be updated to check for the new keys (download_allowed, download_not_allowed, always_available_in_meeting, available_in_next_meeting, not_available_in_next_meeting, configure).

Additionally, line 141 has a key naming inconsistency: always_available_in_meeting omits the _next_ infix present in available_in_next_meeting and not_available_in_next_meeting. Consider renaming to always_available_in_next_meeting for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lang/en/rooms.php` around lines 112 - 115, The English translation keys for
file actions were changed but not propagated—update all locale files that still
define the old keys (files.download_hidden, files.download_visible, files.edit,
files.use_in_next_meeting, files.use_in_next_meeting_disabled) to instead define
the new keys (download_allowed, download_not_allowed,
always_available_in_meeting OR rename to always_available_in_next_meeting for
consistency, available_in_next_meeting, not_available_in_next_meeting,
configure) so all locales match English; update all E2E assertions in
RoomsViewFiles.cy.js and RoomsViewFilesFileActions.cy.js to expect the new keys
(download_allowed, download_not_allowed, always_available_in_next_meeting or
always_available_in_next_meeting if you rename, available_in_next_meeting,
not_available_in_next_meeting, configure); if you choose to rename
always_available_in_meeting to always_available_in_next_meeting, rename that key
across translations, code references and tests to keep naming consistent.

'configure_dialog_title' => 'Configure ":name"',
'configure_system_default' => 'Configure system-wide default presentation',
'filter' => [
'all' => 'All files',
'downloadable' => 'Downloadable files',
Expand All @@ -126,6 +128,8 @@
'filename' => 'Filename',
'uploaded_at' => 'Added',
],
'system_default' => 'System-wide default presentation',
'system_default_description' => 'Automatically used when no other presentation is enabled for the next video conference',
'terms_of_use' => [
'accept' => 'I accept the terms of use',
'required' => 'You must accept the terms of use before downloading this file.',
Expand All @@ -134,8 +138,9 @@
'title' => 'Files',
'upload' => 'Upload files',
'uploaded' => 'File \':name\' uploaded',
'use_in_next_meeting' => 'Use in the next meeting',
'use_in_next_meeting_disabled' => 'Not available in video conference',
'always_available_in_meeting' => 'Always available in next video conference',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Key name is inconsistent with its siblings and the value text is ambiguous.

  • The key always_available_in_meeting drops the _next_ infix, while both siblings use _in_next_meeting. For consistency it should be always_available_in_next_meeting.
  • The value "Always available in next video conference" is internally contradictory: always implies all future meetings, while next implies only the immediately upcoming one. If the intent is a sticky/persistent setting, consider "Always available in video conferences"; if it is one-shot like the others, drop "always".
✏️ Proposed rename and wording fix
-        'always_available_in_meeting' => 'Always available in next video conference',
+        'always_available_in_next_meeting' => 'Always available in video conferences',

Any component referencing files.always_available_in_meeting would need to be updated to files.always_available_in_next_meeting accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lang/en/rooms.php` at line 141, Rename the translation key
files.always_available_in_meeting to files.always_available_in_next_meeting and
update its value text to remove the ambiguity — either make it one-shot like
siblings (e.g., "Available in next video conference") or persistent (e.g.,
"Always available in video conferences"); pick the correct intent, change the
key to always_available_in_next_meeting if it's next-meeting scoped, and update
any component references that use always_available_in_meeting to the new key
name.

'available_in_next_meeting' => 'Available in next video conference',
'not_available_in_next_meeting' => 'Not available in next video conference',
'view' => 'View file',
],
'first_and_lastname' => 'First- and last name',
Expand Down
5 changes: 5 additions & 0 deletions resources/css/override/_animations.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@
animation-duration: 0s !important;
}
}

.inline-note.p-message-enter-active,
.inline-note.p-message-leave-active {
animation-duration: 0s !important;
}
6 changes: 5 additions & 1 deletion resources/js/components/InlineNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ defineProps({
</script>

<template>
<Message :severity="severity" aria-live="off" role="presentation"
<Message
:severity="severity"
class="inline-note"
aria-live="off"
role="presentation"
><slot
/></Message>
</template>
Loading
Loading