diff --git a/PRD.md b/PRD.md deleted file mode 100644 index 99e1630..0000000 --- a/PRD.md +++ /dev/null @@ -1,460 +0,0 @@ -# PRD: Sicherheitshärtung der Datei-Uploads - -## Projektübersicht - -**Ziel:** Alle Datei-Uploads in der Anwendung absichern durch: -1. Migration aller Medien vom öffentlichen zum privaten Storage -2. Implementierung von Signed URLs für alle Medienzugriffe -3. Strikte MIME-Type Validierung für alle Uploads -4. Absicherung des `public/storage` Ordners - -**Risikobewertung:** KRITISCH - Derzeit sind alle hochgeladenen Dateien ohne Authentifizierung öffentlich zugänglich. - ---- - -## Aktuelle Sicherheitslücken - -### Kritische Probleme -- [ ] Alle Medien werden auf dem `public` Disk gespeichert und sind über `/storage/` direkt zugänglich -- [ ] Keine Autorisierungsprüfung für Medienzugriffe -- [ ] API-Endpunkte geben ungeschützte Media-URLs zurück -- [ ] Keine strikte MIME-Type Validierung beim Upload - -### Betroffene Modelle (10) -| Modell | Collections | Conversions | -|--------|-------------|-------------| -| Meetup | logo | preview, thumb | -| Course | logo, images | preview, thumb | -| Lecturer | avatar, images | preview, thumb | -| SelfHostedService | logo | preview, thumb | -| BitcoinEvent | logo | preview, thumb | -| BookCase | images | preview, seo, thumb | -| OrangePill | images | preview, thumb | -| Venue | images | preview, thumb | -| ProjectProposal | main | preview, thumb | -| LibraryItem | main, single_file, images | preview, seo, thumb | - ---- - -## Milestone 1: Vorbereitung & Konfiguration - -### 1.1 Media Library Konfiguration publizieren -- [ ] `php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config"` -- [ ] Konfigurationsdatei `config/media-library.php` überprüfen - -### 1.2 Storage Konfiguration anpassen -- [ ] Neuen privaten Disk in `config/filesystems.php` erstellen: - ```php - 'media' => [ - 'driver' => 'local', - 'root' => storage_path('app/media'), - 'visibility' => 'private', - ], - ``` -- [ ] `.env` Variable hinzufügen: `MEDIA_DISK=media` - -### 1.3 Media Library Konfiguration aktualisieren -- [ ] Default Disk auf `media` setzen -- [ ] Temporary URL Expiration auf 5 Minuten setzen -- [ ] Path Generator konfigurieren (falls custom benötigt) - -### 1.4 Backup erstellen -- [ ] Vollständiges Backup des aktuellen `storage/app/public` Ordners -- [ ] Datenbank-Backup der `media` Tabelle - ---- - -## Milestone 2: Upload-Härtung implementieren - -### 2.1 Custom Upload-Validator erstellen -- [ ] `app/Rules/SecureImageUpload.php` erstellen: - ```php - // Prüft: - // - Erlaubte MIME-Types (image/jpeg, image/png, image/gif, image/webp) - // - Tatsächlicher Dateiinhalt (Magic Bytes) - // - Keine doppelten Erweiterungen (.php.jpg) - // - Keine eingebetteten PHP-Tags - ``` - -### 2.2 Erlaubte MIME-Types definieren -- [ ] Für Bilder: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/svg+xml` -- [ ] Für Dokumente (LibraryItem): `application/pdf`, `application/zip` -- [ ] SVG-Upload deaktivieren oder mit Sanitizer versehen (XSS-Risiko) - -### 2.3 Livewire Upload-Komponenten härten - -#### Lecturers Create (`app/Livewire/Lecturers/Create.php`) -- [ ] Validierung auf SecureImageUpload Rule aktualisieren -- [ ] MIME-Type Whitelist hinzufügen -- [ ] Disk auf `media` umstellen - -#### Lecturers Edit (`app/Livewire/Lecturers/Edit.php`) -- [ ] Validierung auf SecureImageUpload Rule aktualisieren -- [ ] MIME-Type Whitelist hinzufügen -- [ ] Disk auf `media` umstellen - -#### Courses Create (`app/Livewire/Courses/Create.php`) -- [ ] Validierung auf SecureImageUpload Rule aktualisieren -- [ ] MIME-Type Whitelist hinzufügen -- [ ] Disk auf `media` umstellen - -#### Courses Edit (`app/Livewire/Courses/Edit.php`) -- [ ] Validierung auf SecureImageUpload Rule aktualisieren -- [ ] MIME-Type Whitelist hinzufügen -- [ ] Disk auf `media` umstellen - -#### Meetups Create (`app/Livewire/Meetups/Create.php`) -- [ ] Validierung auf SecureImageUpload Rule aktualisieren -- [ ] MIME-Type Whitelist hinzufügen -- [ ] Disk auf `media` umstellen - -#### Meetups Edit (`app/Livewire/Meetups/Edit.php`) -- [ ] Validierung auf SecureImageUpload Rule aktualisieren -- [ ] MIME-Type Whitelist hinzufügen -- [ ] Disk auf `media` umstellen - -### 2.4 Weitere Upload-Stellen prüfen und härten -- [ ] Services Create/Edit prüfen (derzeit keine Uploads) -- [ ] BitcoinEvent Upload-Stellen identifizieren und härten -- [ ] BookCase Upload-Stellen identifizieren und härten -- [ ] OrangePill Upload-Stellen identifizieren und härten -- [ ] Venue Upload-Stellen identifizieren und härten -- [ ] ProjectProposal Upload-Stellen identifizieren und härten -- [ ] LibraryItem Upload-Stellen identifizieren und härten - ---- - -## Milestone 3: Modelle aktualisieren - -### 3.1 Meetup Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] `logoSquare` Accessor auf Signed URL umstellen -- [ ] Tests schreiben - -### 3.2 Course Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] Tests schreiben - -### 3.3 Lecturer Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] Tests schreiben - -### 3.4 SelfHostedService Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] Tests schreiben - -### 3.5 BitcoinEvent Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] Tests schreiben - -### 3.6 BookCase Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] Tests schreiben - -### 3.7 OrangePill Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] Tests schreiben - -### 3.8 Venue Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] Tests schreiben - -### 3.9 ProjectProposal Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] Tests schreiben - -### 3.10 LibraryItem Model -- [ ] `registerMediaCollections()` aktualisieren - Disk auf `media` setzen -- [ ] `toFeedItem()` Methode auf Signed URLs umstellen -- [ ] MIME-Type Validierung für single_file Collection verschärfen -- [ ] Tests schreiben - ---- - -## Milestone 4: Signed URL Service implementieren - -### 4.1 Media URL Helper erstellen -- [ ] `app/Services/MediaUrlService.php` erstellen: - ```php - class MediaUrlService - { - public function getSignedUrl( - Media $media, - string $conversion = '', - int $expirationMinutes = 60 - ): string; - - public function getSignedMediaUrl( - Model $model, - string $collection, - string $conversion = '' - ): ?string; - } - ``` - -### 4.2 Blade Directive erstellen -- [ ] Custom Blade Directive `@mediaUrl($model, 'collection', 'conversion')` erstellen -- [ ] In `AppServiceProvider` registrieren - -### 4.3 API Response Trait erstellen -- [ ] `app/Traits/HasSignedMediaUrls.php` erstellen für konsistente API-Responses - ---- - -## Milestone 5: Controller aktualisieren - -### 5.1 ImageController überarbeiten -- [ ] Authentifizierung/Autorisierung hinzufügen -- [ ] Signed URL Validierung implementieren -- [ ] Rate Limiting hinzufügen (z.B. 100 Anfragen/Minute) -- [ ] Nur erlaubte Pfade bedienen -- [ ] Cache-Header für private Inhalte anpassen - -### 5.2 API MeetupController -- [ ] `getFirstMediaUrl()` durch Signed URL ersetzen (Zeile 51) -- [ ] Tests aktualisieren - -### 5.3 API LecturerController -- [ ] `getFirstMediaUrl()` durch Signed URL ersetzen (Zeile 37) -- [ ] Tests aktualisieren - -### 5.4 API CourseController -- [ ] `getFirstMediaUrl()` durch Signed URL ersetzen (Zeile 36) -- [ ] Tests aktualisieren - -### 5.5 routes/api.php aktualisieren -- [ ] Zeile 55: `/api/bindles` - Signed URL für main collection -- [ ] Zeile 88: `/api/meetups` - Signed URL für logos -- [ ] Zeile 131: `/api/meetup-events/{date}` - Signed URL für logos - ---- - -## Milestone 6: Views aktualisieren - -### 6.1 Lecturer Views -- [ ] `resources/views/livewire/lecturers/index.blade.php` (Zeile 88) -- [ ] `resources/views/livewire/lecturers/edit.blade.php` (Zeile 119) - -### 6.2 Course Views -- [ ] `resources/views/livewire/courses/index.blade.php` -- [ ] `resources/views/livewire/courses/edit.blade.php` (Zeile 101) -- [ ] `resources/views/livewire/courses/landingpage.blade.php` - -### 6.3 Meetup Views -- [ ] `resources/views/livewire/meetups/index.blade.php` (Zeile 88) -- [ ] `resources/views/livewire/meetups/edit.blade.php` (Zeile 119) -- [ ] `resources/views/livewire/meetups/landingpage.blade.php` -- [ ] `resources/views/livewire/meetups/landingpage-event.blade.php` - -### 6.4 Dashboard Views -- [ ] `resources/views/livewire/dashboard/top-meetups.blade.php` -- [ ] `resources/views/livewire/dashboard/activities.blade.php` - -### 6.5 Weitere Views durchsuchen -- [ ] Alle verbleibenden `getFirstMediaUrl()` Aufrufe in Blade-Templates finden -- [ ] Alle auf Signed URLs umstellen - ---- - -## Milestone 7: Datenmigration - -### 7.1 Migration Command erstellen -- [ ] `app/Console/Commands/MigrateMediaToPrivateStorage.php` erstellen: - ```php - // - Alle Medien vom public zum media Disk verschieben - // - media Tabelle aktualisieren (disk Spalte) - // - Conversions neu generieren - // - Fortschrittsanzeige - // - Dry-run Option - ``` - -### 7.2 Migration durchführen -- [ ] Dry-run ausführen und Ergebnis prüfen -- [ ] Tatsächliche Migration durchführen -- [ ] Verifizieren, dass alle Dateien verschoben wurden -- [ ] Verifizieren, dass Conversions funktionieren - -### 7.3 Alte Dateien aufräumen -- [ ] Prüfen ob noch Dateien in `storage/app/public` liegen -- [ ] Alte Dateien löschen (nach Bestätigung) -- [ ] Symlink `public/storage` entfernen oder umbenennen - ---- - -## Milestone 8: public/storage absichern - -### 8.1 Symlink-Analyse -- [ ] Prüfen welche Dateien noch über Symlink erreichbar sein müssen -- [ ] Liste der Legacy-Dateien erstellen - -### 8.2 Symlink-Strategie festlegen -**Option A:** Symlink komplett entfernen -- [ ] Alle Referenzen auf `/storage/` URLs finden und ersetzen -- [ ] Symlink löschen - -**Option B:** Symlink auf leeren/kontrollierten Ordner zeigen lassen -- [ ] Neuen leeren Ordner erstellen -- [ ] Symlink umbiegen -- [ ] 403/404 für unbekannte Pfade - -### 8.3 .htaccess / nginx Regeln -- [ ] Falls Apache: `.htaccess` in `public/storage` mit Deny-Regeln -- [ ] Falls Nginx: Location-Block mit deny all - -### 8.4 Laravel Route für Legacy-URLs -- [ ] Catch-all Route für `/storage/*` erstellen -- [ ] 403 Forbidden oder Redirect zu Signed URL - ---- - -## Milestone 9: Tests - -### 9.1 Unit Tests -- [ ] `tests/Unit/Rules/SecureImageUploadTest.php` -- [ ] `tests/Unit/Services/MediaUrlServiceTest.php` - -### 9.2 Feature Tests - Upload Sicherheit -- [ ] Test: PHP-Datei als Bild getarnt wird abgelehnt -- [ ] Test: Doppelte Erweiterungen werden abgelehnt -- [ ] Test: Nur erlaubte MIME-Types werden akzeptiert -- [ ] Test: Zu große Dateien werden abgelehnt - -### 9.3 Feature Tests - Signed URLs -- [ ] Test: Abgelaufene Signed URL wird abgelehnt -- [ ] Test: Manipulierte Signed URL wird abgelehnt -- [ ] Test: Gültige Signed URL liefert Datei - -### 9.4 Feature Tests - Storage Zugriff -- [ ] Test: Direkter Zugriff auf `/storage/` gibt 403/404 -- [ ] Test: Medien im private Storage nicht öffentlich zugänglich -- [ ] Test: Nur authentifizierte Benutzer können eigene Medien sehen (falls relevant) - -### 9.5 Browser Tests -- [ ] Test: Bild-Upload funktioniert korrekt -- [ ] Test: Bilder werden in Views korrekt angezeigt -- [ ] Test: API gibt korrekte Signed URLs zurück - ---- - -## Milestone 10: Dokumentation & Cleanup - -### 10.1 Entwickler-Dokumentation -- [ ] Neue Upload-Richtlinien dokumentieren -- [ ] Signed URL Nutzung dokumentieren -- [ ] Migration-Prozess dokumentieren - -### 10.2 Code Cleanup -- [ ] Unbenutzte alte Upload-Logik entfernen -- [ ] Deprecation Warnings für alte Methoden hinzufügen (falls noch verwendet) -- [ ] Laravel Pint laufen lassen - -### 10.3 Deployment Checkliste -- [ ] Migration Command in Deployment-Skript aufnehmen -- [ ] Storage-Berechtigungen auf Server prüfen -- [ ] Cache leeren nach Deployment -- [ ] Conversions neu generieren falls nötig - ---- - -## Abnahmekriterien - -### Sicherheit -- [ ] Kein direkter öffentlicher Zugriff auf hochgeladene Dateien möglich -- [ ] Alle Medienzugriffe laufen über Signed URLs mit Ablaufzeit -- [ ] PHP-Dateien und andere gefährliche Dateitypen können nicht hochgeladen werden -- [ ] MIME-Type wird serverseitig validiert (nicht nur Client-seitig) - -### Funktionalität -- [ ] Alle bestehenden Upload-Funktionen arbeiten weiterhin korrekt -- [ ] Alle Bilder werden in der Anwendung korrekt angezeigt -- [ ] API-Endpunkte liefern funktionierende (Signed) URLs -- [ ] Conversions (thumb, preview, seo) funktionieren weiterhin - -### Performance -- [ ] Signed URL Generierung < 10ms -- [ ] Keine merkbare Verzögerung beim Laden von Bildern -- [ ] Caching funktioniert weiterhin effektiv - ---- - -## Technische Details - -### Spatie Media Library Signed URLs - -```php -// In Model -public function getSignedLogoUrl(): string -{ - return $this->getFirstMedia('logo') - ?->getTemporaryUrl(now()->addMinutes(60)) - ?? '/images/default-logo.png'; -} -``` - -### Konfiguration für Signed URLs -```php -// config/media-library.php -'temporary_urls' => [ - 'expiration_time_in_seconds' => 60 * 60, // 1 Stunde -], -``` - -### Validierungsregel Beispiel -```php -// app/Rules/SecureImageUpload.php -public function validate(string $attribute, mixed $value, Closure $fail): void -{ - $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - - // Echte MIME-Type Prüfung via finfo - $finfo = new \finfo(FILEINFO_MIME_TYPE); - $mimeType = $finfo->file($value->getRealPath()); - - if (!in_array($mimeType, $allowedMimes)) { - $fail('Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt.'); - } - - // Prüfe auf PHP-Tags im Dateiinhalt - $content = file_get_contents($value->getRealPath()); - if (preg_match('/<\?php|<\?=/i', $content)) { - $fail('Die Datei enthält unerlaubten Code.'); - } -} -``` - ---- - -## Risiken & Mitigationen - -| Risiko | Wahrscheinlichkeit | Auswirkung | Mitigation | -|--------|-------------------|------------|------------| -| Broken Images nach Migration | Mittel | Hoch | Umfassende Tests, Rollback-Plan | -| Performance-Einbußen durch Signed URLs | Niedrig | Mittel | Caching der generierten URLs | -| Inkompatibilität mit externen Services | Mittel | Mittel | API-Versioning, Übergangszeit | -| Datenverlust bei Migration | Niedrig | Kritisch | Vollständiges Backup vor Migration | - ---- - -## Zeitplan (geschätzt) - -| Milestone | Geschätzter Aufwand | -|-----------|---------------------| -| 1. Vorbereitung & Konfiguration | 2-4 Stunden | -| 2. Upload-Härtung | 4-6 Stunden | -| 3. Modelle aktualisieren | 3-4 Stunden | -| 4. Signed URL Service | 2-3 Stunden | -| 5. Controller aktualisieren | 2-3 Stunden | -| 6. Views aktualisieren | 2-3 Stunden | -| 7. Datenmigration | 2-4 Stunden | -| 8. public/storage absichern | 1-2 Stunden | -| 9. Tests | 4-6 Stunden | -| 10. Dokumentation & Cleanup | 1-2 Stunden | -| **Gesamt** | **23-37 Stunden** | - ---- - -## Referenzen - -- [Spatie Media Library Dokumentation](https://spatie.be/docs/laravel-medialibrary) -- [Laravel Storage Dokumentation](https://laravel.com/docs/filesystem) -- [OWASP File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html) diff --git a/app/Models/BitcoinEvent.php b/app/Models/BitcoinEvent.php index cbdbfd2..8e4f6f5 100644 --- a/app/Models/BitcoinEvent.php +++ b/app/Models/BitcoinEvent.php @@ -43,22 +43,23 @@ class BitcoinEvent extends Model implements HasMedia }); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') ->fit(Manipulations::FIT_CROP, 300, 300) ->nonQueued(); $this->addMediaConversion('thumb') - ->fit(Manipulations::FIT_CROP, 130, 130) - ->width(130) - ->height(130); + ->fit(Manipulations::FIT_CROP, 130, 130) + ->width(130) + ->height(130); } public function registerMediaCollections(): void { $this->addMediaCollection('logo') - ->useFallbackUrl(asset('img/einundzwanzig.png')); + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) + ->useFallbackUrl(asset('img/einundzwanzig.png')); } public function createdBy(): BelongsTo diff --git a/app/Models/BookCase.php b/app/Models/BookCase.php index 1fd949a..dc8f319 100644 --- a/app/Models/BookCase.php +++ b/app/Models/BookCase.php @@ -14,32 +14,34 @@ use Spatie\MediaLibrary\MediaCollections\Models\Media; class BookCase extends Model implements HasMedia { + use Geoly; use HasFactory; use InteractsWithMedia; - use Geoly; /** * The attributes that aren't mass assignable. + * * @var array */ protected $guarded = []; /** * The attributes that should be cast to native types. + * * @var array */ protected $casts = [ - 'id' => 'integer', - 'lat' => 'double', - 'lon' => 'array', - 'digital' => 'boolean', + 'id' => 'integer', + 'lat' => 'double', + 'lon' => 'array', + 'digital' => 'boolean', 'deactivated' => 'boolean', ]; protected static function booted() { static::creating(function ($model) { - if (!$model->created_by) { + if (! $model->created_by) { $model->created_by = auth()->id(); } }); @@ -50,25 +52,26 @@ class BookCase extends Model implements HasMedia return $query->where('deactivated', false); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') ->fit(Manipulations::FIT_CROP, 300, 300) ->nonQueued(); $this->addMediaConversion('seo') - ->fit(Manipulations::FIT_CROP, 1200, 630) - ->width(1200) - ->height(630); + ->fit(Manipulations::FIT_CROP, 1200, 630) + ->width(1200) + ->height(630); $this->addMediaConversion('thumb') - ->fit(Manipulations::FIT_CROP, 130, 130) - ->width(130) - ->height(130); + ->fit(Manipulations::FIT_CROP, 130, 130) + ->width(130) + ->height(130); } public function registerMediaCollections(): void { - $this->addMediaCollection('images'); + $this->addMediaCollection('images') + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']); } public function createdBy(): BelongsTo diff --git a/app/Models/Course.php b/app/Models/Course.php index 92030d1..6e0c04d 100644 --- a/app/Models/Course.php +++ b/app/Models/Course.php @@ -16,8 +16,8 @@ use Spatie\Tags\HasTags; class Course extends Model implements HasMedia { use HasFactory; - use InteractsWithMedia; use HasTags; + use InteractsWithMedia; /** * The attributes that aren't mass assignable. @@ -45,25 +45,27 @@ class Course extends Model implements HasMedia }); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') ->fit(Fit::Crop, 300, 300) ->nonQueued(); $this->addMediaConversion('thumb') - ->fit(Fit::Crop, 130, 130) - ->width(130) - ->height(130); + ->fit(Fit::Crop, 130, 130) + ->width(130) + ->height(130); } public function registerMediaCollections(): void { $this->addMediaCollection('logo') - ->singleFile() - ->useFallbackUrl(asset('img/einundzwanzig.png')); + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) + ->singleFile() + ->useFallbackUrl(asset('img/einundzwanzig.png')); $this->addMediaCollection('images') - ->useFallbackUrl(asset('img/einundzwanzig.png')); + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) + ->useFallbackUrl(asset('img/einundzwanzig.png')); } public function createdBy(): BelongsTo diff --git a/app/Models/Lecturer.php b/app/Models/Lecturer.php index d981727..94218ce 100644 --- a/app/Models/Lecturer.php +++ b/app/Models/Lecturer.php @@ -23,47 +23,51 @@ class Lecturer extends Model implements HasMedia /** * The attributes that aren't mass assignable. + * * @var array */ protected $guarded = []; /** * The attributes that should be cast to native types. + * * @var array */ protected $casts = [ - 'id' => 'integer', - 'active' => 'boolean', + 'id' => 'integer', + 'active' => 'boolean', ]; protected static function booted() { static::creating(function ($model) { - if (!$model->created_by) { + if (! $model->created_by) { $model->created_by = auth()->id(); } }); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') ->fit(Fit::Crop, 300, 300) ->nonQueued(); $this->addMediaConversion('thumb') - ->fit(Fit::Crop, 130, 130) - ->width(130) - ->height(130); + ->fit(Fit::Crop, 130, 130) + ->width(130) + ->height(130); } public function registerMediaCollections(): void { $this->addMediaCollection('avatar') - ->singleFile() - ->useFallbackUrl(asset('img/einundzwanzig.png')); + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) + ->singleFile() + ->useFallbackUrl(asset('img/einundzwanzig.png')); $this->addMediaCollection('images') - ->useFallbackUrl(asset('img/einundzwanzig.png')); + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) + ->useFallbackUrl(asset('img/einundzwanzig.png')); } /** @@ -72,9 +76,9 @@ class Lecturer extends Model implements HasMedia public function getSlugOptions(): SlugOptions { return SlugOptions::create() - ->generateSlugsFrom(['name']) - ->saveSlugsTo('slug') - ->usingLanguage(Cookie::get('lang', config('app.locale'))); + ->generateSlugsFrom(['name']) + ->saveSlugsTo('slug') + ->usingLanguage(Cookie::get('lang', config('app.locale'))); } public function createdBy(): BelongsTo diff --git a/app/Models/LibraryItem.php b/app/Models/LibraryItem.php index 621ffb8..447e759 100644 --- a/app/Models/LibraryItem.php +++ b/app/Models/LibraryItem.php @@ -19,47 +19,49 @@ use Spatie\Sluggable\HasSlug; use Spatie\Sluggable\SlugOptions; use Spatie\Tags\HasTags; -class LibraryItem extends Model implements HasMedia, Sortable, Feedable +class LibraryItem extends Model implements Feedable, HasMedia, Sortable { - use InteractsWithMedia; - use HasTags; - use SortableTrait; - use HasStatuses; use HasSlug; + use HasStatuses; + use HasTags; + use InteractsWithMedia; + use SortableTrait; /** * The attributes that aren't mass assignable. + * * @var array */ protected $guarded = []; /** * The attributes that should be cast to native types. + * * @var array */ protected $casts = [ - 'id' => 'integer', + 'id' => 'integer', 'lecturer_id' => 'integer', - 'library_id' => 'integer', + 'library_id' => 'integer', ]; public static function getFeedItems() { return self::query() - ->with([ - 'media', - 'lecturer', - ]) - ->where('news', true) - ->where('approved', true) - ->orderByDesc('created_at') - ->get(); + ->with([ + 'media', + 'lecturer', + ]) + ->where('news', true) + ->where('approved', true) + ->orderByDesc('created_at') + ->get(); } protected static function booted() { static::creating(function ($model) { - if (!$model->created_by) { + if (! $model->created_by) { $model->created_by = auth()->id(); } }); @@ -68,39 +70,41 @@ class LibraryItem extends Model implements HasMedia, Sortable, Feedable public function getSlugOptions(): SlugOptions { return SlugOptions::create() - ->generateSlugsFrom(['name']) - ->saveSlugsTo('slug') - ->usingLanguage(Cookie::get('lang', config('app.locale'))); + ->generateSlugsFrom(['name']) + ->saveSlugsTo('slug') + ->usingLanguage(Cookie::get('lang', config('app.locale'))); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') ->fit(Manipulations::FIT_CROP, 300, 300) ->nonQueued(); $this->addMediaConversion('seo') - ->fit(Manipulations::FIT_CROP, 1200, 630) - ->nonQueued(); + ->fit(Manipulations::FIT_CROP, 1200, 630) + ->nonQueued(); $this->addMediaConversion('thumb') - ->fit(Manipulations::FIT_CROP, 130, 130) - ->width(130) - ->height(130); + ->fit(Manipulations::FIT_CROP, 130, 130) + ->width(130) + ->height(130); } public function registerMediaCollections(): void { $this->addMediaCollection('main') - ->singleFile() - ->useFallbackUrl(asset('img/einundzwanzig.png')); + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) + ->singleFile() + ->useFallbackUrl(asset('img/einundzwanzig.png')); $this->addMediaCollection('single_file') - ->acceptsMimeTypes([ - 'application/pdf', 'application/zip', 'application/octet-stream', 'application/x-zip-compressed', - 'multipart/x-zip', - ]) - ->singleFile(); + ->acceptsMimeTypes([ + 'application/pdf', 'application/zip', 'application/octet-stream', 'application/x-zip-compressed', + 'multipart/x-zip', + ]) + ->singleFile(); $this->addMediaCollection('images') - ->useFallbackUrl(asset('img/einundzwanzig.png')); + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) + ->useFallbackUrl(asset('img/einundzwanzig.png')); } public function createdBy(): BelongsTo @@ -131,17 +135,17 @@ class LibraryItem extends Model implements HasMedia, Sortable, Feedable public function toFeedItem(): CustomFeedItem { return CustomFeedItem::create() - ->id('news/'.$this->slug) - ->title($this->name) - ->content($this->value) - ->enclosure($this->getFirstMediaUrl('main')) - ->enclosureLength($this->getFirstMedia('main')->size) - ->enclosureType($this->getFirstMedia('main')->mime_type) - ->summary($this->excerpt) - ->updated($this->updated_at) - ->image($this->getFirstMediaUrl('main')) - ->link(url()->route('article.view', ['libraryItem' => $this])) - ->authorName($this->lecturer->name); + ->id('news/'.$this->slug) + ->title($this->name) + ->content($this->value) + ->enclosure($this->getFirstMediaUrl('main')) + ->enclosureLength($this->getFirstMedia('main')->size) + ->enclosureType($this->getFirstMedia('main')->mime_type) + ->summary($this->excerpt) + ->updated($this->updated_at) + ->image($this->getFirstMediaUrl('main')) + ->link(url()->route('article.view', ['libraryItem' => $this])) + ->authorName($this->lecturer->name); } public static function searchLibraryItems($type, $value = null) diff --git a/app/Models/Meetup.php b/app/Models/Meetup.php index 84f8101..d0e1059 100644 --- a/app/Models/Meetup.php +++ b/app/Models/Meetup.php @@ -19,8 +19,8 @@ use Spatie\Sluggable\SlugOptions; class Meetup extends Model implements HasMedia { use HasFactory; - use InteractsWithMedia; use HasSlug; + use InteractsWithMedia; /** * The attributes that aren't mass assignable. @@ -44,7 +44,7 @@ class Meetup extends Model implements HasMedia protected static function booted() { static::creating(function ($model) { - if (!$model->created_by) { + if (! $model->created_by) { $model->created_by = auth()->id(); } }); @@ -58,7 +58,7 @@ class Meetup extends Model implements HasMedia ->usingLanguage(Cookie::get('lang', config('app.locale'))); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') @@ -75,6 +75,7 @@ class Meetup extends Model implements HasMedia { $this ->addMediaCollection('logo') + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) ->singleFile() ->useFallbackUrl(get_domain_attributes()['image']); } @@ -104,8 +105,7 @@ class Meetup extends Model implements HasMedia } return Attribute::make( - get: fn() - => url()->route('img', + get: fn () => url()->route('img', [ 'path' => $path, 'w' => 900, @@ -121,8 +121,7 @@ class Meetup extends Model implements HasMedia $nextEvent = $this->meetupEvents()->where('start', '>=', now())->orderBy('start')->first(); return Attribute::make( - get: fn() - => $nextEvent ? [ + get: fn () => $nextEvent ? [ 'id' => $nextEvent->id, 'start' => $nextEvent->start, 'portalLink' => url()->route('meetups.landingpage-event', @@ -140,7 +139,7 @@ class Meetup extends Model implements HasMedia protected function belongsToMe(): Attribute { return Attribute::make( - get: fn() => DB::table('meetup_user')->where('meetup_id', $this->id)->where('user_id', + get: fn () => DB::table('meetup_user')->where('meetup_id', $this->id)->where('user_id', auth()->id())->exists(), ); } diff --git a/app/Models/OrangePill.php b/app/Models/OrangePill.php index 0a4c4b7..a0dd65f 100644 --- a/app/Models/OrangePill.php +++ b/app/Models/OrangePill.php @@ -45,21 +45,22 @@ class OrangePill extends Model implements HasMedia }); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') ->fit(Manipulations::FIT_CROP, 300, 300) ->nonQueued(); $this->addMediaConversion('thumb') - ->fit(Manipulations::FIT_CROP, 130, 130) - ->width(130) - ->height(130); + ->fit(Manipulations::FIT_CROP, 130, 130) + ->width(130) + ->height(130); } public function registerMediaCollections(): void { - $this->addMediaCollection('images'); + $this->addMediaCollection('images') + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']); } public function user(): BelongsTo diff --git a/app/Models/ProjectProposal.php b/app/Models/ProjectProposal.php index 2987e83..50b51d6 100644 --- a/app/Models/ProjectProposal.php +++ b/app/Models/ProjectProposal.php @@ -16,29 +16,31 @@ use Spatie\Sluggable\SlugOptions; class ProjectProposal extends Model implements HasMedia { - use InteractsWithMedia; use HasFactory; use HasSlug; + use InteractsWithMedia; /** * The attributes that aren't mass assignable. + * * @var array */ protected $guarded = []; /** * The attributes that should be cast to native types. + * * @var array */ protected $casts = [ - 'id' => 'integer', + 'id' => 'integer', 'user_id' => 'integer', ]; protected static function booted() { static::creating(function ($model) { - if (!$model->created_by) { + if (! $model->created_by) { $model->created_by = auth()->id(); } }); @@ -47,28 +49,29 @@ class ProjectProposal extends Model implements HasMedia public function getSlugOptions(): SlugOptions { return SlugOptions::create() - ->generateSlugsFrom(['name']) - ->saveSlugsTo('slug') - ->usingLanguage(Cookie::get('lang', config('app.locale'))); + ->generateSlugsFrom(['name']) + ->saveSlugsTo('slug') + ->usingLanguage(Cookie::get('lang', config('app.locale'))); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') ->fit(Manipulations::FIT_CROP, 300, 300) ->nonQueued(); $this->addMediaConversion('thumb') - ->fit(Manipulations::FIT_CROP, 130, 130) - ->width(130) - ->height(130); + ->fit(Manipulations::FIT_CROP, 130, 130) + ->width(130) + ->height(130); } public function registerMediaCollections(): void { $this->addMediaCollection('main') - ->singleFile() - ->useFallbackUrl(asset('img/einundzwanzig.png')); + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) + ->singleFile() + ->useFallbackUrl(asset('img/einundzwanzig.png')); } public function user(): BelongsTo diff --git a/app/Models/SelfHostedService.php b/app/Models/SelfHostedService.php index f174789..193f438 100644 --- a/app/Models/SelfHostedService.php +++ b/app/Models/SelfHostedService.php @@ -18,9 +18,9 @@ use Spatie\Tags\HasTags; class SelfHostedService extends Model implements HasMedia { use HasFactory; - use InteractsWithMedia; use HasSlug; use HasTags; + use InteractsWithMedia; protected $guarded = []; @@ -35,7 +35,7 @@ class SelfHostedService extends Model implements HasMedia { static::creating(function ($model): void { // Only set created_by if user is authenticated and not explicitly set as anonymous - if (auth()->check() && !isset($model->created_by)) { + if (auth()->check() && ! isset($model->created_by)) { $model->created_by = auth()->id(); } }); @@ -49,7 +49,7 @@ class SelfHostedService extends Model implements HasMedia ->usingLanguage(Cookie::get('lang', config('app.locale'))); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') @@ -66,6 +66,7 @@ class SelfHostedService extends Model implements HasMedia { $this ->addMediaCollection('logo') + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) ->singleFile() ->useFallbackUrl(asset('img/einundzwanzig.png')); } diff --git a/app/Models/Venue.php b/app/Models/Venue.php index 2b39bec..a246246 100644 --- a/app/Models/Venue.php +++ b/app/Models/Venue.php @@ -18,8 +18,8 @@ use Staudenmeir\EloquentHasManyDeep\HasRelationships; class Venue extends Model implements HasMedia { use HasFactory; - use HasSlug; use HasRelationships; + use HasSlug; use InteractsWithMedia; /** @@ -48,22 +48,23 @@ class Venue extends Model implements HasMedia }); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaConversions(?Media $media = null): void { $this ->addMediaConversion('preview') ->fit(Manipulations::FIT_CROP, 300, 300) ->nonQueued(); $this->addMediaConversion('thumb') - ->fit(Manipulations::FIT_CROP, 130, 130) - ->width(130) - ->height(130); + ->fit(Manipulations::FIT_CROP, 130, 130) + ->width(130) + ->height(130); } public function registerMediaCollections(): void { $this->addMediaCollection('images') - ->useFallbackUrl(asset('img/einundzwanzig.png')); + ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']) + ->useFallbackUrl(asset('img/einundzwanzig.png')); } /** @@ -72,9 +73,9 @@ class Venue extends Model implements HasMedia public function getSlugOptions(): SlugOptions { return SlugOptions::create() - ->generateSlugsFrom(['city.slug', 'name']) - ->saveSlugsTo('slug') - ->usingLanguage(Cookie::get('lang', config('app.locale'))); + ->generateSlugsFrom(['city.slug', 'name']) + ->saveSlugsTo('slug') + ->usingLanguage(Cookie::get('lang', config('app.locale'))); } public function createdBy(): BelongsTo @@ -89,12 +90,12 @@ class Venue extends Model implements HasMedia public function lecturers() { - return $this->hasManyDeepFromRelations($this->courses(), (new Course())->lecturer()); + return $this->hasManyDeepFromRelations($this->courses(), (new Course)->lecturer()); } public function courses() { - return $this->hasManyDeepFromRelations($this->events(), (new CourseEvent())->course()); + return $this->hasManyDeepFromRelations($this->events(), (new CourseEvent)->course()); } public function courseEvents(): HasMany