Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6be858f
Refactor frontend file validation and hints
samuelwei Jun 15, 2026
d07bcc0
Fix cs
samuelwei Jun 16, 2026
48226a5
Update changelog
samuelwei Jun 30, 2026
b991852
Fix invalid file upload extension
samuelwei Jun 30, 2026
186288f
Remove whitespaces
samuelwei Jun 30, 2026
7211c72
Fix cs
samuelwei Jun 30, 2026
8b7801d
Fix profile image ext. in tests
samuelwei Jun 30, 2026
07e9380
Merge branch 'develop' into file-hint-and-validation
samuelwei Jul 7, 2026
9d69644
Fix invalid return type of fieldError
samuelwei Jul 7, 2026
8d80348
Rename props
samuelwei Jul 7, 2026
851ef5d
Refactor login components to use formErrors
samuelwei Jul 7, 2026
037e9cc
Code cleanup
samuelwei Jul 7, 2026
efecb48
Fix invalid prop names
samuelwei Jul 7, 2026
3390545
Fix image preview streching
samuelwei Jul 29, 2026
bd3d86a
Move image validation logic to dedicated rule
samuelwei Jul 29, 2026
e6f0a1c
Adjust frontend allowed file ext. to new allowed file ext.
samuelwei Jul 29, 2026
2c24fae
Update location to support document types in BBB
samuelwei Jul 29, 2026
3c1bca4
Fix wrong locale keys
samuelwei Jul 29, 2026
c7e3e4a
Fix file size calc
samuelwei Jul 29, 2026
8fc5ff8
Fix invalid js doctypes
samuelwei Jul 29, 2026
14da060
Fix invalid frontend max file size
samuelwei Jul 29, 2026
e6047d7
Merge branch 'develop' into file-hint-and-validation
samuelwei Jul 29, 2026
c6144af
Make login tabs lazy again
samuelwei Jul 30, 2026
58c229c
Code cleanup
samuelwei Jul 30, 2026
82671c4
Update changelog
samuelwei Jul 30, 2026
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Environment variable `VERSION` to change the displayed version in the footer ([#3300], [#3302])
- System-wide default welcome message ([#3301])
- Hints in admin UI file uploads indicating supported file types and maximum allowed file size ([#3235])

### Changed

- Allow SVG and WebP images to be used as livestream pause images ([#3235])

### Fixed

Expand Down Expand Up @@ -865,6 +870,7 @@ You can find the changelog for older versions there [here](https://github.com/TH
[#3196]: https://github.com/THM-Health/PILOS/pull/3196
[#3198]: https://github.com/THM-Health/PILOS/pull/3198
[#3215]: https://github.com/THM-Health/PILOS/pull/3215
[#3235]: https://github.com/THM-Health/PILOS/pull/3235
[#3264]: https://github.com/THM-Health/PILOS/pull/3264
[#3277]: https://github.com/THM-Health/PILOS/pull/3277
[#3296]: https://github.com/THM-Health/PILOS/pull/3296
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Requests/StoreRoomFileRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@

use App\Rules\Antivirus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\File;

class StoreRoomFileRequest extends FormRequest
{
public function rules()
{
return [
'file' => ['bail', 'required', 'file', 'max:'.(config('bigbluebutton.max_filesize') * 1000), 'mimes:'.config('bigbluebutton.allowed_file_mimes'), new Antivirus], // https://github.com/bigbluebutton/bigbluebutton/blob/v2.2.x-release/bigbluebutton-html5/private/config/settings.yml
'file' => ['bail', 'required', File::types(config('bigbluebutton.allowed_file_mimes'))->extensions(config('bigbluebutton.allowed_file_mimes'))->max(config('bigbluebutton.max_filesize').'mb'), new Antivirus], // https://docs.bigbluebutton.org/development/api/#supported-document-types
];
}
}
4 changes: 3 additions & 1 deletion app/Http/Requests/UpdateRoomStreamingConfigRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace App\Http\Requests;

use App\Rules\Antivirus;
use App\Rules\Image;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateRoomStreamingConfigRequest extends FormRequest
{
Expand All @@ -19,7 +21,7 @@ public function rules()
return [
'enabled' => ['required', 'boolean'],
'url' => ['nullable', 'required_if_accepted:enabled', 'string', 'url:rtmp,rtmps', 'max:255'],
'pause_image' => ['bail', 'nullable', 'image', 'mimes:jpg,bmp,png,gif', 'max:5000', 'dimensions:width=1920,height=1080', new Antivirus], // 5 MB
'pause_image' => ['bail', 'nullable', Image::default()->max('5mb'), Rule::dimensions()->width(1920)->height(1080), new Antivirus],
];
}

Expand Down
4 changes: 3 additions & 1 deletion app/Http/Requests/UpdateRoomTypeStreamingSettingsRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace App\Http\Requests;

use App\Rules\Antivirus;
use App\Rules\Image;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateRoomTypeStreamingSettingsRequest extends FormRequest
{
Expand All @@ -18,7 +20,7 @@ public function rules()
{
return [
'enabled' => ['required', 'boolean'],
'default_pause_image' => ['bail', 'nullable', 'image', 'mimes:jpg,bmp,png,gif', 'max:5000', 'dimensions:width=1920,height=1080', new Antivirus], // 5 MB
'default_pause_image' => ['bail', 'nullable', Image::default()->max('5mb'), Rule::dimensions()->width(1920)->height(1080), new Antivirus],
];
}
}
20 changes: 11 additions & 9 deletions app/Http/Requests/UpdateSettingsRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
use App\Enums\LinkTarget;
use App\Enums\TimePeriod;
use App\Rules\Antivirus;
use App\Rules\Image;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\File;

class UpdateSettingsRequest extends FormRequest
{
Expand Down Expand Up @@ -44,16 +46,16 @@ public function rules()
'general_no_welcome_page' => ['required', 'boolean'],

'theme_logo' => ['required_without:theme_logo_file', 'string', 'max:255'],
'theme_logo_file' => ['bail', 'required_without:theme_logo', 'image:allow_svg', 'max:500', new Antivirus], // 500 KB, larger files are bad for loading times
'theme_logo_file' => ['bail', 'required_without:theme_logo', Image::logo(), new Antivirus],
'theme_logo_dark' => ['required_without:theme_logo_dark_file', 'string', 'max:255'],
'theme_logo_dark_file' => ['bail', 'required_without:theme_logo_dark', 'image:allow_svg', 'max:500', new Antivirus], // 500 KB, larger files are bad for loading times
'theme_logo_dark_file' => ['bail', 'required_without:theme_logo_dark', Image::logo(), new Antivirus],
'theme_favicon' => ['required_without:theme_favicon_file', 'string', 'max:255'],
'theme_favicon_file' => ['bail', 'required_without:theme_favicon', 'mimes:ico', 'max:500', new Antivirus], // 500 KB, larger files are bad for loading times
'theme_favicon_file' => ['bail', 'required_without:theme_favicon', Image::favicon(), new Antivirus],
'theme_favicon_dark' => ['required_without:theme_favicon_dark_file', 'string', 'max:255'],
'theme_favicon_dark_file' => ['bail', 'required_without:theme_favicon_dark', 'mimes:ico', 'max:500', new Antivirus], // 500 KB, larger files are bad for loading times
'theme_favicon_dark_file' => ['bail', 'required_without:theme_favicon_dark', Image::favicon(), new Antivirus],
'theme_primary_color' => ['required', 'string', 'hex_color'],
'theme_rounded' => ['required', 'boolean'],
'theme_custom_css' => ['bail', 'nullable', 'file', 'max:500', 'extensions:css', new Antivirus],
'theme_custom_css' => ['bail', 'nullable', File::types(['css', 'txt'])->extensions('css')->max('500kb'), new Antivirus],

'banner_enabled' => ['required', 'boolean'],
'banner_title' => ['nullable', 'string', 'max:255'],
Expand Down Expand Up @@ -84,12 +86,12 @@ public function rules()
'recording_recording_retention_period' => ['required', 'numeric', Rule::enum(TimePeriod::class)->except($disabledRecordingRetentionPeriods)],

'bbb_logo' => ['nullable', 'string', 'max:255'],
'bbb_logo_file' => ['bail', 'image:allow_svg', 'max:500', new Antivirus],
'bbb_logo_file' => ['bail', Image::logo(), new Antivirus],
'bbb_logo_dark' => ['nullable', 'string', 'max:255'],
'bbb_logo_dark_file' => ['bail', 'image:allow_svg', 'max:500', new Antivirus],
'bbb_logo_dark_file' => ['bail', Image::logo(), new Antivirus],

'bbb_style' => ['bail', 'nullable', 'file', 'max:500', 'extensions:css', new Antivirus],
'bbb_default_presentation' => ['bail', 'nullable', 'file', 'max:'.(config('bigbluebutton.max_filesize') * 1000), 'mimes:'.config('bigbluebutton.allowed_file_mimes'), new Antivirus],
'bbb_style' => ['bail', 'nullable', File::types(['css', 'txt'])->extensions('css')->max('500kb'), new Antivirus], // 500 KB, larger files are bad for loading times
'bbb_default_presentation' => ['bail', 'nullable', File::types(config('bigbluebutton.allowed_file_mimes'))->extensions(config('bigbluebutton.allowed_file_mimes'))->max(config('bigbluebutton.max_filesize').'mb'), new Antivirus],
'bbb_default_welcome_message' => ['bail', 'nullable', 'max:'.config('bigbluebutton.welcome_message_limit'), 'string'],
];
}
Expand Down
7 changes: 5 additions & 2 deletions app/Http/Requests/UpdateStreamingSettingsRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

use App\Rules\Antivirus;
use App\Rules\CustomJoinMeetingParameters;
use App\Rules\Image;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\File;

class UpdateStreamingSettingsRequest extends FormRequest
{
Expand All @@ -18,8 +21,8 @@ class UpdateStreamingSettingsRequest extends FormRequest
public function rules()
{
return [
'default_pause_image' => ['bail', 'nullable', 'image', 'mimes:jpg,bmp,png,gif', 'max:5000', 'dimensions:width=1920,height=1080', new Antivirus], // 5 MB
'css_file' => ['bail', 'nullable', 'file', 'max:500', 'extensions:css', new Antivirus],
'default_pause_image' => ['bail', 'nullable', Image::default()->max('5mb'), Rule::dimensions()->width(1920)->height(1080), new Antivirus],
'css_file' => ['bail', 'nullable', File::types(['css', 'txt'])->extensions('css')->max('500kb'), new Antivirus],
'join_parameters' => ['nullable', 'string', 'max:65000', new CustomJoinMeetingParameters],
];
}
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Requests/UserRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Models\Role;
use App\Rules\Antivirus;
use App\Rules\Image;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
Expand All @@ -27,7 +28,7 @@ public function rules()
'timezone' => ['sometimes', 'required', Rule::in(timezone_identifiers_list())],
'roles' => ['sometimes', 'required', 'array'],
'roles.*' => ['sometimes', 'distinct', 'integer', 'exists:App\Models\Role,id', Rule::notIn($prohibitedRoles)],
'image' => ['bail', 'sometimes', 'nullable', 'mimes:jpg', 'dimensions:width=100,height=100', Rule::prohibitedIf($this->user?->has_external_image), new Antivirus],
'image' => ['bail', 'sometimes', 'nullable', Image::types('jpg')->extensions('jpg')->max('50kb'), Rule::dimensions()->width(100)->height(100), Rule::prohibitedIf($this->user?->has_external_image), new Antivirus],
];

if (! $this->user || $this->user->authenticator === 'local') {
Expand Down
29 changes: 29 additions & 0 deletions app/Rules/Image.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace App\Rules;

use Illuminate\Validation\Rules\File;

class Image extends File
{
public static function default(): Image
{
return self::types(['jpg', 'png', 'gif', 'svg', 'webp', 'bmp'])
->extensions(['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp']);
Comment thread
samuelwei marked this conversation as resolved.
}

public static function logo(): Image
{
return self::default()
->max('500kb'); // larger files are bad for loading times
}

public static function favicon(): Image
{
return self::types(['ico'])
->extensions(['ico'])
->max('500kb'); // larger files are bad for loading times
}
}
5 changes: 4 additions & 1 deletion config/bigbluebutton.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

use BigBlueButton\Enum\HashingAlgorithm;

$allowedFileMimes = array_values(array_unique(array_filter(array_map('trim', explode(',', env('BBB_ALLOWED_FILE_MIMES', ''))))));
$allowedFileMimesFallback = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'odt', 'ods', 'odp', 'odg', 'odc', 'odi', 'jpg', 'jpeg', 'png'];

return [
'test_server' => [
'host' => env('BBB_TEST_SERVER_HOST'),
'secret' => env('BBB_TEST_SERVER_SECRET'),
],
'max_filesize' => (int) env('BBB_MAX_FILESIZE', 30),
'allowed_file_mimes' => env('BBB_ALLOWED_FILE_MIMES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,odp,odg,odc,odi,jpg,jpeg,png'),
'allowed_file_mimes' => ! empty($allowedFileMimes) ? $allowedFileMimes : $allowedFileMimesFallback,
'welcome_message_limit' => (int) env('WELCOME_MESSAGE_LIMIT', 500), // max 5000
'room_name_limit' => (int) env('ROOM_NAME_LIMIT', 50),
'room_id_max_tries' => (int) env('BBB_ROOM_ID_MAX_TRIES', 1000),
Expand Down
4 changes: 3 additions & 1 deletion lang/en/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@
'user' => 'User',
'user_name' => 'Name',
'users' => 'Users',
'validation' => [
'file' => [
'allowed_formats' => 'Allowed file formats: :formats',
'max_size' => 'Max. file size: :size',
'invalid_type' => 'The file type is not allowed.',
'too_large' => 'The selected file is too large.',
],
Expand Down
4 changes: 1 addition & 3 deletions lang/en/rooms.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,8 @@
'downloadable' => 'Downloadable files',
'use_in_meeting' => 'Files available in video conference',
],
'formats' => 'Allowed file formats: :formats',
'nodata' => 'No files available',
'select_or_drag' => 'Select a file or drag and drop it here...',
'size' => 'Max. file size: :size MB',
'sort' => [
'filename' => 'Filename',
'uploaded_at' => 'Added',
Expand Down Expand Up @@ -470,7 +468,7 @@
'enabled' => 'Enabled',
'pause_image' => 'Pause image',
'pause_image_alt' => 'Pause image',
'pause_image_format' => 'Format: PNG, JPEG, GIF, BMP; Resolution: 1920x1080px',
'pause_image_resolution' => 'Resolution: 1920x1080px',
'title' => 'Streaming configuration',
'url' => 'RTMP(S) URL',
],
Expand Down
20 changes: 15 additions & 5 deletions resources/js/components/AdminStreamingRoomTypeEditButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,28 @@
v-model:file="defaultPauseImage"
v-model:file-deleted="defaultPauseImageDeleted"
:disabled="disabled || isLoadingAction"
:max-file-size="5000000"
:max-file-size="5_000_000"
:hide-url="true"
show-delete
:preview-alt="$t('rooms.streaming.config.pause_image_alt')"
:allowed-extensions="['jpg', 'jpeg', 'png', 'gif', 'svg']"
:allowed-extensions="[
'jpg',
'jpeg',
'png',
'gif',
'svg',
'webp',
'bmp',
]"
input-id="pause-image"
:url-invalid="formErrors.fieldInvalid('default_pause_image')"
:file-invalid="formErrors.fieldInvalid('default_pause_image')"
:url-error="formErrors.fieldError('default_pause_image')"
:file-error="formErrors.fieldError('default_pause_image')"
:url-errors="formErrors.fieldError('default_pause_image')"
:file-errors="formErrors.fieldError('default_pause_image')"
/>
<small>{{ $t("rooms.streaming.config.pause_image_format") }}</small>
<small class="block">{{
$t("rooms.streaming.config.pause_image_resolution")
}}</small>
</div>
</fieldset>
</Form>
Expand Down
5 changes: 2 additions & 3 deletions resources/js/components/FormError.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import { isElementInViewport } from "../utils/viewport";

const props = defineProps({
errors: {
type: [Object, null],
type: [Array, null],
required: true,
default: null,
},
});

const formError = ref(null);

const hasError = computed(() => {
return props.errors != null && Object.keys(props.errors).length > 0;
return props.errors != null && props.errors.length > 0;
});

async function scrollToFirstError() {
Expand Down
30 changes: 16 additions & 14 deletions resources/js/components/LoginTabLdap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,13 @@
autocomplete="username"
:placeholder="props.usernameLabel"
aria-describedby="username-help-block"
:invalid="
props.errors !== null &&
props.errors.username &&
props.errors.username.length > 0
"
:invalid="formErrors.fieldInvalid('username')"
required
/>
<small id="username-help-block">{{
$t("auth.ldap.username_help")
}}</small>
<FormError :errors="props.errors?.username" />
<FormError :errors="formErrors.fieldError('username')" />
</div>

<div class="field mt-6 flex flex-col gap-2" data-test="password-field">
Expand All @@ -37,13 +33,9 @@
fluid
:disabled="props.loading"
:placeholder="props.passwordLabel"
:invalid="
props.errors !== null &&
props.errors.password &&
props.errors.password.length > 0
"
:invalid="formErrors.fieldInvalid('password')"
/>
<FormError :errors="props.errors?.password" />
<FormError :errors="formErrors.fieldError('password')" />
</div>
<Button
type="submit"
Expand All @@ -58,15 +50,17 @@
</template>

<script setup>
import { ref } from "vue";
import { ref, toRaw, watch } from "vue";
import FormError from "./FormError.vue";
import { useFormErrors } from "../composables/useFormErrors.js";

const formErrors = useFormErrors();

const emit = defineEmits(["submit"]);
const props = defineProps({
errors: {
type: [Object, null],
required: true,
default: null,
},
id: {
type: String,
Expand Down Expand Up @@ -96,6 +90,14 @@ const props = defineProps({
const username = ref("");
const password = ref("");

watch(
() => props.errors,
(newErrors) => {
formErrors.set(toRaw(newErrors));
},
{ deep: true, immediate: true },
);

function submit() {
emit("submit", {
id: props.id,
Expand Down
Loading
Loading