🔥 Remove redundant PRD.md file, update media models with stricter MIME type validation, and refine media conversion settings.

This commit is contained in:
HolgerHatGarKeineNode
2026-01-25 23:54:44 +01:00
parent 2f4113a3f3
commit 32e327cd9c
11 changed files with 142 additions and 583 deletions

460
PRD.md
View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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(),
);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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'));
}

View File

@@ -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