fix mailing an email

This commit is contained in:
2026-06-23 08:05:51 +02:00
parent c5127da750
commit def8d22544
2479 changed files with 325393 additions and 781 deletions
+7 -5
View File
@@ -2,10 +2,10 @@ APP_NAME=Glastree
APP_ENV=local
APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao=
APP_DEBUG=true
APP_URL=https://glastree.soon.it
APP_PROTOCOL=https
FORCE_HTTPS=true
TRUSTED_PROXIES=*
APP_URL=http://glastree.soon.it
APP_PROTOCOL=http
FORCE_HTTPS=false
TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
#GOOGLE_CLIENT_ID=980774223097-o5h7a6kepvg69te34fof2otn7ibi9uha.apps.googleusercontent.com
#GOOGLE_CLIENT_SECRET=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
@@ -38,9 +38,11 @@ DB_DATABASE=glastree
DB_USERNAME=glastree
DB_PASSWORD=glastree
SESSION_DRIVER=file
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_SECURE_COOKIE=false
SESSION_SAME_SITE=lax
SESSION_PATH=/
SESSION_DOMAIN=null
+1 -1
View File
@@ -28,7 +28,7 @@ DB_DATABASE=glastree
DB_USERNAME=glastree
DB_PASSWORD=
SESSION_DRIVER=file
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_SECURE_COOKIE=false
+339
View File
@@ -26,6 +26,20 @@ App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
- **2026-06-17**: Google OAuth 2.0 unificato (revert Socialite, unified OAuth per Email/Drive/Calendar, XOAUTH2 SMTP+IMAP, UI impostazioni)
- **2026-06-17**: Fix CalendarioConnessione — `encryptAndSetConfig()` perdeva campi sensibili in edit, `is_active` checkbox senza hidden fallback, query duplicata nella view
- **2026-06-18**: Fix import CSV — `declare(strict_types=1)` mancante in IndividuoController, header check assente, Log::warning() su errori; fix freeze server (set_time_limit, session_write_close, DB::transaction, fgetcsv length illimitato) in GruppoController e IndividuoController
- **2026-06-23**: Email compose: firma spostata dopo il corpo; Mailing list: aggiunto `sender_account_id` (mittente predefinito salvato in creazione, usato all'invio come fallback)
- **2026-06-23**: Fix formati nome: responsabili gruppi e individui email ora mostrano "Nome Cognome"; mailing list edit: aggiunto pulsante "Rimuovi selezionati"
- **2026-06-22**: Aggiunto supporto Email Mittente Ufficiale (from_email/from_name) per invio con indirizzo diverso dalle credenziali SMTP/IMAP
- Migration: `from_email`, `from_name` columns su `email_settings`
- `EmailSetting`: metodi `getEffectiveFromAddress()`, `getEffectiveFromName()`, `getEffectiveReplyTo()`
- Tutti i metodi di invio (`sendViaImap`, `sendViaSystem`, `sendPasswordResetNotification`, `storeDraft`, `storeSentMessage`, `testSmtp`) ora usano `getEffectiveFromAddress()`/`getEffectiveFromName()` per il mittente
- Reply-To automatico impostato su `from_email` quando disponibile
- UI: nuovi campi "Email Mittente Ufficiale" e "Nome Mittente Ufficiale" in entrambe le pagine impostazioni email
- **2026-06-22**: Backup system enhancements:
- `BackupService::run()` ora accetta `$options` (include_files, include_env) per override temporanei senza persistere su DB
- `BackupRunCommand --no-files/--no-env` non salva più permanentemente la configurazione (era un bug)
- `manifest.json` ora include `included_components` array (database, env, files)
- Vista backup: badge .env (grigio) e Files (giallo) nella colonna "Contenuto" basati sul manifest
- `run()` restituisce `steps` array; mostrato in flash message (controller) e output (command)
- **2026-06-18**: Diagnose script (`diagnose.php`), .env.example aggiornato (SESSION_SECURE_COOKIE, FORCE_HTTPS, TRUSTED_PROXIES), build-dist.sh aggiornato con passo diagnose
## Funzionalità Implementate
@@ -751,3 +765,328 @@ Aggiunte variabili mancanti:
### Verifica
- `php -l diagnose.php`: OK
- `php diagnose.php`: eseguito con 58/68 check superati su server locale
## 2026-06-19 — Fix 419: Duplicated middleware + session env vars + diagnose.php fixes
### Problema
`POST /login` restituiva **419 Page Expired** su HTTP. La causa era doppia:
1. **Critico — Middleware duplicato in `bootstrap/app.php`**: `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession`, `ShareErrorsFromSession` erano appesi al web group via `$middleware->web(append: [...])`. Questi middleware sono **già attivi di default** in Laravel 13. Appenderli una seconda volta causava doppia crittografia/decrittografia dei cookie di sessione → session ID corrotto → token CSRF non riconosciuto → 419.
2. **`SESSION_SECURE_COOKIE` non impostato** nel `.env` — con `APP_URL=https` + `FORCE_HTTPS=true`, Laravel impostava il flag `Secure` sul cookie di sessione. Poiché il server serviva solo HTTP, il browser non inviava il cookie → session persa → 419.
### Fix apportati
**`bootstrap/app.php`** (righe 22-27):
- Rimosso l'intero blocco `$middleware->web(append: [...])` che duplicava EncryptCookies, AddQueuedCookiesToResponse, StartSession, ShareErrorsFromSession.
- Sostituito con commento esplicativo per prevenire future reintroduzioni.
**`.env`**:
- Aggiunto `SESSION_SECURE_COOKIE=false` — impedisce il flag Secure su HTTP
- Aggiunto `SESSION_SAME_SITE=lax` — esplicito per consistenza cross-protocol
- Cambiato `APP_URL` da HTTPS a HTTP (ambiente locale senza HTTPS)
- Cambiato `APP_PROTOCOL` da `https` a `http`
- Cambiato `FORCE_HTTPS` da `true` a `false`
- Cambiato `TRUSTED_PROXIES` da `*` a CIDR espliciti
**`diagnose.php`** — 4 bug fix:
1. **`unlink()` senza `file_exists()`** nelle linee 863 e 900: causava PHP Warning quando curl non creava il cookie jar (es. HTTPS non raggiungibile). Aggiunto `file_exists($cookieJar) && unlink($cookieJar)`.
2. **Regex middleware duplicato non matchava array multi-riga**: il pattern `[^\]]*` non matcha newline. Sostituito con `(.+?)` con flag `s`. Classe name extraction semplificata a `([a-zA-Z]+)::class`.
3. **`CURLOPT_COOKIEJAR` non funzionante** in questo ambiente (curl 8.14.1 + PHP 8.4 — il file non viene creato da `curl_close()`). Riscritto `testCsrfLogin()` per parsare manualmente gli header `Set-Cookie` dalla prima risposta e reinviarli via `CURLOPT_COOKIE` nella POST. Eliminata dipendenza da `COOKIEJAR`/`COOKIEFILE`.
4. Aggiunto verbose debug per codici HTTP POST anomali (≠ 302/200).
### Risultato finale
```
✅ Passed: 63
⚠️ Warnings: 8 (upload/post_max_size, engine opzionali mancanti)
❌ Failed: 1 (spazio disco 94.6% — server admin)
```
- Test CSRF: ✅ POST login → HTTP 302 (nessun 419)
- Solo il disco (94.6%) è critico — non risolvibile via script.
### File modificati
- `bootstrap/app.php` — rimosso middleware web duplicato
- `.env` — aggiunte SESSION_SECURE_COOKIE, SESSION_SAME_SITE; APP_URL/APP_PROTOCOL/FORCE_HTTPS adeguati
- `diagnose.php` — fix unlink warning, regex middleware multi-line, CSRF test manual-cookie, verbose debug
## 2026-06-19 — diagnose.php: storage false positivo + ForceHttps registration check
### Fix 1 — Storage permission false positive
**Problema**: `diagnose.php` segnava 15 storage directory come ❌ NON SCRIVIBILE quando `php diagnose.php` veniva eseguito da CLI (utente `opencode`). Le directory sono di proprietà `www-data:www-data` con permessi `775`, quindi il web server **può** scrivere — il falso positivo generava confusione.
**Diagnosi**:
- CLI esegue come `opencode``is_writable()` controlla i permessi "other" (5 = r-x) → `false`
- Web server esegue come `www-data` → owner `rwx``true`
- Questo è normale su server condivisi/Debian standard
**Fix** (`diagnose.php:659-660`):
- Aggiunto `elseif ($owner === WEB_USER && in_array($perms, ['775', '777', '755', '770']))`
- Se il proprietario è `www-data` e permessi sono `775`/`777`/`755`/`770`, mostra ⚠️ **warning** invece di ❌ **fail**, con messaggio "NON scrivibile da CLI ma WEB server sì"
- Altrimenti mostra fail come prima
### Fix 2 — ForceHttps registration check
**Problema**: Il controllo ForceHttps usava `str_contains($appContent, 'ForceHttps')` che matchava anche il semplice `use App\Http\Middleware\ForceHttps` (import), segnando ✅ "registrato" anche quando il middleware non era attivo in alcun gruppo.
**Stato attuale**: `ForceHttps::class` è **importato** ma **non registrato** in nessun middleware group (`$middleware->append()`, `->web()`, ecc.) in `bootstrap/app.php`.
**Fix** (`diagnose.php:871-879`):
- Sostituito con `preg_match('/\$middleware\s*->\s*\w+\s*\(.*?ForceHttps::class/s', $appContent)`
- 3 stati:
1.`"registrato correttamente"` — se ForceHttps::class è argomento di una chiamata `$middleware->xxx()`
2. ⚠️ `"IMPORTATO ma NON registrato"` — se la stringa ForceHttps esiste (use import) ma non in una chiamata
3. ⚠️ `"NON presente"` — se ForceHttps non esiste nel file
### Verifica
- `php -l diagnose.php`: No syntax errors detected
- `php diagnose.php` su locale: ForceHttps ora segnala ⚠️ "IMPORTATO ma NON registrato" (corretto)
### File modificati
- `diagnose.php` — storage false positive + ForceHttps registration check
## 2026-06-22 — build-dist.sh: genera post-deploy.sh per upgrade/install automatico
**Obiettivo**: Includere nell'archivio di distribuzione uno script `post-deploy.sh` che esegue controllo e setup dell'installazione sul server target, sia per fresh install che per upgrade.
### Cosa è stato fatto
**`build-dist.sh`** riscritto:
- Prima del `tar`, genera `post-deploy.sh` (heredoc) e lo include nell'archivio
- Dopo il `tar`, `post-deploy.sh` viene rimosso localmente (non sporca la working dir)
- Istruzioni finali aggiornate: mostrano i tre comandi principali
**`post-deploy.sh`** (generato, non versionato):
Tre modalità d'uso:
| Comando | Cosa fa |
|---------|---------|
| `bash post-deploy.sh check` | Diagnostica stato installazione: migration pendenti, permessi directory, symlink, cache. Nessuna modifica. |
| `bash post-deploy.sh upgrade` | Check + upgrade esistente: crea directory, permessi, symlink, `migrate --force`, `key:generate` (se mancante), cache clear, verifica finale. Chiede conferma prima di eseguire. |
| `bash post-deploy.sh install` | Fresh install completo: come upgrade + copia `.env.example → .env` con pausa per modifica utente. |
| `bash post-deploy.sh upgrade --yes` | Batch mode (nessuna conferma richiesta) |
Fasi upgrade (8 step): directory → .gitignore → permessi → symlink → migrazioni → key → cache → diagnose
Fasi install (9 step): come upgrade + setup .env con pausa interattiva
### Vantaggi
- **Idempotente**: `migrate --force` esegue solo migration pendenti
- **Sicuro**: modalità `check` non modifica nulla
- **Interattivo**: chiede conferma prima di azioni distruttive
- **Batch**: `--yes` per automazione CI/CD
- **Compatibile**: integra `diagnose.php` già esistente (check + fix)
- **Autopulente**: `post-deploy.sh` non resta nella working dir di build
### Verifica
- `bash -n build-dist.sh`: syntax OK
- `bash -n post-deploy.sh`: syntax OK
- `bash post-deploy.sh check`: eseguito correttamente
- `tar tzf glastree-*.tar.gz | grep post-deploy`: presente nell'archivio
- `rm -f post-deploy.sh` dopo tar: pulizia OK
## 2026-06-22 — Estensione Gruppi: contatti propri + diocesi multiple
**Obiettivo**: Aggiungere contatti (email, telefono) ai gruppi e supportare assegnazione di più diocesi.
### Cosa è stato fatto
**Migration** (2 nuove tabelle):
- `2026_06_22_000002_create_gruppo_contatti_table.php``gruppo_contatti` con: id, gruppo_id (FK cascade), tipo, valore, etichetta, is_primary, timestamps
- `2026_06_22_000003_create_diocesi_gruppo_table.php` — pivot `diocesi_gruppo` con: id, gruppo_id (FK cascade), diocesi_id (FK cascade), unique(gruppo_id, diocesi_id)
**Model `GruppoContatto.php`** (nuovo):
- `$table = 'gruppo_contatti'`, fillable: gruppo_id, tipo, valore, etichetta, is_primary
- `gruppo()`: BelongsTo Gruppo
**Model `Gruppo.php`**:
- `diocesi()`: cambiata da `BelongsTo` a `BelongsToMany` via `diocesi_gruppo`
- `gruppoContatti()`: nuova `HasMany` relation
- Accessor `getEmailPrimariaAttribute()`: primo contatto email (is_primary preferito)
- Accessor `getTelefonoPrimarioAttribute()`: primo contatto telefono/cellulare (is_primary preferito)
- `$appends`: già presente `email_primaria`, `telefono_primario` (invariato)
- `$fillable`: `diocesi_id` mantenuto (nullable) per backward compat con import CSV
**Model `Diocesi.php`**:
- `gruppi()`: cambiata da `HasMany` a `BelongsToMany` via `diocesi_gruppo`
**`GruppoController.php`**:
- `store()`/`update()`: validazione cambiata da `diocesi_id` (nullable|exists) a `diocesi_ids` (nullable|array|exists)
- `store()`/`update()`: nuova validazione `contatti` array con tipo (in:email,telefono,cellulare), valore, etichetta, is_primary
- `store()`: crea contatti dopo create; sync diocesi
- `update()`: cancella+ricrea contatti se presenti; sync diocesi (o detach se assenti)
- `edit()`: eager-load `gruppoContatti`, passa `$selectedDiocesiIds`
- `show()`: eager-load `gruppoContatti`
- `index()`: eager-load `gruppoContatti`
- `create()`: invariato (passa già `$diocesi`)
**`ReportController.php`**:
- 4 occorrenze `$g->diocesi?->nome` → collection pattern `$g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-'`
**View `gruppi/create.blade.php`**:
- Select diocesi: cambiato da single `select name="diocesi_id"` a multiple `select name="diocesi_ids[]" class="select2-multi"`
- Aggiunta card "Contatti del Gruppo" con tabella inline (tipo select, valore input, etichetta, checkbox primario, elimina)
- Select2 CSS/JS caricati via CDN
**View `gruppi/edit.blade.php`**:
- Stessa modifica diocesi → Select2 multi con `$selectedDiocesiIds` pre-selezionati
- Aggiunta card "Contatti del Gruppo" con righe pre-popolate da `$gruppo->gruppoContatti`
- Select2 CSS/JS caricati via CDN
**View `gruppi/show.blade.php`**:
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
- Aggiunte righe Email/Telefono (da accessor) e "Altri Contatti" nella card info
**View `gruppi/index.blade.php`**:
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
- Telefono/Email: da hardcoded `-` a `$gruppo->telefono_primario` / `$gruppo->email_primaria`
**View `gruppi/partials/tree-item.blade.php`**:
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
**View `individui/show.blade.php` + `individui/edit.blade.php`**:
- Diocesi nei gruppi dell'individuo: da `$gruppo->diocesi?->nome` a collection implode
### Backward compatibility
- Colonna `diocesi_id` su `gruppi` mantenuta (nullable) per import CSV
- CSV import (`importStore()`) usa ancora `diocesi_id` → singola diocesi
- `$fillable` include ancora `diocesi_id`
- Nessuna modifica a `Individuo` o `Contatto` esistenti
### Verifica
- `php -l` su tutti i file modificati: OK
- Migrations eseguite: ✅
- Relazioni verificate: `diocesi()` BelongsToMany, `gruppoContatti()` HasMany, `email_primaria`/`telefono_primario` accessors funzionanti
## 2026-06-22 — Fix produzione: 500 pagina gruppi (migration pending + autoloader stale)
**Problema**: Dopo deploy su server produzione (`192.168.222.177`):
- Migration `diocesi_gruppo` non eseguita (PENDING)
- `GruppoContatto.php` non nell'autoloader (`composer dump-autoload` non eseguito)
- `storage/logs/laravel.log` non scrivibile da www-data
- 3 migration pre-esistenti non marcate come "ran" (google_oauth_connections, add_auth_method, add_google_id) — tabelle/colonne già esistenti ma migration record mancanti → bloccavano `migrate --force`
**Fix**:
1. `sudo chmod -R 775 storage bootstrap/cache` — permessi
2. `sudo usermod -a -G www-data opencode` — utente CLI nel gruppo www-data
3. Inseriti manualmente 3 migration record mancanti in `migrations` table (già eseguite in passato ma mai registrate)
4. `php artisan migrate --force` — eseguita `2026_06_22_000003_create_diocesi_gruppo_table`
5. `composer dump-autoload` — autoloader rigenerato (41464 classi)
6. `php artisan view:clear && php artisan route:clear && php artisan config:clear`
**Risultato**:
- `class_exists(App\Models\GruppoContatto)` → ✅
- `gruppi` page HTTP 200 (after login redirect 302) — 500 risolto
- Tutte le migration marked as Ran (batch 40-42)
- Views/routes/config cache pulite
**Lezione**: `build-dist.sh` fa `composer install --no-scripts` che non rigenera autoloader ottimizzato per il target. `post-deploy.sh upgrade` dovrebbe includere `composer dump-autoload` (o almeno `composer install --no-dev --optimize-autoloader`).
## 2026-06-22 — Fix: Select2 search field visibile sotto le diocesi selezionate
**Problema**: In create/edit gruppi, il `<textarea class="select2-search__field">` creato da Select2 in modalità multiple era visibile sotto i tag delle diocesi selezionate, con altezza 65px (ereditata da Bootstrap form-control).
**Causa**:
1. `allowClear: true` in Select2 multiple mode non serve (ogni tag ha già la X per rimuoverlo) e causa conflitti di rendering
2. Bootstrap 4 applica `height: 65px` al textarea del search field
3. Nessun CSS specifico per normalizzare il search field inline
**Fix**:
1. Rimosso `allowClear: true` da entrambi i file (`create.blade.php`, `edit.blade.php`)
2. Aggiunto CSS per normalizzare `height: 28px`, `border: none`, `background: transparent`, `width: auto` con `min-width: 30px`
**File modificati**:
- `resources/views/gruppi/create.blade.php` — rimosso allowClear, aggiunto CSS search field
- `resources/views/gruppi/edit.blade.php` — rimosso allowClear, aggiunto CSS search field
## 2026-06-23 — Fix: Select2 counter rimosso (utente vuole vedere tutti i nomi)
**Problema**: Il `templateSelection` con counter mostrava "5 diocesi selezionate" invece dei nomi.
**Fix**: Rimosso l'intero blocco `templateSelection` da entrambi create/edit. Ogni diocesi selezionata mostra ora il proprio nome.
**File modificati**:
- `resources/views/gruppi/create.blade.php` — rimosso templateSelection
- `resources/views/gruppi/edit.blade.php` — rimosso templateSelection
## 2026-06-23 — Fix: DiocesiSeeder + build-dist.sh per deploy
**Problema**: La tabella `diocesi` con 225 record importati da ODS non veniva popolata sul server target dopo deploy. Il seeder `DiocesiSeeder.php` aveva solo ~100 nomi obsoleti/inaccurati (es. "Mongolia", "Donegal", "Tirana").
**Fix**:
1. **`DiocesiSeeder.php`** riscritto completamente:
- 225 nomi corretti (Arcidiocesi/Diocesi/Sede/Patriarcato/Abbazia/Eparachia)
- Encoding: 7 nomi con mojibake da ODS fixati via `where('id', ...)->update()` (Trinità, Cefalù, Città, Forlì, Mondovì, Nardò, Perugia-Città)
- Apostrofo: "Val d Elsa" → "Val d'Elsa"
- `declare(strict_types=1)` aggiunto
- `firstOrCreate` per idempotenza
2. **`build-dist.sh`** (`post-deploy.sh` generato):
- Upgrade: aggiunto step `[6/10] Seed diocesi` dopo migrazioni (rinumerati da [1-5/9] → [1-10/10])
- Install: aggiunto step `[9/11] Seed diocesi` dopo migrazioni (rinumerato da [1-8/10] → [1-11/11])
- Usa `--force` per bypassare conferma in produzione
**File modificati**:
- `database/seeders/DiocesiSeeder.php` — riscritto con 225 nomi + fix encoding
- `build-dist.sh` — aggiunto step seed diocesi in upgrade/install
## 2026-06-23 — Fix 419: SESSION_DRIVER=database + diagnose CSRF su IP locale
**Problema**: Accesso via IP di rete locale (`http://192.168.222.174`) poteva causare 419 Page Expired. Sessione su file vulnerabile a permessi/LOCK del filesystem.
**Fix**:
1. **`SESSION_DRIVER=database`**: cambiato da `file` a `database`. La sessione su DB non soffre di:
- Permessi filesystem errati
- Lock concorrente su file
- Pulizia sessioni scadute lottery-based
2. **Migration `2026_06_23_000001_create_sessions_table.php`**: con guard `Schema::hasTable` per idempotenza cross-DB
3. **`diagnose.php`**: aggiunto test CSRF su IP locale automatico. Rileva `hostname -I` ed esegue POST login → verifica 419 su `http://<lan-ip>/login`
4. **`.env` e `.env.example`**: `SESSION_DRIVER=file``SESSION_DRIVER=database`
5. Vecchi file di sessione in `storage/framework/sessions/` eliminati
**Risultato**:
- diagnose.php: `✅ HTTP: POST login → HTTP 302 (nessun 419)`
- diagnose.php: `✅ LOCAL IP: POST login → HTTP 302 (nessun 419)`
- Sessioni persistono in tabella `sessions` (DB) invece di file
**File modificati**:
- `database/migrations/2026_06_23_000001_create_sessions_table.php` (nuovo)
- `.env` — SESSION_DRIVER=database
- `.env.example` — SESSION_DRIVER=database
- `diagnose.php` — test CSRF su IP locale
## 2026-06-23 — Email compose: firma dopo corpo + Mailing list: mittente e firma
**Obiettivo**: Spostare la select firma dopo il corpo email nella pagina di composizione email. Aggiungere mittente predefinito (`sender_account_id`) alle mailing list (salvato in creazione/edit, usato come fallback all'invio).
### Cosa è stato fatto
**Migration** (`2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php`):
- Aggiunta colonna `sender_account_id` (FK → sender_accounts, nullOnDelete) a `mailing_lists`
**Model `MailingList.php`**:
- `sender_account_id` aggiunto a `$fillable`
- `senderAccount()`: nuova relazione BelongsTo → `SenderAccount`
**Controller `MailingListController.php`**:
- `create()`: passa `$senderAccounts` (SenderAccount::active()->get()) alla view
- `edit()`: passa `$senderAccounts` alla view, eager-load `senderAccount`
- `store()`: validazione `sender_account_id` nullable|exists, salvato in create
- `update()`: validazione `sender_account_id` nullable|exists, salvato in update
**View `mailing-liste/create.blade.php`**:
- Aggiunto select per `sender_account_id` dopo il select firma
**View `mailing-liste/edit.blade.php`**:
- Aggiunto select per `sender_account_id` dopo il select firma, con `selected` se uguale a `$mailingList->sender_account_id`
**View `email/compose.blade.php`**:
- Spostato blocco firma (prima del body) → dopo il textarea body (prima della sezione allegati)
**Controller `MailingController.php`**:
- `invia()`: fallback a `$lista->senderAccount` se `mittente_id` non fornito esplicitamente (stesso pattern del firma_id fallback già esistente)
- `invioElabora()`: fallback a `mittente_id`/`firma_id` dalla mailing list se non forniti esplicitamente (solo quando una singola lista è selezionata)
**File modificati**:
- `database/migrations/2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php` (nuovo)
- `app/Models/MailingList.php` — fillable + senderAccount relazione
- `app/Http/Controllers/MailingListController.php` — sender_account in create/edit/store/update
- `app/Http/Controllers/MailingController.php` — fallback mittente/firma da mailing list
- `resources/views/email/compose.blade.php` — firma spostata dopo il body
- `resources/views/mailing-liste/create.blade.php` — select mittente aggiunto
- `resources/views/mailing-liste/edit.blade.php` — select mittente aggiunto
+11 -3
View File
@@ -28,16 +28,17 @@ class BackupRunCommand extends Command
{
$this->info('Avvio backup...');
$options = [];
if ($this->option('no-files')) {
$this->backupService->saveConfig(['backup_include_files' => false]);
$options['include_files'] = false;
$this->warn('Files esclusi dal backup.');
}
if ($this->option('no-env')) {
$this->backupService->saveConfig(['backup_include_env' => false]);
$options['include_env'] = false;
$this->warn('.env escluso dal backup.');
}
$result = $this->backupService->run();
$result = $this->backupService->run($options);
if ($result['success']) {
$this->info(' Backup completato con successo!');
@@ -45,6 +46,13 @@ class BackupRunCommand extends Command
$this->line('Dimensione: ' . $result['size_formatted']);
$this->line('Percorso: ' . storage_path('app/backups/' . $result['filename']));
if (!empty($result['steps'])) {
$this->line('Contenuto:');
foreach ($result['steps'] as $step) {
$this->line(' - ' . $step);
}
}
return Command::SUCCESS;
}
@@ -34,8 +34,9 @@ class BackupController extends Controller
$result = $this->backupService->run();
if ($result['success']) {
$steps = !empty($result['steps']) ? ' — ' . implode(', ', $result['steps']) : '';
return redirect()->route('admin.backup.index')
->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')');
->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')' . $steps);
}
return redirect()->route('admin.backup.index')
@@ -7,12 +7,15 @@ use App\Http\Controllers\Controller;
use App\Models\EmailAttachment;
use App\Models\EmailFolder;
use App\Models\EmailSetting;
use App\Models\Firma;
use App\Models\SenderAccount;
use App\Services\GoogleOAuthService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
class EmailSettingsController extends Controller
{
@@ -22,7 +25,8 @@ class EmailSettingsController extends Controller
$settings = EmailSetting::first() ?? new EmailSetting();
$senderAccounts = SenderAccount::orderBy('email_address')->get();
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
return view('admin.email-settings.index', compact('settings', 'senderAccounts', 'googleStatus'));
$firme = $settings->exists ? $settings->firme : collect();
return view('admin.email-settings.index', compact('settings', 'senderAccounts', 'googleStatus', 'firme'));
}
public function save(Request $request)
@@ -41,6 +45,8 @@ class EmailSettingsController extends Controller
'smtp_password' => 'nullable|string',
'email_address' => 'required|email',
'email_name' => 'nullable|string|max:255',
'from_email' => 'nullable|email|max:255',
'from_name' => 'nullable|string|max:255',
'reply_to' => 'nullable|email',
'sync_interval_minutes' => 'nullable|integer|min:1|max:60',
'is_active' => 'boolean',
@@ -128,25 +134,20 @@ class EmailSettingsController extends Controller
return response()->json(['success' => false, 'message' => 'Impossibile creare il mailer SMTP. Verifica la configurazione.']);
}
$fromName = $settings->email_name ?? '';
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
$fromName = 'Glastree';
} else {
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName) || strlen($fromName) > 50) {
$fromName = 'Glastree';
}
}
$smtpUser = $settings->smtp_username ?: $settings->email_address;
$fromName = $settings->getEffectiveFromName();
$replyToEmail = $settings->from_email ?: $smtpUser;
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
$email = (new Email())
->from(Address::create($smtpUser, $fromName))
->to($request->email)
->subject('Test Configurazione SMTP - Glastree')
->text('Test email da Glastree - Configurazione SMTP funziona!');
if ($replyToEmail !== $smtpUser) {
$email->replyTo(Address::create($replyToEmail, $fromName));
}
$mailer->send($email);
\Illuminate\Support\Facades\Log::info('Test SMTP ok', ['to' => $request->email, 'from' => $settings->email_address]);
@@ -294,4 +295,66 @@ class EmailSettingsController extends Controller
return response()->json(['success' => false, 'message' => 'Errore invio: ' . $e->getMessage()]);
}
}
public function firmaStore(Request $request)
{
$this->authorizeWrite('settings');
$data = $request->validate([
'nome' => 'required|string|max:255',
'contenuto_html' => 'nullable|string',
'is_default' => 'boolean',
]);
$settings = EmailSetting::getActive();
if (!$settings) {
return redirect('/impostazioni/email')->with('error', 'Account email non configurato.');
}
$firma = $settings->firme()->create($data);
if ($data['is_default'] ?? false) {
$settings->firme()->where('id', '!=', $firma->id)->update(['is_default' => false]);
}
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" creata.');
}
public function firmaUpdate(Request $request, int $id)
{
$this->authorizeWrite('settings');
$data = $request->validate([
'nome' => 'required|string|max:255',
'contenuto_html' => 'nullable|string',
'is_default' => 'boolean',
]);
$firma = Firma::findOrFail($id);
$firma->update($data);
if ($data['is_default'] ?? false) {
Firma::where('email_setting_id', $firma->email_setting_id)
->where('id', '!=', $firma->id)
->update(['is_default' => false]);
}
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" aggiornata.');
}
public function firmaSetDefault(int $id)
{
$this->authorizeWrite('settings');
$firma = Firma::findOrFail($id);
Firma::where('email_setting_id', $firma->email_setting_id)->update(['is_default' => false]);
$firma->update(['is_default' => true]);
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" impostata come predefinita.');
}
public function firmaDestroy(int $id)
{
$this->authorizeWrite('settings');
$firma = Firma::findOrFail($id);
$nome = $firma->nome;
$firma->delete();
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $nome . '" eliminata.');
}
}
+41 -25
View File
@@ -11,6 +11,7 @@ use App\Models\MailingList;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Documento;
use App\Models\Firma;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
@@ -96,6 +97,9 @@ class EmailController extends Controller
$documenti = Documento::orderBy('nome_file')->get();
$settings = EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$prefill = [];
if ($request->reply_to) {
$original = EmailMessage::find($request->reply_to);
@@ -113,7 +117,7 @@ class EmailController extends Controller
}
}
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts'));
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts', 'firme'));
}
public function send(Request $request)
@@ -128,6 +132,7 @@ class EmailController extends Controller
$rules['to'] = 'required';
}
$rules['firma_id'] = 'nullable|integer|exists:firme,id';
$validated = $request->validate($rules);
$recipients = $this->resolveRecipients($request);
@@ -159,9 +164,11 @@ class EmailController extends Controller
$imapError = null;
$attachments = $this->processAttachments($request);
$firmaId = $request->firma_id;
foreach ($recipients as $recipient) {
try {
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
$bodyWithSig = $this->appendSignature($validated['body'], $firmaId, $settings);
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachments);
$imapSuccess = true;
} catch (\Exception $e) {
@@ -171,7 +178,7 @@ class EmailController extends Controller
}
try {
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
$bodyWithSig = $this->appendSignature($validated['body'], $firmaId, $settings);
$this->storeSentMessage($settings, $recipients, $validated['subject'], $bodyWithSig, $request->cc);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Store sent message error', ['error' => $e->getMessage()]);
@@ -189,12 +196,16 @@ class EmailController extends Controller
{
$sender = SenderAccount::findOrFail($request->mittente_id);
$attachmentPaths = $this->resolveAttachmentPaths($request);
$settings = EmailSetting::getActive();
$body = $this->appendSignature($validated['body'], $request->firma_id, $settings);
$ok = 0;
$errors = [];
foreach ($recipients as $recipient) {
try {
$sender->sendEmail($recipient, $validated['subject'], $validated['body'], $attachmentPaths);
$sender->sendEmail($recipient, $validated['subject'], $body, $attachmentPaths);
$ok++;
} catch (\Exception $e) {
$errors[] = $recipient . ': ' . $e->getMessage();
@@ -237,8 +248,8 @@ class EmailController extends Controller
'body_text' => $request->body,
'to_email' => $request->to,
'is_draft' => true,
'from_email' => $settings->email_address ?? '',
'from_name' => $settings->email_name ?? '',
'from_email' => $settings->getEffectiveFromAddress(),
'from_name' => $settings->getEffectiveFromName(),
]);
return response()->json(['success' => true, 'draft_id' => $message->id]);
@@ -496,12 +507,20 @@ class EmailController extends Controller
return array_filter(array_unique($recipients));
}
private function appendSignature(string $body, ?string $signature, bool $enabled): string
private function appendSignature(string $body, int|string|null $firmaId = null, ?EmailSetting $settings = null): string
{
if (empty($signature) || !$enabled) {
return $body;
if ($firmaId) {
$firma = Firma::find($firmaId);
if ($firma && $firma->contenuto_html) {
return $body . "\n\n---\n" . $firma->contenuto_html;
}
return $body . "\n\n---\n" . $signature;
}
if ($settings && $settings->signature && ($settings->signature_enabled ?? false)) {
return $body . "\n\n---\n" . $settings->signature;
}
return $body;
}
private function sendViaImap($settings, $to, $subject, $body, $cc = null, array $attachmentPaths = [])
@@ -527,18 +546,10 @@ class EmailController extends Controller
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? '';
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
$fromName = 'Glastree';
} else {
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName)) {
$fromName = 'Glastree';
}
}
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$fromAddress = \Symfony\Component\Mime\Address::create(
$settings->getEffectiveFromAddress(),
$settings->getEffectiveFromName()
);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
@@ -546,6 +557,11 @@ class EmailController extends Controller
->subject($subject)
->text($body);
$replyTo = $settings->getEffectiveReplyTo();
if ($replyTo) {
$email->replyTo($replyTo);
}
if ($cc) {
$email->cc($cc);
}
@@ -619,14 +635,14 @@ class EmailController extends Controller
private function storeSentMessage($settings, $recipients, $subject, $body, $cc = null)
{
$sentFolder = EmailFolder::where('type', 'sent')->first();
$bodyWithSignature = $this->appendSignature($body, $settings->getSignature(), $settings->signature_enabled ?? false);
$bodyWithSignature = $body;
$message = EmailMessage::create([
'email_folder_id' => $sentFolder->id,
'subject' => $subject,
'body_text' => $bodyWithSignature,
'from_email' => $settings->email_address,
'from_name' => $settings->email_name,
'from_email' => $settings->getEffectiveFromAddress(),
'from_name' => $settings->getEffectiveFromName(),
'to_email' => implode(', ', $recipients),
'cc' => $cc,
'is_sent' => true,
+54 -8
View File
@@ -66,7 +66,7 @@ class GruppoController extends Controller
$entityType = 'gruppi';
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
$gruppiQuery = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar', 'tags']);
$gruppiQuery = Gruppo::with(['diocesi', 'gruppoContatti', 'parent', 'children', 'individui', 'avatar', 'tags']);
if (request()->filled('tag')) {
$tagSlugs = (array) request()->input('tag');
@@ -142,7 +142,7 @@ class GruppoController extends Controller
public function create(Request $request)
{
$this->authorizeWrite('gruppi');
$diocesi = Diocesi::orderBy('nome')->get();
$diocesi = Diocesi::orderBy('id')->get();
$allGruppi = Gruppo::orderBy('nome')->get();
$selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null;
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
@@ -157,7 +157,8 @@ class GruppoController extends Controller
'nome' => 'required|string|max:255',
'descrizione' => 'nullable|string',
'parent_id' => 'nullable|exists:gruppi,id',
'diocesi_id' => 'nullable|exists:diocesi,id',
'diocesi_ids' => 'nullable|array',
'diocesi_ids.*' => 'exists:diocesi,id',
'responsabile_ids' => 'nullable|array',
'responsabile_ids.*' => 'exists:individui,id',
'indirizzo_incontro' => 'nullable|string|max:500',
@@ -165,6 +166,11 @@ class GruppoController extends Controller
'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string',
'contatti' => 'nullable|array',
'contatti.*.tipo' => 'required|string|in:email,telefono,cellulare',
'contatti.*.valore' => 'required|string|max:255',
'contatti.*.etichetta' => 'nullable|string|max:255',
'contatti.*.is_primary' => 'nullable|boolean',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
@@ -175,6 +181,21 @@ class GruppoController extends Controller
$gruppo = Gruppo::create($data);
if (!empty($data['diocesi_ids'])) {
$gruppo->diocesi()->sync($data['diocesi_ids']);
}
if (!empty($data['contatti'])) {
foreach ($data['contatti'] as $contattoData) {
$gruppo->gruppoContatti()->create([
'tipo' => $contattoData['tipo'],
'valore' => $contattoData['valore'],
'etichetta' => $contattoData['etichetta'] ?? null,
'is_primary' => $contattoData['is_primary'] ?? false,
]);
}
}
if ($request->has('tags')) {
$gruppo->tags()->sync($request->tags);
}
@@ -196,7 +217,7 @@ class GruppoController extends Controller
public function show($id)
{
$this->authorizeRead('gruppi');
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'gruppoContatti', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
}, 'avatar', 'tags', 'eventi' => function ($q) {
$q->where('is_incontro_gruppo', true);
@@ -207,13 +228,14 @@ class GruppoController extends Controller
public function edit($id)
{
$this->authorizeWrite('gruppi');
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar', 'tags'])->findOrFail($id);
$diocesi = Diocesi::orderBy('nome')->get();
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'gruppoContatti', 'avatar', 'tags'])->findOrFail($id);
$diocesi = Diocesi::orderBy('id')->get();
$gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get();
$membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get();
$tags = Tag::orderBy('name')->get();
$selectedTags = $gruppo->tags->pluck('id')->toArray();
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags'));
$selectedDiocesiIds = $gruppo->diocesi()->pluck('diocesi.id')->toArray();
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags', 'selectedDiocesiIds'));
}
public function update(Request $request, $id)
@@ -224,7 +246,8 @@ class GruppoController extends Controller
'nome' => 'required|string|max:255',
'descrizione' => 'nullable|string',
'parent_id' => 'nullable|exists:gruppi,id',
'diocesi_id' => 'nullable|exists:diocesi,id',
'diocesi_ids' => 'nullable|array',
'diocesi_ids.*' => 'exists:diocesi,id',
'responsabile_ids' => 'nullable|array',
'responsabile_ids.*' => 'exists:individui,id',
'indirizzo_incontro' => 'nullable|string|max:500',
@@ -232,6 +255,11 @@ class GruppoController extends Controller
'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string',
'contatti' => 'nullable|array',
'contatti.*.tipo' => 'required|string|in:email,telefono,cellulare',
'contatti.*.valore' => 'required|string|max:255',
'contatti.*.etichetta' => 'nullable|string|max:255',
'contatti.*.is_primary' => 'nullable|boolean',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
@@ -244,6 +272,24 @@ class GruppoController extends Controller
$gruppo->update($data);
if ($request->has('diocesi_ids')) {
$gruppo->diocesi()->sync($data['diocesi_ids'] ?? []);
} else {
$gruppo->diocesi()->detach();
}
if ($request->has('contatti')) {
$gruppo->gruppoContatti()->delete();
foreach ($data['contatti'] as $contattoData) {
$gruppo->gruppoContatti()->create([
'tipo' => $contattoData['tipo'],
'valore' => $contattoData['valore'],
'etichetta' => $contattoData['etichetta'] ?? null,
'is_primary' => $contattoData['is_primary'] ?? false,
]);
}
}
if ($request->has('tags')) {
$gruppo->tags()->sync($request->tags);
} else {
+1 -1
View File
@@ -152,7 +152,7 @@ public function create()
{
$this->authorizeWrite('individui');
$comuni = Comune::orderBy('nome')->get();
$diocesi = Diocesi::orderBy('nome')->get();
$diocesi = Diocesi::orderBy('id')->get();
$tags = Tag::orderBy('name')->get();
return view('individui.create', compact('comuni', 'diocesi', 'tags'));
}
+65 -22
View File
@@ -10,6 +10,8 @@ use App\Models\MailingMessaggio;
use App\Models\SenderAccount;
use App\Models\Individuo;
use App\Models\Documento;
use App\Models\EmailSetting;
use App\Models\Firma;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
@@ -30,6 +32,8 @@ class MailingController extends Controller
$liste = MailingList::all();
$documenti = Documento::orderBy('nome_file')->get();
$senderAccounts = SenderAccount::active()->get();
$settings = EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$documentiSelezionati = collect();
if ($request->has('documenti_selezionati')) {
@@ -37,7 +41,7 @@ class MailingController extends Controller
$documentiSelezionati = Documento::whereIn('id', $docIds)->get();
}
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts'));
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts', 'firme'));
}
public function invia(Request $request)
@@ -46,16 +50,22 @@ class MailingController extends Controller
$data = $request->validate([
'oggetto' => 'required|string|max:255',
'corpo' => 'required',
'lista_id' => 'nullable|exists:mailing_liste,id',
'lista_id' => 'nullable|exists:mailing_lists,id',
'destinatari_ids' => 'nullable|string',
'documenti_selezionati' => 'nullable|string',
'allegato' => 'nullable|file|max:10240',
'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
'firma_id' => 'nullable|integer|exists:firme,id',
]);
$sender = null;
if (!empty($data['mittente_id'])) {
$sender = SenderAccount::findOrFail($data['mittente_id']);
} elseif (!empty($data['lista_id'])) {
$lista = MailingList::with('senderAccount')->find($data['lista_id']);
if ($lista && $lista->senderAccount) {
$sender = $lista->senderAccount;
}
}
$documentiAllegati = [];
@@ -112,6 +122,20 @@ class MailingController extends Controller
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
$firmaId = $data['firma_id'] ?? null;
if (!$firmaId && !empty($data['lista_id'])) {
$lista = MailingList::with('firma')->find($data['lista_id']);
$firmaId = $lista?->firma?->id;
}
$corpoConFirma = $data['corpo'];
if ($firmaId) {
$firma = Firma::find($firmaId);
if ($firma && $firma->contenuto_html) {
$corpoConFirma .= "\n\n---\n" . $firma->contenuto_html;
}
}
$messaggio = MailingMessaggio::create([
'mailing_list_id' => $data['lista_id'] ?? null,
'user_id' => auth()->id(),
@@ -121,6 +145,7 @@ class MailingController extends Controller
'totale_destinatari' => $emails->count(),
'mittente_nome' => $sender?->email_name,
'mittente_email' => $sender?->email_address,
'firma_id' => $firmaId,
]);
$ok = 0;
@@ -130,9 +155,9 @@ class MailingController extends Controller
foreach ($emails as $email) {
try {
if ($sender) {
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
$sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
} else {
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
$this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
}
$ok++;
} catch (\Throwable $e) {
@@ -181,11 +206,13 @@ class MailingController extends Controller
public function invio(Request $request)
{
$this->authorizeWrite('mailing');
$liste = MailingList::where('attiva', true)->orderBy('nome')->get();
$liste = MailingList::with('firma')->where('attiva', true)->orderBy('nome')->get();
$documenti = Documento::orderBy('nome_file')->get();
$senderAccounts = SenderAccount::active()->get();
$settings = EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts'));
return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts', 'firme'));
}
public function invioElabora(Request $request)
@@ -193,18 +220,23 @@ class MailingController extends Controller
$this->authorizeWrite('mailing');
$data = $request->validate([
'liste' => 'required|array',
'liste.*' => 'exists:mailing_liste,id',
'liste.*' => 'exists:mailing_lists,id',
'oggetto' => 'required|string|max:255',
'corpo' => 'required',
'documenti_selezionati' => 'nullable|array',
'documenti_selezionati.*' => 'exists:documenti,id',
'allegato' => 'nullable|file|max:10240',
'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
'firma_id' => 'nullable|integer|exists:firme,id',
]);
$listeModels = MailingList::with('firma', 'senderAccount')->whereIn('id', $data['liste'])->get();
$sender = null;
if (!empty($data['mittente_id'])) {
$sender = SenderAccount::findOrFail($data['mittente_id']);
} elseif ($listeModels->count() === 1) {
$sender = $listeModels->first()->senderAccount;
}
$documentiAllegati = [];
@@ -254,7 +286,20 @@ class MailingController extends Controller
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
$listeNomi = MailingList::whereIn('id', $data['liste'])->pluck('nome')->implode(', ');
$firmaId = $data['firma_id'] ?? null;
if (!$firmaId && $listeModels->count() === 1) {
$firmaId = $listeModels->first()->firma_id;
}
$corpoConFirma = $data['corpo'];
if ($firmaId) {
$firma = Firma::find($firmaId);
if ($firma && $firma->contenuto_html) {
$corpoConFirma .= "\n\n---\n" . $firma->contenuto_html;
}
}
$listeNomi = $listeModels->pluck('nome')->implode(', ');
$messaggio = MailingMessaggio::create([
'mailing_list_id' => null,
@@ -265,6 +310,7 @@ class MailingController extends Controller
'totale_destinatari' => $emails->count(),
'mittente_nome' => $sender?->email_name,
'mittente_email' => $sender?->email_address,
'firma_id' => $firmaId,
]);
$ok = 0;
@@ -274,9 +320,9 @@ class MailingController extends Controller
foreach ($emails as $email) {
try {
if ($sender) {
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
$sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
} else {
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
$this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
}
$ok++;
} catch (\Throwable $e) {
@@ -347,18 +393,10 @@ class MailingController extends Controller
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? '';
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
$fromName = 'Glastree';
} else {
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName) || strlen($fromName) > 50) {
$fromName = 'Glastree';
}
}
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$fromAddress = \Symfony\Component\Mime\Address::create(
$settings->getEffectiveFromAddress(),
$settings->getEffectiveFromName()
);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
@@ -366,6 +404,11 @@ class MailingController extends Controller
->subject($subject)
->text($body);
$replyTo = $settings->getEffectiveReplyTo();
if ($replyTo) {
$email->replyTo($replyTo);
}
foreach ($attachmentPaths as $path) {
$email->attachFromPath($path);
}
+18 -3
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\MailingList;
use App\Models\SenderAccount;
use App\Models\Tag;
use Illuminate\Http\Request;
@@ -60,7 +61,10 @@ class MailingListController extends Controller
{
$this->authorizeWrite('mailing');
$tags = Tag::orderBy('name')->get();
return view('mailing-liste.create', compact('tags'));
$settings = \App\Models\EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$senderAccounts = SenderAccount::active()->get();
return view('mailing-liste.create', compact('tags', 'firme', 'senderAccounts'));
}
public function store(Request $request)
@@ -73,6 +77,8 @@ class MailingListController extends Controller
'contatti_json' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'firma_id' => 'nullable|integer|exists:firme,id',
'sender_account_id' => 'nullable|integer|exists:sender_accounts,id',
]);
$data['user_id'] = auth()->id();
@@ -83,6 +89,8 @@ class MailingListController extends Controller
'descrizione' => $data['descrizione'] ?? null,
'attiva' => $data['attiva'],
'user_id' => $data['user_id'],
'firma_id' => $data['firma_id'] ?? null,
'sender_account_id' => $data['sender_account_id'] ?? null,
]);
if ($request->has('tags')) {
@@ -117,10 +125,13 @@ class MailingListController extends Controller
public function edit($mailingList)
{
$this->authorizeWrite('mailing');
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailingList);
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags', 'firma', 'senderAccount'])->findOrFail($mailingList);
$tags = Tag::orderBy('name')->get();
$selectedTags = $mailingList->tags->pluck('id')->toArray();
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags'));
$settings = \App\Models\EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$senderAccounts = SenderAccount::active()->get();
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags', 'firme', 'senderAccounts'));
}
public function update(Request $request, $mailingList)
@@ -135,6 +146,8 @@ class MailingListController extends Controller
'contatti_json' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'firma_id' => 'nullable|integer|exists:firme,id',
'sender_account_id' => 'nullable|integer|exists:sender_accounts,id',
]);
$data['attiva'] = $data['attiva'] ?? false;
@@ -143,6 +156,8 @@ class MailingListController extends Controller
'nome' => $data['nome'],
'descrizione' => $data['descrizione'] ?? null,
'attiva' => $data['attiva'],
'firma_id' => $data['firma_id'] ?? null,
'sender_account_id' => $data['sender_account_id'] ?? null,
]);
if ($request->has('tags')) {
+4 -4
View File
@@ -515,7 +515,7 @@ class ReportController extends Controller
$rows[] = [
'livello' => 0,
'nome' => $gruppo->nome,
'diocesi' => $gruppo->diocesi?->nome ?? '-',
'diocesi' => $gruppo->diocesi->count() > 0 ? $gruppo->diocesi->pluck('nome')->implode(', ') : '-',
'membri' => $gruppo->individui()->count(),
'padre' => '-',
];
@@ -538,7 +538,7 @@ class ReportController extends Controller
$rows[] = [
'livello' => $level,
'nome' => str_repeat(' ', $level) . $child->nome,
'diocesi' => $child->diocesi?->nome ?? '-',
'diocesi' => $child->diocesi->count() > 0 ? $child->diocesi->pluck('nome')->implode(', ') : '-',
'membri' => $child->individui()->count(),
'padre' => $parent?->nome ?? '-',
];
@@ -554,7 +554,7 @@ class ReportController extends Controller
$parent = $g->parent;
return [
'nome' => $g->full_path,
'diocesi' => $g->diocesi?->nome ?? '-',
'diocesi' => $g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
'membri' => $g->individui_count,
'padre' => $parent?->nome ?? '-',
'responsabili' => $g->getResponsabili()->pluck('cognome')->implode(', ') ?: '-',
@@ -895,7 +895,7 @@ class ReportController extends Controller
return [
'nome' => $g->nome,
'descrizione' => $g->descrizione ?? '-',
'diocesi' => $g->diocesi?->nome ?? '-',
'diocesi' => $g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
'livello' => $g->parent_id ? (count($g->getAncestors()) + 1) : 0,
'membri' => $g->individui_count,
'padre' => $parent?->nome ?? '-',
+3 -3
View File
@@ -3,15 +3,15 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Diocesi extends Model
{
protected $table = 'diocesi';
protected $fillable = ['nome', 'regione'];
public function gruppi(): HasMany
public function gruppi(): BelongsToMany
{
return $this->hasMany(Gruppo::class);
return $this->belongsToMany(Gruppo::class, 'diocesi_gruppo');
}
}
+36 -1
View File
@@ -15,7 +15,8 @@ class EmailSetting extends Model
protected $fillable = [
'imap_host', 'imap_port', 'imap_encryption', 'imap_username', 'imap_password',
'smtp_host', 'smtp_port', 'smtp_encryption', 'smtp_username', 'smtp_password',
'email_address', 'email_name', 'reply_to', 'sync_interval_minutes',
'email_address', 'email_name', 'from_email', 'from_name', 'reply_to',
'sync_interval_minutes',
'last_sync_at', 'is_active', 'signature', 'signature_enabled',
'auth_method', 'google_oauth_connection_id',
];
@@ -247,4 +248,38 @@ class EmailSetting extends Model
{
return $this->signature;
}
public function getEffectiveFromAddress(): string
{
return $this->from_email ?: $this->email_address;
}
public function getEffectiveFromName(): string
{
$name = $this->from_name ?: $this->email_name;
if (filter_var($name, FILTER_VALIDATE_EMAIL)) {
return 'Glastree';
}
$name = preg_replace('/[^\p{L}\p{N}\s]/u', '', $name);
$name = trim($name);
return empty($name) ? 'Glastree' : $name;
}
public function getEffectiveReplyTo(): ?string
{
if ($this->from_email) {
return $this->from_email;
}
return $this->reply_to ?: null;
}
public function firme(): HasMany
{
return $this->hasMany(Firma::class);
}
public function firmaPredefinita(): ?Firma
{
return $this->firme()->where('is_default', true)->first() ?? $this->firme()->first();
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Firma extends Model
{
protected $table = 'firme';
protected $fillable = [
'email_setting_id',
'nome',
'contenuto_html',
'is_default',
];
protected $casts = [
'is_default' => 'boolean',
];
public function emailSetting(): BelongsTo
{
return $this->belongsTo(EmailSetting::class);
}
}
+21 -2
View File
@@ -24,6 +24,8 @@ class Gruppo extends Model
'responsabile_ids' => 'array',
];
protected $appends = ['email_primaria', 'telefono_primario'];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
@@ -39,9 +41,14 @@ class Gruppo extends Model
return $this->hasMany(Gruppo::class, 'parent_id');
}
public function diocesi(): BelongsTo
public function diocesi(): BelongsToMany
{
return $this->belongsTo(Diocesi::class);
return $this->belongsToMany(Diocesi::class, 'diocesi_gruppo');
}
public function gruppoContatti(): HasMany
{
return $this->hasMany(GruppoContatto::class, 'gruppo_id');
}
public function individui(): BelongsToMany
@@ -66,6 +73,18 @@ class Gruppo extends Model
return null;
}
public function getEmailPrimariaAttribute(): ?string
{
return $this->gruppoContatti()->where('tipo', 'email')->where('is_primary', true)->first()?->valore
?? $this->gruppoContatti()->where('tipo', 'email')->first()?->valore;
}
public function getTelefonoPrimarioAttribute(): ?string
{
return $this->gruppoContatti()->whereIn('tipo', ['telefono', 'cellulare'])->where('is_primary', true)->first()?->valore
?? $this->gruppoContatti()->whereIn('tipo', ['telefono', 'cellulare'])->first()?->valore;
}
public function getResponsabili()
{
if (empty($this->responsabile_ids)) {
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class GruppoContatto extends Model
{
protected $table = 'gruppo_contatti';
protected $fillable = ['gruppo_id', 'tipo', 'valore', 'etichetta', 'is_primary'];
protected $casts = [
'is_primary' => 'boolean',
];
public function gruppo(): BelongsTo
{
return $this->belongsTo(Gruppo::class);
}
}
+11 -1
View File
@@ -12,7 +12,7 @@ class MailingList extends Model
use HasTagsLight;
protected $table = 'mailing_lists';
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva'];
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva', 'firma_id', 'sender_account_id'];
protected $casts = ['attiva' => 'boolean'];
@@ -36,6 +36,16 @@ class MailingList extends Model
return $this->hasMany(MailingMessaggio::class);
}
public function firma(): BelongsTo
{
return $this->belongsTo(Firma::class);
}
public function senderAccount(): BelongsTo
{
return $this->belongsTo(SenderAccount::class);
}
public function getIndividui()
{
return Individuo::whereHas('mailingContacts', function ($q) {
+1 -1
View File
@@ -11,7 +11,7 @@ class MailingMessaggio extends Model
protected $fillable = [
'tenant_id', 'mailing_list_id', 'user_id', 'oggetto', 'corpo',
'canale', 'stato', 'inviato_al', 'totale_destinatari', 'invii_ok', 'invii_falliti',
'log_falliti', 'mittente_nome', 'mittente_email',
'log_falliti', 'mittente_nome', 'mittente_email', 'firma_id',
];
protected $casts = [
+5 -4
View File
@@ -197,12 +197,13 @@ class User extends Authenticatable implements CanResetPasswordContract
$resetUrl = url('/password/reset/' . $token);
$fromName = $settings->email_name ?? config('app.name');
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName) ?: config('app.name');
$fromAddress = new Address(
$settings->getEffectiveFromAddress(),
$settings->getEffectiveFromName()
);
$email = (new Email())
->from(new Address($settings->email_address, $fromName))
->from($fromAddress)
->to($this->email)
->subject('Reset della password - ' . config('app.name'))
->html(view('auth.emails.reset-password', [
+29 -10
View File
@@ -60,7 +60,7 @@ class BackupService
return $backups;
}
public function run(): array
public function run(array $options = []): array
{
$result = ['success' => true, 'message' => '', 'filename' => ''];
@@ -74,12 +74,28 @@ class BackupService
$filename = $appName . '_' . $timestamp . '.zip';
$filepath = $this->backupDir . '/' . $filename;
$includeFiles = $options['include_files'] ?? (bool) AppSetting::getSetting('backup_include_files', true);
$includeEnv = $options['include_env'] ?? (bool) AppSetting::getSetting('backup_include_env', true);
$steps = [];
$includedComponents = [];
$steps[] = $this->backupDatabase();
$steps[] = $this->backupEnv();
$steps[] = $this->backupFiles();
$this->writeManifest($timestamp);
$includedComponents[] = 'database';
$stepEnv = $this->backupEnv($includeEnv);
$steps[] = $stepEnv;
if ($includeEnv) {
$includedComponents[] = 'env';
}
$stepFiles = $this->backupFiles($includeFiles);
$steps[] = $stepFiles;
if ($includeFiles) {
$includedComponents[] = 'files';
}
$this->writeManifest($timestamp, $includedComponents);
$zipResult = $this->createZip($filepath);
if (!$zipResult) {
@@ -90,7 +106,7 @@ class BackupService
$size = file_exists($filepath) ? filesize($filepath) : 0;
Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ")");
Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ')');
$this->cleanupOldBackups();
@@ -100,6 +116,7 @@ class BackupService
'filename' => $filename,
'size' => $size,
'size_formatted' => $this->formatBytes($size),
'steps' => $steps,
];
} catch (\Exception $e) {
$this->cleanTemp();
@@ -109,6 +126,7 @@ class BackupService
'success' => false,
'message' => 'Backup fallito: ' . $e->getMessage(),
'filename' => '',
'steps' => [],
];
}
}
@@ -181,9 +199,9 @@ class BackupService
return 'database.sql (' . $this->formatBytes(filesize($sqlFile)) . ')';
}
private function backupEnv(): string
private function backupEnv(bool $includeEnv = true): string
{
if (!AppSetting::getSetting('backup_include_env', true)) {
if (!$includeEnv) {
return '.env (escluso)';
}
@@ -200,9 +218,9 @@ class BackupService
return '.env copiato';
}
private function backupFiles(): string
private function backupFiles(bool $includeFiles = true): string
{
if (!AppSetting::getSetting('backup_include_files', true)) {
if (!$includeFiles) {
return 'files (esclusi)';
}
@@ -220,7 +238,7 @@ class BackupService
return 'files copiati';
}
private function writeManifest(string $timestamp): void
private function writeManifest(string $timestamp, array $includedComponents = ['database']): void
{
$manifest = [
'created_at' => $timestamp,
@@ -230,6 +248,7 @@ class BackupService
'db_name' => config('database.connections.mysql.database'),
'php_version' => PHP_VERSION,
'laravel_version' => app()->version(),
'included_components' => $includedComponents,
];
File::put($this->tempDir . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT));
+3 -6
View File
@@ -19,12 +19,9 @@ return Application::configure(basePath: dirname(__DIR__))
'permission' => CheckPermission::class,
]);
$middleware->trustProxies(at: '*');
$middleware->web(append: [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
]);
// Web middleware defaults are already applied by the framework.
// Do NOT append EncryptCookies, StartSession, etc. here — they
// would run twice and corrupt session cookies, causing 419 errors.
})
->withExceptions(function (Exceptions $exceptions): void {
//
+227 -27
View File
@@ -20,6 +20,223 @@ echo "==> Pulizia cache..."
rm -rf bootstrap/cache/*.php storage/framework/cache/data/* storage/framework/sessions/* storage/framework/views/* storage/logs/* 2>/dev/null || true
echo " OK"
echo "==> Generazione post-deploy.sh..."
cat > post-deploy.sh << 'POSTDEPLOY'
#!/bin/bash
# post-deploy.sh — Generated by build-dist.sh
# Usage: bash post-deploy.sh {check|upgrade|install} [--yes]
set -u
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
ARTISAN="php ${APP_DIR}/artisan"
DIAGNOSE="php ${APP_DIR}/diagnose.php"
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'
CYAN='\033[36m'; BOLD='\033[1m'; NC='\033[0m'
MODE="${1:-check}"
BATCH=false; [ "${2:-}" = "--yes" ] && BATCH=true
echo -e "${CYAN}${BOLD}============================================${NC}"
echo -e "${CYAN}${BOLD} Glastree Post-Deploy — ${MODE}${NC}"
echo -e "${CYAN}${BOLD}============================================${NC}"
echo ""
IS_FRESH=false; [ ! -f "${APP_DIR}/.env" ] && IS_FRESH=true
log() { echo -e " ${BOLD}$1${NC}"; }
ok() { echo -e " ${GREEN}✅ $1${NC}"; }
warn() { echo -e " ${YELLOW}⚠ $1${NC}"; }
fail() { echo -e " ${RED}❌ $1${NC}"; }
check_artisan() {
if [ ! -f "${APP_DIR}/artisan" ]; then
fail "artisan non trovato. Esegui da root dell'applicazione."
exit 1
fi
}
run_step() {
local step="$1" desc="$2"; shift 2
log "${step} ${desc}"
if "$@" 2>&1 | sed 's/^/ /'; then
ok "${desc}"
return 0
else
local rc=$?
fail "${desc} — exit code ${rc}"
return ${rc}
fi
}
run_diagnose() {
if ${DIAGNOSE} 2>&1 | tail -5; then
ok "Diagnostica OK"
else
warn "Diagnostica ha rilevato problemi (non bloccante)"
fi
}
run_check() {
echo -e "${YELLOW}═══════════ CONTROLLO ═══════════${NC}" ; echo ""
echo -e "${BOLD}[0] Diagnostica sistema${NC}"
run_diagnose ; echo ""
echo -e "${BOLD}[1] Ambiente${NC}"
if $IS_FRESH; then warn ".env non trovato — fresh install necessario"
else ok ".env presente"
fi ; echo ""
echo -e "${BOLD}[2] Migration pendenti${NC}"
if [ -f "${APP_DIR}/artisan" ]; then
$ARTISAN migrate:status 2>&1 | grep -E '(Pending|Ran)' | head -5 || true
PENDING=$(php -r "echo trim(shell_exec('${ARTISAN} migrate:status 2>&1 | grep -c Pending || true'));" 2>/dev/null || echo "0")
if [ "${PENDING:-0}" -gt 0 ] 2>/dev/null; then
warn "${PENDING} migration da eseguire"
else
ok "Nessuna migration pendente"
fi
else
warn "artisan non disponibile — skip migration check"
fi ; echo ""
echo -e "${BOLD}[3] Storage & Permessi${NC}"
for d in storage/framework/cache/data storage/framework/sessions \
storage/framework/views storage/logs bootstrap/cache \
storage/app/public storage/app/backups; do
p="${APP_DIR}/${d}"
if [ -d "$p" ]; then
[ -w "$p" ] && ok "${d}" || warn "${d} — non scrivibile"
else
warn "${d} — mancante"
fi
done ; echo ""
echo -e "${BOLD}[4] Symlink storage${NC}"
if [ -L "${APP_DIR}/public/storage" ]; then
ok "public/storage → $(readlink "${APP_DIR}/public/storage")"
else
warn "public/storage non presente"
fi ; echo ""
echo -e "${BOLD}[5] Cache${NC}"
for d in bootstrap/cache storage/framework/cache/data \
storage/framework/sessions storage/framework/views; do
c=$(find "${APP_DIR}/${d}" -type f 2>/dev/null | wc -l)
[ "$c" -gt 0 ] && warn "${d}: ${c} file" || ok "${d}: vuoto"
done
echo "" ; echo -e "${YELLOW}═══════════ FINE CONTROLLO ═══════════${NC}"
}
run_upgrade() {
echo -e "${YELLOW}═══════════ UPGRADE ═══════════${NC}" ; check_artisan
run_step "[1/9]" "Directory storage" mkdir -p \
"${APP_DIR}/storage/framework/cache/data" \
"${APP_DIR}/storage/framework/sessions" \
"${APP_DIR}/storage/framework/views" \
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
"${APP_DIR}/storage/app/public" \
"${APP_DIR}/storage/app/public/logos" \
"${APP_DIR}/storage/app/public/documenti/eventi" \
"${APP_DIR}/storage/app/backups" \
"${APP_DIR}/storage/app/documenti" \
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
run_step "[2/9]" ".gitignore storage" bash -c \
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
run_step "[3/9]" "Permessi" chmod -R 775 \
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
run_step "[4/9]" "Symlink storage" bash -c \
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
run_step "[5/10]" "Migrazioni DB" $ARTISAN migrate --force || true
run_step "[6/10]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
run_step "[7/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
run_step "[8/10]" "Key generate" $ARTISAN key:generate --force || true
run_step "[9/10]" "Pulizia cache" bash -c \
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
run_step "[10/10]" "Verifica finale" $DIAGNOSE || true
echo "" ; echo -e "${GREEN}${BOLD}✅ Upgrade completato.${NC}"
}
run_install() {
echo -e "${YELLOW}═══════════ INSTALL (fresh) ═══════════${NC}" ; check_artisan
run_step "[1/10]" "Directory storage" mkdir -p \
"${APP_DIR}/storage/framework/cache/data" \
"${APP_DIR}/storage/framework/sessions" \
"${APP_DIR}/storage/framework/views" \
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
"${APP_DIR}/storage/app/public" \
"${APP_DIR}/storage/app/public/logos" \
"${APP_DIR}/storage/app/public/documenti/eventi" \
"${APP_DIR}/storage/app/backups" \
"${APP_DIR}/storage/app/documenti" \
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
run_step "[2/10]" ".gitignore storage" bash -c \
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
run_step "[3/10]" "Permessi" chmod -R 775 \
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
run_step "[4/10]" "File .env" cp "${APP_DIR}/.env.example" "${APP_DIR}/.env" || true
warn "MODIFICA .env con i tuoi dati DB/URL prima di proseguire"
if ! $BATCH; then read -p " Fatto? (Invio per continuare) " _; fi
run_step "[5/10]" "Symlink storage" bash -c \
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
run_step "[6/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
run_step "[7/10]" "Key generate" $ARTISAN key:generate --force || true
run_step "[8/11]" "Migrazioni DB" $ARTISAN migrate --force || true
run_step "[9/11]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
run_step "[10/11]" "Pulizia cache" bash -c \
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
run_step "[11/11]" "Verifica finale" $DIAGNOSE || true
echo "" ; echo -e "${GREEN}${BOLD}✅ Installazione completata.${NC}"
}
case "$MODE" in
check) run_check ;;
upgrade)
run_check ; echo ""
if $BATCH; then run_upgrade
else
read -p "Procedere con l'upgrade? (s/N) " c
[ "$c" = "s" ] || [ "$c" = "S" ] && run_upgrade || echo "Annullato."
fi
;;
install) run_install ;;
*)
echo "Uso: bash post-deploy.sh {check|upgrade|install} [--yes]"
echo " check — diagnostica (nessuna modifica)"
echo " upgrade — aggiorna installazione esistente"
echo " install — fresh install"
echo " --yes — batch (nessuna conferma)"
exit 1
;;
esac
POSTDEPLOY
chmod +x post-deploy.sh
echo " OK"
echo "==> Creazione archivio: ${ARCHIVE}"
tar czf "${ARCHIVE}" \
@@ -60,6 +277,8 @@ tar czf "${ARCHIVE}" \
--warning=no-file-changed \
.
rm -f post-deploy.sh
echo ""
echo "============================================"
echo " Archivio creato: ${ARCHIVE}"
@@ -70,33 +289,14 @@ echo "Per estrarre sul server di destinazione:"
echo " tar xzf ${ARCHIVE}"
echo ""
echo "Poi esegui:"
echo " # 0. Diagnostica preliminare"
echo " php diagnose.php"
echo " # Upgrade di un'installazione esistente:"
echo " bash post-deploy.sh upgrade"
echo ""
echo " # 1. Crea directory necessarie (se non esistono già)"
echo ' mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views storage/logs bootstrap/cache storage/app/public storage/app/public/logos storage/app/public/documenti/eventi storage/app/backups storage/app/documenti storage/app/private/avatars/gruppi'
echo " # Fresh install:"
echo " bash post-deploy.sh install"
echo ""
echo " # 2. Crea file .gitignore in storage/app/public (serve a Laravel)"
echo ' echo "*" > storage/app/public/.gitignore'
echo ' echo "!.gitignore" >> storage/app/public/.gitignore'
echo " # Solo controllo (nessuna modifica):"
echo " bash post-deploy.sh check"
echo ""
echo " # 3. Permessi (web server = www-data)"
echo ' chmod -R 775 storage bootstrap/cache'
echo ' chown -R www-data:www-data storage bootstrap/cache'
echo ""
echo " # 4. Configura ambiente"
echo " cp .env.example .env # modifica DB, APP_URL, etc."
echo " php artisan key:generate"
echo ""
echo " # 5. Pulisci cache e ricrea symlink storage (relativo per portabilità)"
echo ' rm -f public/storage'
echo ' ln -sf ../storage/app/public public/storage'
echo " php artisan config:clear"
echo " php artisan route:clear"
echo " php artisan view:clear"
echo ""
echo " # 6. Avvia installazione MySQL"
echo " php install.php"
echo ""
echo " # 7. Verifica finale"
echo " php diagnose.php"
echo " # Batch mode (nessuna conferma):"
echo " bash post-deploy.sh upgrade --yes"
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('email_settings')) {
return;
}
if (!Schema::hasColumn('email_settings', 'from_email')) {
Schema::table('email_settings', function (Blueprint $table) {
$table->string('from_email')->nullable()->after('email_name');
});
}
if (!Schema::hasColumn('email_settings', 'from_name')) {
Schema::table('email_settings', function (Blueprint $table) {
$table->string('from_name')->nullable()->after('from_email');
});
}
}
public function down(): void
{
Schema::table('email_settings', function (Blueprint $table) {
$table->dropColumn(['from_email', 'from_name']);
});
}
};
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('gruppo_contatti')) {
Schema::create('gruppo_contatti', function (Blueprint $table) {
$table->id();
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
$table->string('tipo');
$table->string('valore');
$table->string('etichetta')->nullable();
$table->boolean('is_primary')->default(false);
$table->timestamps();
$table->index('gruppo_id');
});
}
}
public function down(): void
{
Schema::dropIfExists('gruppo_contatti');
}
};
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('diocesi_gruppo')) {
Schema::create('diocesi_gruppo', function (Blueprint $table) {
$table->id();
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
$table->foreignId('diocesi_id')->constrained('diocesi')->onDelete('cascade');
$table->unique(['gruppo_id', 'diocesi_id']);
});
}
}
public function down(): void
{
Schema::dropIfExists('diocesi_gruppo');
}
};
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('firme', function (Blueprint $table) {
$table->id();
$table->foreignId('email_setting_id')->constrained()->cascadeOnDelete();
$table->string('nome', 255);
$table->longText('contenuto_html')->nullable();
$table->boolean('is_default')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('firme');
}
};
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('mailing_lists', function (Blueprint $table) {
$table->foreignId('firma_id')->nullable()->constrained('firme')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('mailing_lists', function (Blueprint $table) {
$table->dropForeign(['firma_id']);
$table->dropColumn('firma_id');
});
}
};
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('mailing_messaggi', function (Blueprint $table) {
$table->foreignId('firma_id')->nullable()->constrained('firme')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('mailing_messaggi', function (Blueprint $table) {
$table->dropForeign(['firma_id']);
$table->dropColumn('firma_id');
});
}
};
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('sessions')) {
Schema::create('sessions', function (Blueprint $table): void {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
}
public function down(): void
{
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('mailing_lists', function (Blueprint $table) {
$table->foreignId('sender_account_id')->nullable()->constrained('sender_accounts')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('mailing_lists', function (Blueprint $table) {
$table->dropForeign(['sender_account_id']);
$table->dropColumn('sender_account_id');
});
}
};
+243 -138
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
use Illuminate\Database\Seeder;
@@ -9,147 +11,250 @@ class DiocesiSeeder extends Seeder
{
public function run(): void
{
$fixes = [
50 => 'Abbazia territoriale della Santissima Trinità di Cava de Tirreni',
51 => 'Diocesi di Cefalù',
58 => 'Diocesi di Città di Castello',
79 => 'Diocesi di Forlì-Bertinoro',
121 => 'Diocesi di Mondovì',
128 => 'Diocesi di Nardò-Gallipoli',
148 => 'Arcidiocesi di Perugia-Città della Pieve',
185 => "Arcidiocesi di Siena-Colle di Val d'Elsa-Montalcino",
];
foreach ($fixes as $id => $correctNome) {
Diocesi::where('id', $id)->update(['nome' => $correctNome]);
}
$diocesi = [
['nome' => 'Acqui Terme', 'regione' => 'Piemonte'],
['nome' => 'Afragola', 'regione' => 'Campania'],
['nome' => 'Agrigento', 'regione' => 'Sicilia'],
['nome' => 'Alba', 'regione' => 'Piemonte'],
['nome' => 'Alessandria', 'regione' => 'Piemonte'],
['nome' => 'Ancona', 'regione' => 'Marche'],
['nome' => 'Aosta', 'regione' => 'Valle d\'Aosta'],
['nome' => 'Aquila', 'regione' => 'Abruzzo'],
['nome' => 'Arezzo', 'regione' => 'Toscana'],
['nome' => 'Ascoli Piceno', 'regione' => 'Marche'],
['nome' => 'Assisi', 'regione' => 'Umbria'],
['nome' => 'Asti', 'regione' => 'Piemonte'],
['nome' => 'Avellino', 'regione' => 'Campania'],
['nome' => 'Bari', 'regione' => 'Puglia'],
['nome' => 'Barletta', 'regione' => 'Puglia'],
['nome' => 'Belluno', 'regione' => 'Veneto'],
['nome' => 'Benevento', 'regione' => 'Campania'],
['nome' => 'Bergamo', 'regione' => 'Lombardia'],
['nome' => 'Biella', 'regione' => 'Piemonte'],
['nome' => 'Bologna', 'regione' => 'Emilia-Romagna'],
['nome' => 'Bolzano', 'regione' => 'Trentino-Alto Adige'],
['nome' => 'Brescia', 'regione' => 'Lombardia'],
['nome' => 'Brindisi', 'regione' => 'Puglia'],
['nome' => 'Cagliari', 'regione' => 'Sardegna'],
['nome' => 'Caltanissetta', 'regione' => 'Sicilia'],
['nome' => 'Campobasso', 'regione' => 'Molise'],
['nome' => 'Caserta', 'regione' => 'Campania'],
['nome' => 'Cassano allo Ionio', 'regione' => 'Calabria'],
['nome' => 'Castelvetrano', 'regione' => 'Sicilia'],
['nome' => 'Catania', 'regione' => 'Sicilia'],
['nome' => 'Catanzaro', 'regione' => 'Calabria'],
['nome' => 'Cefalù', 'regione' => 'Sicilia'],
['nome' => 'Cerro al Lambro', 'regione' => 'Lombardia'],
['nome' => 'Cesena', 'regione' => 'Emilia-Romagna'],
['nome' => 'Chieti', 'regione' => 'Abruzzo'],
['nome' => 'Civitavecchia', 'regione' => 'Lazio'],
['nome' => 'Cosenza', 'regione' => 'Calabria'],
['nome' => 'Cremona', 'regione' => 'Lombardia'],
['nome' => 'Crotone', 'regione' => 'Calabria'],
['nome' => 'Cuneo', 'regione' => 'Piemonte'],
['nome' => 'Donegal', 'regione' => 'Irlanda'],
['nome' => 'Enna', 'regione' => 'Sicilia'],
['nome' => 'Fano', 'regione' => 'Marche'],
['nome' => 'Fermo', 'regione' => 'Marche'],
['nome' => 'Ferrara', 'regione' => 'Emilia-Romagna'],
['nome' => 'Firenze', 'regione' => 'Toscana'],
['nome' => 'Foggia', 'regione' => 'Puglia'],
['nome' => 'Foligno', 'regione' => 'Umbria'],
['nome' => 'Forlì', 'regione' => 'Emilia-Romagna'],
['nome' => 'Frosinone', 'regione' => 'Lazio'],
['nome' => 'Genova', 'regione' => 'Liguria'],
['nome' => 'Gorizia', 'regione' => 'Friuli-Venezia Giulia'],
['nome' => 'Grosseto', 'regione' => 'Toscana'],
['nome' => 'Gubbio', 'regione' => 'Umbria'],
['nome' => 'Imola', 'regione' => 'Emilia-Romagna'],
['nome' => 'Isernia', 'regione' => 'Molise'],
['nome' => 'L\'Aquila', 'regione' => 'Abruzzo'],
['nome' => 'La Spezia', 'regione' => 'Liguria'],
['nome' => 'Lamezia Terme', 'regione' => 'Calabria'],
['nome' => 'Latina', 'regione' => 'Lazio'],
['nome' => 'Lecce', 'regione' => 'Puglia'],
['nome' => 'Lecco', 'regione' => 'Lombardia'],
['nome' => 'Livorno', 'regione' => 'Toscana'],
['nome' => 'Lodi', 'regione' => 'Lombardia'],
['nome' => 'Lucca', 'regione' => 'Toscana'],
['nome' => 'Lunedì', 'regione' => 'Marche'],
['nome' => 'Macerata', 'regione' => 'Marche'],
['nome' => 'Mantova', 'regione' => 'Lombardia'],
['nome' => 'Massa', 'regione' => 'Toscana'],
['nome' => 'Matera', 'regione' => 'Basilicata'],
['nome' => 'Messina', 'regione' => 'Sicilia'],
['nome' => 'Milano', 'regione' => 'Lombardia'],
['nome' => 'Modena', 'regione' => 'Emilia-Romagna'],
['nome' => 'Monaco', 'regione' => 'Germania'],
['nome' => 'Mongolia', 'regione' => 'Asia'],
['nome' => 'Monreale', 'regione' => 'Sicilia'],
['nome' => 'Montefeltro', 'regione' => 'Marche'],
['nome' => 'Montepulciano', 'regione' => 'Toscana'],
['nome' => 'Monza', 'regione' => 'Lombardia'],
['nome' => 'Napoli', 'regione' => 'Campania'],
['nome' => 'Novara', 'regione' => 'Piemonte'],
['nome' => 'Nuoro', 'regione' => 'Sardegna'],
['nome' => 'Oristano', 'regione' => 'Sardegna'],
['nome' => 'Otranto', 'regione' => 'Puglia'],
['nome' => 'Padova', 'regione' => 'Veneto'],
['nome' => 'Palermo', 'regione' => 'Sicilia'],
['nome' => 'Parma', 'regione' => 'Emilia-Romagna'],
['nome' => 'Pavia', 'regione' => 'Lombardia'],
['nome' => 'Perugia', 'regione' => 'Umbria'],
['nome' => 'Pesaro', 'regione' => 'Marche'],
['nome' => 'Piacenza', 'regione' => 'Emilia-Romagna'],
['nome' => 'Pisa', 'regione' => 'Toscana'],
['nome' => 'Pistoia', 'regione' => 'Toscana'],
['nome' => 'Pordenone', 'regione' => 'Friuli-Venezia Giulia'],
['nome' => 'Potenza', 'regione' => 'Basilicata'],
['nome' => 'Prato', 'regione' => 'Toscana'],
['nome' => 'Ragusa', 'regione' => 'Sicilia'],
['nome' => 'Ravenna', 'regione' => 'Emilia-Romagna'],
['nome' => 'Reggio Calabria', 'regione' => 'Calabria'],
['nome' => 'Reggio Emilia', 'regione' => 'Emilia-Romagna'],
['nome' => 'Rieti', 'regione' => 'Lazio'],
['nome' => 'Rimini', 'regione' => 'Emilia-Romagna'],
['nome' => 'Roma', 'regione' => 'Lazio'],
['nome' => 'Rosario', 'regione' => 'Argentina'],
['nome' => 'Rovigo', 'regione' => 'Veneto'],
['nome' => 'Salerno', 'regione' => 'Campania'],
['nome' => 'San Severo', 'regione' => 'Puglia'],
['nome' => 'Sassari', 'regione' => 'Sardegna'],
['nome' => 'Savona', 'regione' => 'Liguria'],
['nome' => 'Siena', 'regione' => 'Toscana'],
['nome' => 'Siracusa', 'regione' => 'Sicilia'],
['nome' => 'Sondrio', 'regione' => 'Lombardia'],
['nome' => 'Spezia', 'regione' => 'Liguria'],
['nome' => 'Spoleto', 'regione' => 'Umbria'],
['nome' => 'Syracuse', 'regione' => 'USA'],
['nome' => 'Taranto', 'regione' => 'Puglia'],
['nome' => 'Tempio Ampurias', 'regione' => 'Sardegna'],
['nome' => 'Teramo', 'regione' => 'Abruzzo'],
['nome' => 'Terni', 'regione' => 'Umbria'],
['nome' => 'Tirana', 'regione' => 'Albania'],
['nome' => 'Torino', 'regione' => 'Piemonte'],
['nome' => 'Tortona', 'regione' => 'Piemonte'],
['nome' => 'Trani', 'regione' => 'Puglia'],
['nome' => 'Trapani', 'regione' => 'Sicilia'],
['nome' => 'Trento', 'regione' => 'Trentino-Alto Adige'],
['nome' => 'Treviso', 'regione' => 'Veneto'],
['nome' => 'Trieste', 'regione' => 'Friuli-Venezia Giulia'],
['nome' => 'Udine', 'regione' => 'Friuli-Venezia Giulia'],
['nome' => 'Urbino', 'regione' => 'Marche'],
['nome' => 'Vaticano', 'regione' => 'Lazio'],
['nome' => 'Vercelli', 'regione' => 'Piemonte'],
['nome' => 'Verona', 'regione' => 'Veneto'],
['nome' => 'Vibo Valentia', 'regione' => 'Calabria'],
['nome' => 'Vicenza', 'regione' => 'Veneto'],
['nome' => 'Viterbo', 'regione' => 'Lazio'],
['nome' => 'Vittorio Veneto', 'regione' => 'Veneto'],
'Arcidiocesi di Acerenza',
'Diocesi di Acerra',
'Diocesi di Acireale',
'Diocesi di Acqui',
'Diocesi di Adria-Rovigo',
'Arcidiocesi di Agrigento',
'Diocesi di Alba',
'Sede suburbicaria di Albano',
'Diocesi di Albenga-Imperia',
'Diocesi di Ales-Terralba',
'Diocesi di Alessandria',
'Diocesi di Alghero-Bosa',
'Diocesi di Alife-Caiazzo',
'Diocesi di Altamura-Gravina-Acquaviva delle Fonti',
'Arcidiocesi di Amalfi-Cava dei Tirreni',
'Diocesi di Anagni-Alatri',
'Arcidiocesi di Ancona-Osimo',
'Diocesi di Andria',
'Diocesi di Aosta',
'Diocesi di Arezzo-Cortona-Sansepolcro',
'Diocesi di Ariano Irpino-Lacedonia',
'Diocesi di Ascoli Piceno',
'Diocesi di Assisi-Nocera Umbra-Gualdo Tadino',
'Diocesi di Asti',
'Diocesi di Avellino',
'Diocesi di Aversa',
'Diocesi di Avezzano',
'Arcidiocesi di Bari-Bitonto',
'Diocesi di Belluno-Feltre',
'Arcidiocesi di Benevento',
'Diocesi di Bergamo',
'Diocesi di Biella',
'Arcidiocesi di Bologna',
'Diocesi di Bolzano-Bressanone',
'Diocesi di Brescia',
'Arcidiocesi di Brindisi-Ostuni',
'Arcidiocesi di Cagliari',
'Diocesi di Caltagirone',
'Diocesi di Caltanissetta',
'Arcidiocesi di Camerino-San Severino Marche',
'Arcidiocesi di Campobasso-Boiano',
'Arcidiocesi di Capua',
'Diocesi di Carpi',
'Diocesi di Casale Monferrato',
'Diocesi di Caserta',
'Diocesi di Cassano allo Ionio',
'Diocesi di Castellaneta',
'Arcidiocesi di Catania',
'Arcidiocesi di Catanzaro-Squillace',
'Abbazia territoriale della Santissima Trinità di Cava de Tirreni',
'Diocesi di Cefalù',
'Diocesi di Cerignola-Ascoli Satriano',
'Diocesi di Cerreto Sannita-Telese-Sant\'Agata de Goti',
'Diocesi di Cesena-Sarsina',
'Diocesi di Chiavari',
'Arcidiocesi di Chieti-Vasto',
'Diocesi di Chioggia',
'Diocesi di Città di Castello',
'Diocesi di Civita Castellana',
'Diocesi di Civitavecchia-Tarquinia',
'Diocesi di Como',
'Diocesi di Concordia-Pordenone',
'Diocesi di Conversano-Monopoli',
'Arcidiocesi di Cosenza-Bisignano',
'Diocesi di Crema',
'Diocesi di Cremona',
'Arcidiocesi di Crotone-Santa Severina',
'Diocesi di Cuneo',
'Diocesi di Fabriano-Matelica',
'Diocesi di Faenza-Modigliana',
'Diocesi di Fano-Fossombrone-Cagli-Pergola',
'Arcidiocesi di Fermo',
'Arcidiocesi di Ferrara-Comacchio',
'Diocesi di Fidenza',
'Diocesi di Fiesole',
'Arcidiocesi di Firenze',
'Arcidiocesi di Foggia-Bovino',
'Diocesi di Foligno',
'Diocesi di Forlì-Bertinoro',
'Diocesi di Fossano',
'Sede suburbicaria di Frascati',
'Diocesi di Frosinone-Veroli-Ferentino',
'Arcidiocesi di Gaeta',
'Arcidiocesi di Genova',
'Arcidiocesi di Gorizia',
'Diocesi di Grosseto',
'Diocesi di Gubbio',
'Diocesi di Iglesias',
'Diocesi di Imola',
'Diocesi di Ischia',
'Diocesi di Isernia-Venafro',
'Diocesi di Ivrea',
'Diocesi di Jesi',
'Diocesi della Spezia-Sarzana-Brugnato',
'Diocesi di Lamezia Terme',
'Arcidiocesi di Lanciano-Ortona',
'Diocesi di Lanusei',
'Arcidiocesi dell\'Aquila',
'Diocesi di Latina-Terracina-Sezze-Priverno',
'Arcidiocesi di Lecce',
'Diocesi di Livorno',
'Diocesi di Locri-Gerace',
'Diocesi di Lodi',
'Prelatura territoriale di Loreto',
'Arcidiocesi di Lucca',
'Diocesi di Lucera-Troia',
'Eparchia di Lungro',
'Diocesi di Macerata-Tolentino-Recanati-Cingoli-Treia',
'Arcidiocesi di Manfredonia-Vieste-San Giovanni Rotondo',
'Diocesi di Mantova',
'Diocesi di Massa Carrara-Pontremoli',
'Diocesi di Massa Marittima-Piombino',
'Arcidiocesi di Matera-Irsina',
'Diocesi di Mazara del Vallo',
'Diocesi di Melfi-Rapolla-Venosa',
'Arcidiocesi di Messina-Lipari-Santa Lucia del Mela',
'Arcidiocesi di Milano',
'Diocesi di Mileto-Nicotera-Tropea',
'Arcidiocesi di Modena-Nonantola',
'Diocesi di Molfetta-Ruvo-Giovinazzo-Terlizzi',
'Diocesi di Mondovì',
'Arcidiocesi di Monreale',
'Abbazia territoriale di Montecassino',
'Abbazia territoriale di Monte Oliveto Maggiore',
'Diocesi di Montepulciano-Chiusi-Pienza',
'Abbazia territoriale di Montevergine',
'Arcidiocesi di Napoli',
'Diocesi di Nardò-Gallipoli',
'Diocesi di Nicosia',
'Diocesi di Nocera Inferiore-Sarno',
'Diocesi di Nola',
'Diocesi di Noto',
'Diocesi di Novara',
'Diocesi di Nuoro',
'Diocesi di Oppido Mamertina-Palmi',
'Diocesi di Oria',
'Arcidiocesi di Oristano',
'Diocesi di Orvieto-Todi',
'Sede suburbicaria di Ostia',
'Arcidiocesi di Otranto',
'Diocesi di Ozieri',
'Diocesi di Padova',
'Arcidiocesi di Palermo',
'Sede suburbicaria di Palestrina',
'Diocesi di Parma',
'Diocesi di Patti',
'Diocesi di Pavia',
'Arcidiocesi di Perugia-Città della Pieve',
'Arcidiocesi di Pesaro',
'Arcidiocesi di Pescara-Penne',
'Diocesi di Pescia',
'Eparchia di Piana degli Albanesi',
'Diocesi di Piazza Armerina',
'Diocesi di Pinerolo',
'Arcidiocesi di Pisa',
'Diocesi di Pistoia',
'Diocesi di Pitigliano-Sovana-Orbetello',
'Prelatura territoriale di Pompei',
'Sede suburbicaria di Porto-Santa Rufina',
'Arcidiocesi di Potenza-Muro Lucano-Marsico Nuovo',
'Diocesi di Pozzuoli',
'Diocesi di Prato',
'Diocesi di Ragusa',
'Arcidiocesi di Ravenna-Cervia',
'Arcidiocesi di Reggio Calabria-Bova',
'Diocesi di Reggio Emilia-Guastalla',
'Diocesi di Rieti',
'Diocesi di Rimini',
'Diocesi di Roma',
'Arcidiocesi di Rossano-Cariati',
'Sede suburbicaria di Sabina-Poggio Mirteto',
'Arcidiocesi di Salerno-Campagna-Acerno',
'Diocesi di Saluzzo',
'Diocesi di San Benedetto del Tronto-Ripatransone-Montalto',
'Diocesi di San Marco Argentano-Scalea',
'Diocesi di San Marino-Montefeltro',
'Diocesi di San Miniato',
'Diocesi di San Severo',
'Abbazia territoriale di Santa Maria di Grottaferrata',
'Arcidiocesi di Sant\'Angelo dei Lombardi-Conza-Nusco-Bisaccia',
'Arcidiocesi di Sassari',
'Diocesi di Savona-Noli',
'Diocesi di Senigallia',
'Diocesi di Sessa Aurunca',
'Arcidiocesi di Siena-Colle di Val d\'Elsa-Montalcino',
'Arcidiocesi di Siracusa',
'Diocesi di Sora-Cassino-Aquino-Pontecorvo',
'Arcidiocesi di Sorrento-Castellammare di Stabia',
'Arcidiocesi di Spoleto-Norcia',
'Abbazia territoriale di Subiaco',
'Diocesi di Sulmona-Valva',
'Diocesi di Susa',
'Arcidiocesi di Taranto',
'Diocesi di Teano-Calvi',
'Diocesi di Teggiano-Policastro',
'Diocesi di Tempio-Ampurias',
'Diocesi di Teramo-Atri',
'Diocesi di Termoli-Larino',
'Diocesi di Terni-Narni-Amelia',
'Diocesi di Tivoli',
'Arcidiocesi di Torino',
'Diocesi di Tortona',
'Arcidiocesi di Trani-Barletta-Bisceglie-Nazareth',
'Diocesi di Trapani',
'Arcidiocesi di Trento',
'Diocesi di Treviso',
'Diocesi di Tricarico',
'Diocesi di Trieste',
'Diocesi di Trivento',
'Diocesi di Tursi-Lagonegro',
'Arcidiocesi di Udine',
'Diocesi di Ugento-Santa Maria di Leuca',
'Arcidiocesi di Urbino-Urbania-Sant\'Angelo in Vado',
'Diocesi di Vallo della Lucania',
'Sede suburbicaria di Velletri-Segni',
'Patriarcato di Venezia',
'Diocesi di Ventimiglia-Sanremo',
'Arcidiocesi di Vercelli',
'Diocesi di Verona',
'Diocesi di Vicenza',
'Diocesi di Vigevano',
'Diocesi di Viterbo',
'Diocesi di Vittorio Veneto',
'Diocesi di Volterra',
'Diocesi di Piacenza-Bobbio',
];
foreach ($diocesi as $d) {
Diocesi::firstOrCreate(['nome' => $d['nome']], $d);
foreach ($diocesi as $nome) {
Diocesi::firstOrCreate(['nome' => $nome]);
}
}
}
+662 -297
View File
File diff suppressed because it is too large Load Diff
Executable
+209
View File
@@ -0,0 +1,209 @@
#!/bin/bash
# post-deploy.sh — Generated by build-dist.sh
# Usage: bash post-deploy.sh {check|upgrade|install} [--yes]
set -u
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
ARTISAN="php ${APP_DIR}/artisan"
DIAGNOSE="php ${APP_DIR}/diagnose.php"
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'
CYAN='\033[36m'; BOLD='\033[1m'; NC='\033[0m'
MODE="${1:-check}"
BATCH=false; [ "${2:-}" = "--yes" ] && BATCH=true
echo -e "${CYAN}${BOLD}============================================${NC}"
echo -e "${CYAN}${BOLD} Glastree Post-Deploy — ${MODE}${NC}"
echo -e "${CYAN}${BOLD}============================================${NC}"
echo ""
IS_FRESH=false; [ ! -f "${APP_DIR}/.env" ] && IS_FRESH=true
log() { echo -e " ${BOLD}$1${NC}"; }
ok() { echo -e " ${GREEN}$1${NC}"; }
warn() { echo -e " ${YELLOW}$1${NC}"; }
fail() { echo -e " ${RED}$1${NC}"; }
check_artisan() {
if [ ! -f "${APP_DIR}/artisan" ]; then
fail "artisan non trovato. Esegui da root dell'applicazione."
exit 1
fi
}
run_step() {
local step="$1" desc="$2"; shift 2
log "${step} ${desc}"
if "$@" 2>&1 | sed 's/^/ /'; then
ok "${desc}"
return 0
else
local rc=$?
fail "${desc} — exit code ${rc}"
return ${rc}
fi
}
run_diagnose() {
if ${DIAGNOSE} 2>&1 | tail -5; then
ok "Diagnostica OK"
else
warn "Diagnostica ha rilevato problemi (non bloccante)"
fi
}
run_check() {
echo -e "${YELLOW}═══════════ CONTROLLO ═══════════${NC}" ; echo ""
echo -e "${BOLD}[0] Diagnostica sistema${NC}"
run_diagnose ; echo ""
echo -e "${BOLD}[1] Ambiente${NC}"
if $IS_FRESH; then warn ".env non trovato — fresh install necessario"
else ok ".env presente"
fi ; echo ""
echo -e "${BOLD}[2] Migration pendenti${NC}"
if [ -f "${APP_DIR}/artisan" ]; then
$ARTISAN migrate:status 2>&1 | grep -E '(Pending|Ran)' | head -5 || true
PENDING=$(php -r "echo trim(shell_exec('${ARTISAN} migrate:status 2>&1 | grep -c Pending || true'));" 2>/dev/null || echo "0")
if [ "${PENDING:-0}" -gt 0 ] 2>/dev/null; then
warn "${PENDING} migration da eseguire"
else
ok "Nessuna migration pendente"
fi
else
warn "artisan non disponibile — skip migration check"
fi ; echo ""
echo -e "${BOLD}[3] Storage & Permessi${NC}"
for d in storage/framework/cache/data storage/framework/sessions \
storage/framework/views storage/logs bootstrap/cache \
storage/app/public storage/app/backups; do
p="${APP_DIR}/${d}"
if [ -d "$p" ]; then
[ -w "$p" ] && ok "${d}" || warn "${d} — non scrivibile"
else
warn "${d} — mancante"
fi
done ; echo ""
echo -e "${BOLD}[4] Symlink storage${NC}"
if [ -L "${APP_DIR}/public/storage" ]; then
ok "public/storage → $(readlink "${APP_DIR}/public/storage")"
else
warn "public/storage non presente"
fi ; echo ""
echo -e "${BOLD}[5] Cache${NC}"
for d in bootstrap/cache storage/framework/cache/data \
storage/framework/sessions storage/framework/views; do
c=$(find "${APP_DIR}/${d}" -type f 2>/dev/null | wc -l)
[ "$c" -gt 0 ] && warn "${d}: ${c} file" || ok "${d}: vuoto"
done
echo "" ; echo -e "${YELLOW}═══════════ FINE CONTROLLO ═══════════${NC}"
}
run_upgrade() {
echo -e "${YELLOW}═══════════ UPGRADE ═══════════${NC}" ; check_artisan
run_step "[1/9]" "Directory storage" mkdir -p \
"${APP_DIR}/storage/framework/cache/data" \
"${APP_DIR}/storage/framework/sessions" \
"${APP_DIR}/storage/framework/views" \
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
"${APP_DIR}/storage/app/public" \
"${APP_DIR}/storage/app/public/logos" \
"${APP_DIR}/storage/app/public/documenti/eventi" \
"${APP_DIR}/storage/app/backups" \
"${APP_DIR}/storage/app/documenti" \
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
run_step "[2/9]" ".gitignore storage" bash -c \
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
run_step "[3/9]" "Permessi" chmod -R 775 \
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
run_step "[4/9]" "Symlink storage" bash -c \
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
run_step "[5/10]" "Migrazioni DB" $ARTISAN migrate --force || true
run_step "[6/10]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
run_step "[7/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
run_step "[8/10]" "Key generate" $ARTISAN key:generate --force || true
run_step "[9/10]" "Pulizia cache" bash -c \
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
run_step "[10/10]" "Verifica finale" $DIAGNOSE || true
echo "" ; echo -e "${GREEN}${BOLD}✅ Upgrade completato.${NC}"
}
run_install() {
echo -e "${YELLOW}═══════════ INSTALL (fresh) ═══════════${NC}" ; check_artisan
run_step "[1/10]" "Directory storage" mkdir -p \
"${APP_DIR}/storage/framework/cache/data" \
"${APP_DIR}/storage/framework/sessions" \
"${APP_DIR}/storage/framework/views" \
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
"${APP_DIR}/storage/app/public" \
"${APP_DIR}/storage/app/public/logos" \
"${APP_DIR}/storage/app/public/documenti/eventi" \
"${APP_DIR}/storage/app/backups" \
"${APP_DIR}/storage/app/documenti" \
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
run_step "[2/10]" ".gitignore storage" bash -c \
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
run_step "[3/10]" "Permessi" chmod -R 775 \
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
run_step "[4/10]" "File .env" cp "${APP_DIR}/.env.example" "${APP_DIR}/.env" || true
warn "MODIFICA .env con i tuoi dati DB/URL prima di proseguire"
if ! $BATCH; then read -p " Fatto? (Invio per continuare) " _; fi
run_step "[5/10]" "Symlink storage" bash -c \
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
run_step "[6/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
run_step "[7/10]" "Key generate" $ARTISAN key:generate --force || true
run_step "[8/11]" "Migrazioni DB" $ARTISAN migrate --force || true
run_step "[9/11]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
run_step "[10/11]" "Pulizia cache" bash -c \
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
run_step "[11/11]" "Verifica finale" $DIAGNOSE || true
echo "" ; echo -e "${GREEN}${BOLD}✅ Installazione completata.${NC}"
}
case "$MODE" in
check) run_check ;;
upgrade)
run_check ; echo ""
if $BATCH; then run_upgrade
else
read -p "Procedere con l'upgrade? (s/N) " c
[ "$c" = "s" ] || [ "$c" = "S" ] && run_upgrade || echo "Annullato."
fi
;;
install) run_install ;;
*)
echo "Uso: bash post-deploy.sh {check|upgrade|install} [--yes]"
echo " check — diagnostica (nessuna modifica)"
echo " upgrade — aggiorna installazione esistente"
echo " install — fresh install"
echo " --yes — batch (nessuna conferma)"
exit 1
;;
esac
@@ -46,9 +46,20 @@
<td>{{ $backup['size_formatted'] }}</td>
<td>
@if($backup['manifest'])
@php $components = $backup['manifest']['included_components'] ?? ['database']; @endphp
<span class="badge badge-info" title="PHP {{ $backup['manifest']['php_version'] ?? '?' }}, Laravel {{ $backup['manifest']['laravel_version'] ?? '?' }}">
<i class="fas fa-database mr-1"></i>DB
</span>
@if(in_array('env', $components))
<span class="badge badge-secondary" title="Configurazione .env">
<i class="fas fa-cog mr-1"></i>.env
</span>
@endif
@if(in_array('files', $components))
<span class="badge badge-warning" title="File caricati">
<i class="fas fa-file mr-1"></i>Files
</span>
@endif
@if(isset($backup['manifest']['db_name']))
<small class="text-muted">{{ $backup['manifest']['db_name'] }}</small>
@endif
@@ -31,6 +31,9 @@
<a href="#signature" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-signature"></i> Firma
</a>
<a href="#firme" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-pen-fancy"></i> Firme Multiple
</a>
<a href="#mittenti" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-user-plus"></i> Mittenti Aggiuntivi
</a>
@@ -197,12 +200,32 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="from_email">Email Mittente Ufficiale <small class="text-muted">(se diversa dall'indirizzo IMAP)</small></label>
<input type="email" name="from_email" id="from_email" class="form-control"
value="{{ $settings->from_email ?? '' }}"
placeholder="es. ufficiale@esempio.it">
<small class="text-muted">Se impostata, viene usata come mittente e Reply-To nelle email in uscita</small>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="from_name">Nome Mittente Ufficiale</label>
<input type="text" name="from_name" id="from_name" class="form-control"
value="{{ $settings->from_name ?? '' }}"
placeholder="es. Nome Ufficiale">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="reply_to">Email Risposta (Reply-To)</label>
<input type="email" name="reply_to" id="reply_to" class="form-control"
value="{{ $settings->reply_to ?? '' }}">
<small class="text-muted">Usato solo se "Email Mittente Ufficiale" non è impostata</small>
</div>
</div>
</div>
@@ -284,6 +307,64 @@
</div>
</div>
<div class="tab-pane" id="firme">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Firme Multiple</h3>
<div class="card-tools">
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#firmaModal" onclick="resetFirmaForm()">
<i class="fas fa-plus mr-1"></i> Nuova Firma
</button>
</div>
</div>
<div class="card-body">
@if($firme->count() > 0)
<div class="table-responsive">
<table class="table table-bordered table-hover table-sm">
<thead>
<tr>
<th>Nome</th>
<th>Anteprima</th>
<th>Predefinita</th>
<th style="width: 140px;">Azioni</th>
</tr>
</thead>
<tbody>
@foreach($firme as $firma)
<tr>
<td><strong>{{ $firma->nome }}</strong></td>
<td><small class="text-muted">{!! Str::limit(strip_tags($firma->contenuto_html), 80) !!}</small></td>
<td>
@if($firma->is_default)
<span class="badge badge-success"><i class="fas fa-check"></i> Default</span>
@else
<form action="{{ route('impostazioni.firma.default', $firma->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Impostare questa firma come predefinita?')">
@csrf @method('PUT')
<button type="submit" class="btn btn-xs btn-outline-secondary">Imposta default</button>
</form>
@endif
</td>
<td>
<button type="button" class="btn btn-xs btn-info" onclick="editFirma({{ $firma->id }}, '{{ addslashes($firma->nome) }}', '{{ addslashes($firma->contenuto_html ?? '') }}')">
<i class="fas fa-edit"></i>
</button>
<form action="{{ route('impostazioni.firma.destroy', $firma->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Eliminare questa firma?')">
@csrf @method('DELETE')
<button type="submit" class="btn btn-xs btn-danger"><i class="fas fa-trash"></i></button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-muted">Nessuna firma multipla configurata. La firma predefinita nella scheda "Firma" verrà usata come fallback.</p>
@endif
</div>
</div>
</div>
<div class="tab-pane" id="mittenti">
<div class="card card-primary">
<div class="card-header">
@@ -513,9 +594,47 @@
</div>
</div>
<div class="modal fade" id="firmaModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="firmaForm" method="POST" action="{{ route('impostazioni.firma.store') }}">
@csrf
<input type="hidden" name="_method" id="firmaMethod" value="POST">
<input type="hidden" name="id" id="firmaId" value="">
<div class="modal-header bg-primary">
<h5 class="modal-title" id="firmaModalTitle"><i class="fas fa-pen-fancy mr-2"></i>Nuova Firma</h5>
<button type="button" class="close text-white" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="firma_nome">Nome *</label>
<input type="text" name="nome" id="firma_nome" class="form-control" required placeholder="Es: Default, Personale, Ufficio">
</div>
<div class="form-group">
<label>Contenuto Firma (HTML)</label>
<div id="firma-quill-editor" style="height: 200px;"></div>
<input type="hidden" name="contenuto_html" id="firma_contenuto_input" value="">
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="firma_is_default" name="is_default" value="1">
<label class="custom-control-label" for="firma_is_default">Imposta come predefinita</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary"><i class="fas fa-save mr-1"></i> Salva Firma</button>
</div>
</form>
</div>
</div>
</div>
@section('scripts')
<script>
let senderAccounts = @json($senderAccounts);
let firmaQuillInstance = null;
function resetSenderForm() {
document.getElementById('senderForm').action = '{{ route('impostazioni.sender.store') }}';
@@ -696,13 +815,89 @@ function createEditor() {
quillEditorInstance = quill;
}
function resetFirmaForm() {
document.getElementById('firmaForm').action = '{{ route('impostazioni.firma.store') }}';
document.getElementById('firmaMethod').value = 'POST';
document.getElementById('firmaModalTitle').textContent = 'Nuova Firma';
document.getElementById('firmaForm').reset();
document.getElementById('firmaId').value = '';
if (firmaQuillInstance) {
firmaQuillInstance.root.innerHTML = '';
}
document.getElementById('firma_contenuto_input').value = '';
}
function editFirma(id, nome, contenuto) {
document.getElementById('firmaForm').action = '{{ route('impostazioni.firma.update', '__ID__') }}'.replace('__ID__', id);
document.getElementById('firmaMethod').value = 'PUT';
document.getElementById('firmaModalTitle').textContent = 'Modifica Firma - ' + nome;
document.getElementById('firmaId').value = id;
document.getElementById('firma_nome').value = nome;
if (firmaQuillInstance && contenuto) {
firmaQuillInstance.root.innerHTML = contenuto;
document.getElementById('firma_contenuto_input').value = contenuto;
}
$('#firmaModal').modal('show');
}
function initFirmaQuill() {
var container = document.querySelector('#firma-quill-editor');
if (!container || firmaQuillInstance) return;
var quill = new Quill('#firma-quill-editor', {
theme: 'snow',
modules: {
toolbar: [
['bold', 'italic', 'underline'],
[{ 'color': [] }, { 'background': [] }],
[{ 'header': 1 }, { 'header': 2 }, { 'header': 3 }],
[{ 'align': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'image'],
['clean']
]
}
});
var input = document.getElementById('firma_contenuto_input');
quill.on('text-change', function() {
input.value = quill.root.innerHTML;
});
firmaQuillInstance = quill;
}
$('#firmaModal').on('shown.bs.modal', function () {
if (typeof Quill === 'undefined') {
var script = document.createElement('script');
script.src = 'https://cdn.quilljs.com/1.3.7/quill.min.js';
script.onload = function() {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
document.head.appendChild(link);
setTimeout(initFirmaQuill, 200);
};
document.head.appendChild(script);
} else {
setTimeout(initFirmaQuill, 100);
}
});
function initSignatureEditor() {
if (quillEditorInstance) return;
if (typeof Quill === 'undefined') {
var script = document.createElement('script');
script.src = 'https://cdn.quilljs.com/1.3.7/quill.min.js';
script.onload = createEditor;
script.onload = function() {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
document.head.appendChild(link);
setTimeout(createEditor, 200);
};
document.head.appendChild(script);
var link = document.createElement('link');
+15 -2
View File
@@ -17,7 +17,7 @@
<div class="form-group">
<label for="mittente_id">Da (mittente)</label>
<select name="mittente_id" id="mittente_id" class="form-control">
<option value="0">Sistema {{ optional(\App\Models\EmailSetting::getActive())->email_address ?? 'Configura email' }}</option>
<option value="0">Sistema {{ optional(\App\Models\EmailSetting::getActive())->getEffectiveFromAddress() ?? 'Configura email' }}</option>
@foreach($senderAccounts as $sender)
<option value="{{ $sender->id }}">{{ $sender->email_name ?: $sender->email_address }} &lt;{{ $sender->email_address }}&gt;</option>
@endforeach
@@ -45,7 +45,7 @@
<div id="individui-input" class="destinatari-section" style="display: none;">
<select name="individui[]" id="individui-select" class="form-control" multiple size="5">
@foreach($individui as $ind)
<option value="{{ $ind->id }}">{{ $ind->cognito }} {{ $ind->nome }} ({{ $ind->email_primaria ?? 'senza email' }})</option>
<option value="{{ $ind->id }}">{{ $ind->nome }} {{ $ind->cognome }} ({{ $ind->email_primaria ?? 'senza email' }})</option>
@endforeach
</select>
<small class="text-muted">Tieni premuto Ctrl per selezionare più individui</small>
@@ -84,6 +84,19 @@
<textarea name="body" id="body" class="form-control" rows="15" required>{{ $prefill['body'] ?? '' }}</textarea>
</div>
@if($firme->count() > 0)
<div class="form-group">
<label for="firma_id">Firma</label>
<select name="firma_id" id="firma_id" class="form-control">
<option value="">Nessuna</option>
@foreach($firme as $firma)
<option value="{{ $firma->id }}">{{ $firma->nome }}</option>
@endforeach
</select>
<small class="text-muted">Seleziona la firma da appender al messaggio.</small>
</div>
@endif
<div class="form-group">
<label>Allegati</label>
<div class="border rounded p-3 bg-light">
+108 -3
View File
@@ -2,6 +2,26 @@
@section('title', 'Nuovo Gruppo')
@section('page_title', 'Nuovo Gruppo')
@push('styles')
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap4-theme@1.0.0/dist/select2-bootstrap4.min.css" rel="stylesheet" />
<style>
.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice {
color: #333;
}
.select2-container--bootstrap4 .select2-selection--multiple .select2-search__field {
height: 28px !important;
min-height: 28px !important;
width: auto !important;
min-width: 30px;
border: none !important;
background: transparent !important;
margin: 0;
padding: 0 4px;
}
</style>
@endpush
@section('content')
<form action="/gruppi" method="POST">
@csrf
@@ -37,10 +57,9 @@
</div>
<div class="form-group">
<label>Diocesi</label>
<select name="diocesi_id" class="form-control">
<option value="">Seleziona...</option>
<select name="diocesi_ids[]" class="form-control select2-multi" multiple style="width: 100%;">
@foreach($diocesi as $d)
<option value="{{ $d->id }}" {{ old('diocesi_id') == $d->id ? 'selected' : '' }}>{{ $d->nome }}</option>
<option value="{{ $d->id }}" {{ in_array($d->id, old('diocesi_ids', [])) ? 'selected' : '' }}>{{ $d->nome }}</option>
@endforeach
</select>
</div>
@@ -89,6 +108,33 @@
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-address-book mr-2"></i>Contatti del Gruppo</h3>
<button type="button" class="btn btn-xs btn-success float-right" onclick="aggiungiContatto()">
<i class="fas fa-plus mr-1"></i> Aggiungi Contatto
</button>
</div>
<div class="card-body p-0">
<table class="table table-bordered mb-0" id="contatti-table">
<thead class="thead-light">
<tr>
<th style="width: 20%;">Tipo</th>
<th style="width: 40%;">Valore</th>
<th style="width: 25%;">Etichetta</th>
<th style="width: 80px;">Primario</th>
<th style="width: 50px;"></th>
</tr>
</thead>
<tbody id="contatti-tbody">
</tbody>
</table>
<div id="no-contatti-msg" class="text-center text-muted py-4">
<p class="mb-0">Nessun contatto. Clicca su "Aggiungi Contatto" per iniziare.</p>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-users mr-2"></i>Membri</h3>
@@ -138,10 +184,69 @@
@endsection
@section('scripts')
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
$('.select2-multi').select2({
theme: 'bootstrap4',
placeholder: 'Cerca diocesi...',
width: '100%',
sorter: function(data) {
return data.sort(function(a, b) {
if (a.selected && !b.selected) return -1;
if (!a.selected && b.selected) return 1;
return a.text.localeCompare(b.text, 'it');
});
},
language: {
noResults: function() { return 'Nessuna diocesi trovata'; },
searching: function() { return 'Ricerca...'; }
}
});
});
let contattiIndex = 0;
let membriIndex = 0;
const membriResponsabiliMap = new Map();
function aggiungiContatto() {
const tbody = document.getElementById('contatti-tbody');
const noMsg = document.getElementById('no-contatti-msg');
if (noMsg) noMsg.style.display = 'none';
const row = document.createElement('tr');
const idx = contattiIndex;
row.innerHTML = `
<td>
<select name="contatti[${idx}][tipo]" class="form-control" required>
<option value="">Seleziona...</option>
<option value="email">Email</option>
<option value="telefono">Telefono</option>
<option value="cellulare">Cellulare</option>
</select>
</td>
<td><input type="text" name="contatti[${idx}][valore]" class="form-control" placeholder="Es. info@example.com" required></td>
<td><input type="text" name="contatti[${idx}][etichetta]" class="form-control" placeholder="Es. Ufficio"></td>
<td class="text-center">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="contatto-primary-${idx}" name="contatti[${idx}][is_primary]" value="1">
<label class="custom-control-label" for="contatto-primary-${idx}"></label>
</div>
</td>
<td><button type="button" class="btn btn-danger btn-xs" onclick="rimuoviContatto(this)"><i class="fas fa-trash"></i></button></td>
`;
tbody.appendChild(row);
contattiIndex++;
}
function rimuoviContatto(btn) {
const row = btn.closest('tr');
row.remove();
if (document.getElementById('contatti-tbody').children.length === 0) {
document.getElementById('no-contatti-msg').style.display = 'block';
}
}
function aggiungiMembro() {
const tbody = document.getElementById('membri-tbody');
const noMsg = document.getElementById('no-membri-msg');
+131 -3
View File
@@ -2,6 +2,26 @@
@section('title', 'Modifica Gruppo')
@section('page_title', 'Modifica Gruppo')
@push('styles')
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap4-theme@1.0.0/dist/select2-bootstrap4.min.css" rel="stylesheet" />
<style>
.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice {
color: #333;
}
.select2-container--bootstrap4 .select2-selection--multiple .select2-search__field {
height: 28px !important;
min-height: 28px !important;
width: auto !important;
min-width: 30px;
border: none !important;
background: transparent !important;
margin: 0;
padding: 0 4px;
}
</style>
@endpush
@section('content')
<form action="/gruppi/{{ $gruppo->id }}" method="POST">
@csrf @method('PUT')
@@ -31,10 +51,9 @@
</div>
<div class="form-group">
<label>Diocesi</label>
<select name="diocesi_id" class="form-control">
<option value="">Seleziona...</option>
<select name="diocesi_ids[]" class="form-control select2-multi" multiple style="width: 100%;">
@foreach($diocesi as $d)
<option value="{{ $d->id }}" {{ old('diocesi_id', $gruppo->diocesi_id) == $d->id ? 'selected' : '' }}>{{ $d->nome }}</option>
<option value="{{ $d->id }}" {{ in_array($d->id, old('diocesi_ids', $selectedDiocesiIds ?? [])) ? 'selected' : '' }}>{{ $d->nome }}</option>
@endforeach
</select>
</div>
@@ -91,6 +110,55 @@
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-address-book mr-2"></i>Contatti del Gruppo</h3>
<button type="button" class="btn btn-xs btn-success float-right" onclick="aggiungiContatto()">
<i class="fas fa-plus mr-1"></i> Aggiungi Contatto
</button>
</div>
<div class="card-body p-0">
<table class="table table-bordered mb-0" id="contatti-table">
<thead class="thead-light">
<tr>
<th style="width: 20%;">Tipo</th>
<th style="width: 40%;">Valore</th>
<th style="width: 25%;">Etichetta</th>
<th style="width: 80px;">Primario</th>
<th style="width: 50px;"></th>
</tr>
</thead>
<tbody id="contatti-tbody">
@forelse(old('contatti', $gruppo->gruppoContatti) as $idx => $contatto)
@php $contattoVal = is_array($contatto) ? $contatto : $contatto->toArray(); @endphp
<tr>
<td>
<select name="contatti[{{ $idx }}][tipo]" class="form-control" required>
<option value="email" {{ ($contattoVal['tipo'] ?? '') === 'email' ? 'selected' : '' }}>Email</option>
<option value="telefono" {{ ($contattoVal['tipo'] ?? '') === 'telefono' ? 'selected' : '' }}>Telefono</option>
<option value="cellulare" {{ ($contattoVal['tipo'] ?? '') === 'cellulare' ? 'selected' : '' }}>Cellulare</option>
</select>
</td>
<td><input type="text" name="contatti[{{ $idx }}][valore]" class="form-control" value="{{ $contattoVal['valore'] ?? '' }}" required></td>
<td><input type="text" name="contatti[{{ $idx }}][etichetta]" class="form-control" value="{{ $contattoVal['etichetta'] ?? '' }}" placeholder="Es. Ufficio"></td>
<td class="text-center">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="contatto-primary-{{ $idx }}" name="contatti[{{ $idx }}][is_primary]" value="1" {{ !empty($contattoVal['is_primary']) ? 'checked' : '' }}>
<label class="custom-control-label" for="contatto-primary-{{ $idx }}"></label>
</div>
</td>
<td><button type="button" class="btn btn-danger btn-xs" onclick="rimuoviContatto(this)"><i class="fas fa-trash"></i></button></td>
</tr>
@empty
@endforelse
</tbody>
</table>
<div id="no-contatti-msg" class="text-center text-muted py-4" style="{{ $gruppo->gruppoContatti->count() > 0 ? 'display:none;' : '' }}">
<p class="mb-0">Nessun contatto. Clicca su "Aggiungi Contatto" per iniziare.</p>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-users mr-2"></i>Membri</h3>
@@ -352,7 +420,67 @@
@endsection
@section('scripts')
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
$('.select2-multi').select2({
theme: 'bootstrap4',
placeholder: 'Cerca diocesi...',
width: '100%',
sorter: function(data) {
return data.sort(function(a, b) {
if (a.selected && !b.selected) return -1;
if (!a.selected && b.selected) return 1;
return a.text.localeCompare(b.text, 'it');
});
},
language: {
noResults: function() { return 'Nessuna diocesi trovata'; },
searching: function() { return 'Ricerca...'; }
}
});
});
let contattiIndex = {{ old('contatti') ? count(old('contatti')) : $gruppo->gruppoContatti->count() }};
function aggiungiContatto() {
const tbody = document.getElementById('contatti-tbody');
const noMsg = document.getElementById('no-contatti-msg');
if (noMsg) noMsg.style.display = 'none';
const row = document.createElement('tr');
const idx = contattiIndex;
row.innerHTML = `
<td>
<select name="contatti[${idx}][tipo]" class="form-control" required>
<option value="">Seleziona...</option>
<option value="email">Email</option>
<option value="telefono">Telefono</option>
<option value="cellulare">Cellulare</option>
</select>
</td>
<td><input type="text" name="contatti[${idx}][valore]" class="form-control" placeholder="Es. info@example.com" required></td>
<td><input type="text" name="contatti[${idx}][etichetta]" class="form-control" placeholder="Es. Ufficio"></td>
<td class="text-center">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="contatto-primary-${idx}" name="contatti[${idx}][is_primary]" value="1">
<label class="custom-control-label" for="contatto-primary-${idx}"></label>
</div>
</td>
<td><button type="button" class="btn btn-danger btn-xs" onclick="rimuoviContatto(this)"><i class="fas fa-trash"></i></button></td>
`;
tbody.appendChild(row);
contattiIndex++;
}
function rimuoviContatto(btn) {
const row = btn.closest('tr');
row.remove();
if (document.getElementById('contatti-tbody').children.length === 0) {
document.getElementById('no-contatti-msg').style.display = 'block';
}
}
function showMembroForm() {
console.log('showMembroForm called');
var card = document.getElementById('membro-card');
+4 -4
View File
@@ -161,7 +161,7 @@ $entityType = $entityType ?? 'gruppi';
<td>{{ Str::limit($gruppo->descrizione, 50) ?: '-' }}</td>
@endif
@if(in_array('diocesi', $visibleColumns))
<td>{{ $gruppo->diocesi?->nome ?? '-' }}</td>
<td>{{ $gruppo->diocesi->count() > 0 ? $gruppo->diocesi->pluck('nome')->implode(', ') : '-' }}</td>
@endif
@if(in_array('livello', $visibleColumns))
<td><span class="badge badge-{{ ($gruppo->depth ?? 0) == 0 ? 'primary' : 'secondary' }}">{{ $gruppo->depth ?? 0 }}</span></td>
@@ -176,7 +176,7 @@ $entityType = $entityType ?? 'gruppi';
<td>
@php $resp = $gruppo->getResponsabili() @endphp
@if($resp->count() > 0)
{{ $resp->pluck('cognome')->implode(', ') }}
{{ $resp->map(fn($r) => $r->nome . ' ' . $r->cognome)->implode(', ') }}
@else
-
@endif
@@ -189,10 +189,10 @@ $entityType = $entityType ?? 'gruppi';
<td>{{ $gruppo->città_incontro ?: '-' }}</td>
@endif
@if(in_array('telefono', $visibleColumns))
<td>-</td>
<td>{{ $gruppo->telefono_primario ?? '-' }}</td>
@endif
@if(in_array('email', $visibleColumns))
<td>-</td>
<td>{{ $gruppo->email_primaria ?? '-' }}</td>
@endif
@if(in_array('tag', $visibleColumns))
<td>
@@ -20,7 +20,7 @@ $responsabili = $gruppo->getResponsabili();
</a>
<span class="badge badge-{{ ($gruppo->depth ?? 0) == 0 ? 'primary' : 'secondary' }} badge-level">{{ $gruppo->depth ?? 0 }}</span>
<small class="text-muted ml-2">
{{ $gruppo->diocesi?->nome ?? '' }}
{{ $gruppo->diocesi->count() > 0 ? $gruppo->diocesi->pluck('nome')->implode(', ') : '' }}
@if($responsabili->count() > 0)
· <i class="fas fa-user text-info"></i> {{ $responsabili->pluck('cognome')->implode(', ') }}
@endif
+117 -87
View File
@@ -26,7 +26,7 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
</div>
@endif
<div class="row">
<div class="col-md-4">
<div class="col-md-12">
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">
@@ -35,7 +35,9 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
</h3>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<div class="row">
<div class="col-md-6">
<table class="table table-sm table-borderless mb-0">
<tr>
<td style="width: 130px;"><strong>Path:</strong></td>
<td class="text-muted small">{{ $gruppo->full_path }}</td>
@@ -52,10 +54,22 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
</tr>
<tr>
<td><strong>Diocesi:</strong></td>
<td>{{ $gruppo->diocesi?->nome ?? '-' }}</td>
<td>
@if($gruppo->diocesi->count() > 0)
@foreach($gruppo->diocesi as $diocesi)
<span class="badge badge-secondary mr-1">{{ $diocesi->nome }}</span>
@endforeach
@else
<span class="text-muted">-</span>
@endif
</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-sm table-borderless mb-0">
<tr>
<td><strong>Responsabili:</strong></td>
<td style="width: 130px;"><strong>Responsabili:</strong></td>
<td>
@php $responsabili = $gruppo->getResponsabili() @endphp
@if($responsabili->count() > 0)
@@ -70,17 +84,64 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
@endif
</td>
</tr>
<tr>
<td><strong>Email:</strong></td>
<td>{{ $gruppo->email_primaria ?? '-' }}</td>
</tr>
<tr>
<td><strong>Telefono:</strong></td>
<td>{{ $gruppo->telefono_primario ?? '-' }}</td>
</tr>
@if($gruppo->gruppoContatti->count() > 0)
<tr>
<td><strong>Altri Contatti:</strong></td>
<td>
@foreach($gruppo->gruppoContatti as $contatto)
@if(!$contatto->is_primary || ($contatto->tipo !== 'email' && $contatto->tipo !== 'telefono' && $contatto->tipo !== 'cellulare'))
<div>
@if($contatto->etichetta)<small class="text-muted">{{ $contatto->etichetta }}:</small>@endif
<span>{{ $contatto->valore }}</span>
</div>
@endif
@endforeach
</td>
</tr>
@endif
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
</div>
</div>
</div>
@php $incontroEvento = $gruppo->eventi->where('is_incontro_gruppo', true)->first(); @endphp
<div class="row mt-3">
<div class="col-md-3">
<div class="card h-100">
<div class="card-header"><h3 class="card-title"><i class="fas fa-chart-pie mr-2"></i>Statistiche</h3></div>
<div class="card-body d-flex align-items-center">
<div class="row text-center w-100">
<div class="col-6">
<h2 class="mb-0 text-primary">{{ $gruppo->individui->count() }}</h2>
<small class="text-muted">Membri</small>
</div>
<div class="col-6">
<h2 class="mb-0 text-warning">{{ $gruppo->children->count() }}</h2>
<small class="text-muted">Sottogruppi</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-header"><h3 class="card-title"><i class="fas fa-map-marker-alt mr-2"></i>Luogo Incontro</h3></div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tr>
<td style="width: 100px;"><strong>Indirizzo:</strong></td>
<td style="width: 80px;"><strong>Indirizzo:</strong></td>
<td>{{ $gruppo->indirizzo_incontro ?: '-' }}</td>
</tr>
<tr>
@@ -94,77 +155,15 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
</table>
@if($gruppo->mappa_posizione)
<a href="{{ $gruppo->mappa_posizione }}" target="_blank" class="btn btn-sm btn-outline-primary mt-2">
<i class="fas fa-external-link-alt mr-1"></i> Visualizza su mappa
<i class="fas fa-external-link-alt mr-1"></i> Mappa
</a>
@endif
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header"><h3 class="card-title"><i class="fas fa-chart-pie mr-2"></i>Statistiche</h3></div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<div class="border-right">
<h2 class="mb-0 text-primary">{{ $gruppo->individui->count() }}</h2>
<small class="text-muted">Membri</small>
</div>
</div>
<div class="col-6">
<h2 class="mb-0 text-warning">{{ $gruppo->children->count() }}</h2>
<small class="text-muted">Sottogruppi</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-camera mr-2"></i>Avatar</h3>
</div>
<div class="card-body text-center">
@if($gruppo->avatar_url)
<img src="{{ $gruppo->avatar_url }}"
alt="{{ $gruppo->nome }}"
class="img-thumbnail mb-2"
style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 50%;">
<br>
<form id="remove-avatar-form" class="d-inline">
<button type="button" class="btn btn-danger btn-sm" onclick="removeAvatar()">
<i class="fas fa-trash mr-1"></i>Rimuovi
</button>
</form>
@else
<div class="bg-secondary d-flex align-items-center justify-content-center mb-3"
style="width: 120px; height: 120px; margin: 0 auto; border-radius: 50%;">
<i class="fas fa-folder fa-4x text-white"></i>
</div>
@endif
<form id="avatar-upload-form" enctype="multipart/form-data">
@csrf
<div class="custom-file mb-2">
<input type="file" class="custom-file-input" id="avatar-input" name="avatar" accept="image/jpeg,image/png,image/gif,image/webp">
<label class="custom-file-label" for="avatar-input">Scegli immagine...</label>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="uploadAvatar()">
<i class="fas fa-upload mr-1"></i>Carica
</button>
</form>
<div id="avatar-message" class="mt-2"></div>
</div>
</div>
</div>
@php
$incontroEvento = $gruppo->eventi->where('is_incontro_gruppo', true)->first();
@endphp
@if($incontroEvento)
<div class="row">
<div class="col-md-4">
<div class="card card-success">
<div class="col-md-3">
<div class="card card-success h-100">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-calendar-check mr-2"></i>
@@ -174,7 +173,7 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tr>
<td style="width: 100px;"><strong>Evento:</strong></td>
<td style="width: 70px;"><strong>Evento:</strong></td>
<td>{{ $incontroEvento->nome_evento }}</td>
</tr>
<tr>
@@ -182,19 +181,13 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
<td>
@if($incontroEvento->tipo_recorrenza === 'settimanale')
{{ $incontroEvento->giorno_settimana_label }}
<span class="badge badge-info ml-1">Settimanale</span>
<span class="badge badge-info ml-1">Sett.</span>
@elseif($incontroEvento->tipo_recorrenza === 'mensile')
{{ $incontroEvento->occorrenza_mensile_label }}
@if($incontroEvento->mesi_recorrenza)
<small class="text-muted">({{ $incontroEvento->mesi_recorrenza_label }})</small>
@endif
<span class="badge badge-info ml-1">Mensile</span>
<span class="badge badge-info ml-1">Mens.</span>
@elseif($incontroEvento->tipo_recorrenza === 'annuale')
{{ $incontroEvento->mese_annuale_label }}
@if($incontroEvento->giorno_mese)
, giorno {{ $incontroEvento->giorno_mese }}
@endif
<span class="badge badge-info ml-1">Annuale</span>
<span class="badge badge-info ml-1">Ann.</span>
@elseif($incontroEvento->tipo_recorrenza === 'altro')
{{ $incontroEvento->giorno_settimana_label }}
<span class="badge badge-info ml-1">Altro</span>
@@ -212,7 +205,7 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
</table>
@if($incontroEvento->responsabili->count() > 0)
<hr>
<small class="text-muted"><strong>Responsabili:</strong></small>
<small class="text-muted"><strong>Resp.:</strong></small>
@foreach($incontroEvento->responsabili as $resp)
<div class="mt-1">
<i class="fas fa-user text-info mr-1"></i>
@@ -225,18 +218,55 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
@endif
<div class="mt-2">
<a href="/eventi/{{ $incontroEvento->id }}" class="btn btn-xs btn-outline-primary">
<i class="fas fa-eye mr-1"></i> Vedi evento
<i class="fas fa-eye mr-1"></i> Vedi
</a>
</div>
</div>
</div>
</div>
@endif
<div class="col-md-3">
<div class="card h-100">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-camera mr-2"></i>Avatar</h3>
</div>
<div class="card-body text-center">
@if($gruppo->avatar_url)
<img src="{{ $gruppo->avatar_url }}"
alt="{{ $gruppo->nome }}"
class="img-thumbnail mb-2"
style="max-width: 100px; max-height: 100px; object-fit: cover; border-radius: 50%;">
<br>
<form id="remove-avatar-form" class="d-inline">
<button type="button" class="btn btn-danger btn-sm" onclick="removeAvatar()">
<i class="fas fa-trash mr-1"></i>
</button>
</form>
@else
<div class="bg-secondary d-flex align-items-center justify-content-center mx-auto mb-3"
style="width: 100px; height: 100px; border-radius: 50%;">
<i class="fas fa-folder fa-3x text-white"></i>
</div>
@endif
<form id="avatar-upload-form" enctype="multipart/form-data">
@csrf
<div class="custom-file mb-2">
<input type="file" class="custom-file-input" id="avatar-input" name="avatar" accept="image/jpeg,image/png,image/gif,image/webp">
<label class="custom-file-label" for="avatar-input">Scegli...</label>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="uploadAvatar()">
<i class="fas fa-upload mr-1"></i> Carica
</button>
</form>
<div id="avatar-message" class="mt-2"></div>
</div>
</div>
</div>
</div>
@if($gruppo->descrizione)
@if($gruppo->descrizione)
<div class="row mt-3">
<div class="col-md-8">
<div class="col-md-12">
<div class="card">
<div class="card-header"><h3 class="card-title"><i class="fas fa-align-left mr-2"></i>Descrizione</h3></div>
<div class="card-body">
@@ -413,7 +443,7 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
{{ $child->nome }}
</a>
</td>
<td>{{ $child->diocesi?->nome ?? '-' }}</td>
<td>@if($child->diocesi->count() > 0)@foreach($child->diocesi as $dio)<span class="badge badge-secondary mr-1">{{ $dio->nome }}</span>@endforeach @else <span class="text-muted">-</span>@endif</td>
<td>{{ $child->getResponsabili()->isNotEmpty() ? $child->getResponsabili()->first()->nome_completo : '-' }}</td>
<td class="text-center">
@if($child->individui()->count() > 0)
@@ -384,6 +384,7 @@
<li class="nav-item"><a href="#email-account" class="nav-link" data-toggle="tab">Account</a></li>
<li class="nav-item"><a href="#email-sync" class="nav-link" data-toggle="tab">Sincronizzazione</a></li>
<li class="nav-item"><a href="#email-firma" class="nav-link" data-toggle="tab">Firma</a></li>
<li class="nav-item"><a href="#email-firme" class="nav-link" data-toggle="tab">Firme Multiple</a></li>
<li class="nav-item"><a href="#email-mittenti" class="nav-link" data-toggle="tab">Mittenti Aggiuntivi</a></li>
</ul>
</div>
@@ -538,12 +539,32 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="from_email">Email Mittente Ufficiale <small class="text-muted">(se diversa dall'indirizzo IMAP)</small></label>
<input type="email" name="from_email" id="from_email" class="form-control"
value="{{ $emailSettings->from_email ?? '' }}"
placeholder="es. ufficiale@esempio.it">
<small class="text-muted">Se impostata, viene usata come mittente e Reply-To nelle email in uscita</small>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="from_name">Nome Mittente Ufficiale</label>
<input type="text" name="from_name" id="from_name" class="form-control"
value="{{ $emailSettings->from_name ?? '' }}"
placeholder="es. Nome Ufficiale">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="reply_to">Email Risposta (Reply-To)</label>
<input type="email" name="reply_to" id="reply_to" class="form-control"
value="{{ $emailSettings->reply_to ?? '' }}">
<small class="text-muted">Usato solo se "Email Mittente Ufficiale" non è impostata</small>
</div>
</div>
</div>
@@ -625,6 +646,47 @@
</div>
</div>
<div class="tab-pane" id="email-firme">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Firme Multiple</h3>
<div class="card-tools">
<button type="button" class="btn btn-sm btn-success" onclick="window.location.href='/impostazioni/email#firme'">
<i class="fas fa-plus mr-1"></i> Gestisci Firme
</button>
</div>
</div>
<div class="card-body">
@php $firme = optional(\App\Models\EmailSetting::getActive())->firme ?? collect(); @endphp
@if($firme->count() > 0)
<div class="table-responsive">
<table class="table table-bordered table-hover table-sm">
<thead>
<tr>
<th>Nome</th>
<th>Anteprima</th>
<th>Predefinita</th>
</tr>
</thead>
<tbody>
@foreach($firme as $firma)
<tr>
<td><strong>{{ $firma->nome }}</strong></td>
<td><small class="text-muted">{!! Str::limit(strip_tags($firma->contenuto_html), 80) !!}</small></td>
<td>@if($firma->is_default)<span class="badge badge-success"><i class="fas fa-check"></i> Default</span>@endif</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-muted">Nessuna firma multipla configurata. <a href="/impostazioni/email#firme">Gestisci firme nelle impostazioni avanzate.</a></p>
@endif
<p class="mt-2"><a href="/impostazioni/email#firme" class="btn btn-sm btn-primary"><i class="fas fa-cog mr-1"></i> Gestione completa firme</a></p>
</div>
</div>
</div>
<div class="tab-pane" id="email-mittenti">
<div class="card card-primary">
<div class="card-header">
+1 -1
View File
@@ -226,7 +226,7 @@
<i class="fas fa-folder text-warning mr-1"></i>
<a href="/gruppi/{{ $gruppo->id }}">{{ $gruppo->nome }}</a>
</td>
<td>{{ $gruppo->diocesi?->nome ?: '-' }}</td>
<td>{{ $gruppo->diocesi->count() > 0 ? $gruppo->diocesi->pluck('nome')->implode(', ') : '-' }}</td>
<td>
@php $ruoliIndividuo = $individuo->getRuoliForGruppo($gruppo->id) @endphp
@if($ruoliIndividuo->count() > 0)
+1 -1
View File
@@ -256,7 +256,7 @@ if (in_array($contatto->tipo, ['web', 'telegram'])) {
<i class="fas fa-folder text-warning mr-1"></i>
<a href="{{ url('/gruppi/' . $gruppo->id) }}">{{ $gruppo->nome }}</a>
</td>
<td>{{ $gruppo->diocesi?->nome ?: '-' }}</td>
<td>{{ $gruppo->diocesi->count() > 0 ? $gruppo->diocesi->pluck('nome')->implode(', ') : '-' }}</td>
<td>{{ $gruppo->getResponsabili()->isNotEmpty() ? $gruppo->getResponsabili()->first()->nome_completo : '-' }}</td>
<td>
@php
@@ -34,6 +34,32 @@
<hr>
@include('partials._tag-selector', ['allTags' => $tags ?? collect(), 'selectedTags' => old('tags', []), 'label' => 'Tag'])
@if(isset($firme) && $firme->count() > 0)
<div class="form-group">
<label for="firma_id">Firma predefinita</label>
<select name="firma_id" id="firma_id" class="form-control">
<option value="">Nessuna</option>
@foreach($firme as $firma)
<option value="{{ $firma->id }}">{{ $firma->nome }}</option>
@endforeach
</select>
<small class="text-muted">Firma da appender automaticamente quando invii email da questa lista.</small>
</div>
@endif
@if(isset($senderAccounts) && $senderAccounts->count() > 0)
<div class="form-group">
<label for="sender_account_id">Mittente predefinito</label>
<select name="sender_account_id" id="sender_account_id" class="form-control">
<option value="">Sistema predefinito</option>
@foreach($senderAccounts as $sender)
<option value="{{ $sender->id }}">{{ $sender->email_name ?: $sender->email_address }} &lt;{{ $sender->email_address }}&gt;</option>
@endforeach
</select>
<small class="text-muted">Mittente da usare automaticamente quando invii email da questa lista.</small>
</div>
@endif
<div class="alert alert-info">
<i class="fas fa-info-circle mr-1"></i>
<strong>Come aggiungere contatti:</strong> vai su <em>Individui</em>, seleziona le righe desired, e usa il menu <em>Azioni &gt; Crea Mailing List</em> per creare una lista dai contatti selezionati.
+49 -3
View File
@@ -34,6 +34,32 @@
@include('partials._tag-selector', ['allTags' => $tags ?? collect(), 'selectedTags' => $selectedTags ?? [], 'label' => 'Tag'])
@if(isset($firme) && $firme->count() > 0)
<div class="form-group">
<label for="firma_id">Firma predefinita</label>
<select name="firma_id" id="firma_id" class="form-control">
<option value="">Nessuna</option>
@foreach($firme as $firma)
<option value="{{ $firma->id }}" {{ $mailingList->firma_id == $firma->id ? 'selected' : '' }}>{{ $firma->nome }}</option>
@endforeach
</select>
<small class="text-muted">Firma da appender automaticamente quando invii email da questa lista.</small>
</div>
@endif
@if(isset($senderAccounts) && $senderAccounts->count() > 0)
<div class="form-group">
<label for="sender_account_id">Mittente predefinito</label>
<select name="sender_account_id" id="sender_account_id" class="form-control">
<option value="">Sistema predefinito</option>
@foreach($senderAccounts as $sender)
<option value="{{ $sender->id }}" {{ $mailingList->sender_account_id == $sender->id ? 'selected' : '' }}>{{ $sender->email_name ?: $sender->email_address }} &lt;{{ $sender->email_address }}&gt;</option>
@endforeach
</select>
<small class="text-muted">Mittente da usare automaticamente quando invii email da questa lista.</small>
</div>
@endif
<hr>
<div class="row">
@@ -65,6 +91,9 @@
<button type="button" class="btn btn-warning" onclick="deselezionaTutti()">
<i class="fas fa-square mr-1"></i> Deseleziona tutti
</button>
<button type="button" class="btn btn-danger" onclick="rimuoviSelezionati()">
<i class="fas fa-trash mr-1"></i> Rimuovi selezionati
</button>
</div>
<div id="no-email-warning" class="alert alert-warning alert-dismissible" role="alert" style="display:none;">
@@ -313,9 +342,26 @@ function renderContattiTable() {
}
function rimuoviContatto(btn) {
btn.closest('tr').remove();
const count = document.querySelectorAll('.contatto-checkbox').length;
document.getElementById('contatti-count').textContent = count;
const row = btn.closest('tr');
const cb = row.querySelector('.contatto-checkbox');
const id = parseInt(cb.value);
contattiCaricati = contattiCaricati.filter(function(c) { return c.individuo_id !== id; });
row.remove();
document.getElementById('contatti-count').textContent = contattiCaricati.length;
}
function rimuoviSelezionati() {
const checkboxes = document.querySelectorAll('.contatto-checkbox:checked');
if (checkboxes.length === 0) {
alert('Seleziona almeno un contatto da rimuovere.');
return;
}
checkboxes.forEach(function(cb) {
const id = parseInt(cb.value);
contattiCaricati = contattiCaricati.filter(function(c) { return c.individuo_id !== id; });
cb.closest('tr').remove();
});
document.getElementById('contatti-count').textContent = contattiCaricati.length;
}
function toggleSelectAll(source) {
+13
View File
@@ -26,6 +26,19 @@
</div>
@endif
@if(isset($firme) && $firme->count() > 0)
<div class="form-group">
<label for="firma_id">Firma</label>
<select name="firma_id" id="firma_id" class="form-control">
<option value="">Nessuna</option>
@foreach($firme as $firma)
<option value="{{ $firma->id }}">{{ $firma->nome }}</option>
@endforeach
</select>
<small class="text-muted">Seleziona la firma da appender al messaggio.</small>
</div>
@endif
<div class="form-group">
<label>Liste Destinatari *</label>
<select name="liste[]" id="liste" class="form-control" multiple required size="4">
+13
View File
@@ -27,6 +27,19 @@
</div>
@endif
@if(isset($firme) && $firme->count() > 0)
<div class="form-group">
<label for="firma_id">Firma</label>
<select name="firma_id" id="firma_id" class="form-control">
<option value="">Nessuna</option>
@foreach($firme as $firma)
<option value="{{ $firma->id }}">{{ $firma->nome }}</option>
@endforeach
</select>
<small class="text-muted">Seleziona la firma da appender al messaggio.</small>
</div>
@endif
@if($individui->count() > 0)
<div class="form-group" id="destinatari-form-group">
<label>Destinatari selezionati</label>
+5
View File
@@ -211,6 +211,11 @@ Route::put('/impostazioni/sender/{id}', [EmailSettingsController::class, 'sender
Route::delete('/impostazioni/sender/{id}', [EmailSettingsController::class, 'senderDestroy'])->middleware('auth')->name('impostazioni.sender.destroy');
Route::post('/impostazioni/sender/{id}/test', [EmailSettingsController::class, 'senderTestSmtp'])->middleware('auth')->name('impostazioni.sender.test');
Route::post('/impostazioni/firma', [EmailSettingsController::class, 'firmaStore'])->middleware('auth')->name('impostazioni.firma.store');
Route::put('/impostazioni/firma/{id}', [EmailSettingsController::class, 'firmaUpdate'])->middleware('auth')->name('impostazioni.firma.update');
Route::put('/impostazioni/firma/{id}/default', [EmailSettingsController::class, 'firmaSetDefault'])->middleware('auth')->name('impostazioni.firma.default');
Route::delete('/impostazioni/firma/{id}', [EmailSettingsController::class, 'firmaDestroy'])->middleware('auth')->name('impostazioni.firma.destroy');
// Calendar Connections (CalDAV / Google Calendar)
Route::post('/calendario-connessioni', [App\Http\Controllers\CalendarioConnessioneController::class, 'store'])->middleware('auth')->name('calendario-connessioni.store');
Route::put('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'update'])->middleware('auth')->name('calendario-connessioni.update');
Vendored Executable
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = 'phpvfscomposer://'.$this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
$data = str_replace('__FILE__', var_export($this->realpath, true), $data);
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
}
}
return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
Vendored Executable
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../laravel/pint/builds/pint)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/laravel/pint/builds/pint');
}
}
return include __DIR__ . '/..'.'/laravel/pint/builds/pint';
File diff suppressed because it is too large Load Diff
+11 -4
View File
@@ -11,11 +11,11 @@ return array(
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
@@ -27,13 +27,15 @@ return array(
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
'b33e3d135e5d9e47d845c576147bda89' => $vendorDir . '/php-di/php-di/src/functions.php',
'1f87db08236948d07391152dccb70f04' => $vendorDir . '/google/apiclient-services/autoload.php',
'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php',
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
'a8d3953fd9959404dd22d3dfcd0a79f0' => $vendorDir . '/google/apiclient/src/aliases.php',
'23dd7ece5822da3d0100ef3deb0ef55f' => $vendorDir . '/laravel/agent-detector/src/functions.php',
'47e1160838b5e5a10346ac4084b58c23' => $vendorDir . '/laravel/prompts/src/helpers.php',
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
@@ -47,5 +49,10 @@ return array(
'91892b814db86b8442ad76273bb7aec5' => $vendorDir . '/laravel/framework/src/Illuminate/Reflection/helpers.php',
'493c6aea52f6009bab023b26c21a386a' => $vendorDir . '/laravel/framework/src/Illuminate/Support/functions.php',
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
'309cd39bb536ff667b25a3a76938cb83' => $vendorDir . '/laravel/pao/src/Autoload.php',
'c72349b1fe8d0deeedd3a52e8aa814d8' => $vendorDir . '/mockery/mockery/library/helpers.php',
'ce9671a430e4846b44e1c68c7611f9f5' => $vendorDir . '/mockery/mockery/library/Mockery.php',
'a1cfe24d14977df6878b9bf804af2d1c' => $vendorDir . '/nunomaduro/collision/src/Adapters/Phpunit/Autoload.php',
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
'377b22b161c09ed6e5152de788ca020a' => $vendorDir . '/spatie/laravel-permission/src/helpers.php',
);
+12 -3
View File
@@ -10,8 +10,10 @@ return array(
'ZBateson\\StreamDecorators\\' => array($vendorDir . '/zbateson/stream-decorators/src'),
'ZBateson\\MbWrapper\\' => array($vendorDir . '/zbateson/mb-wrapper/src'),
'ZBateson\\MailMimeParser\\' => array($vendorDir . '/zbateson/mail-mime-parser/src'),
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
'Webklex\\PHPIMAP\\' => array($vendorDir . '/webklex/php-imap/src'),
'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
'Tests\\' => array($baseDir . '/tests'),
'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'),
'Symfony\\Polyfill\\Uuid\\' => array($vendorDir . '/symfony/polyfill-uuid'),
'Symfony\\Polyfill\\Php86\\' => array($vendorDir . '/symfony/polyfill-php86'),
@@ -64,8 +66,10 @@ return array(
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
'Mockery\\' => array($vendorDir . '/mockery/mockery/library/Mockery'),
'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'),
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
'League\\Flysystem\\WebDAV\\' => array($vendorDir . '/league/flysystem-webdav'),
@@ -76,6 +80,9 @@ return array(
'Laravel\\Tinker\\' => array($vendorDir . '/laravel/tinker/src'),
'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'),
'Laravel\\Prompts\\' => array($vendorDir . '/laravel/prompts/src'),
'Laravel\\Pao\\' => array($vendorDir . '/laravel/pao/src'),
'Laravel\\Pail\\' => array($vendorDir . '/laravel/pail/src'),
'Laravel\\AgentDetector\\' => array($vendorDir . '/laravel/agent-detector/src'),
'Invoker\\' => array($vendorDir . '/php-di/invoker/src'),
'Illuminate\\Support\\' => array($vendorDir . '/laravel/framework/src/Illuminate/Macroable', $vendorDir . '/laravel/framework/src/Illuminate/Collections', $vendorDir . '/laravel/framework/src/Illuminate/Conditionable', $vendorDir . '/laravel/framework/src/Illuminate/Reflection'),
'Illuminate\\' => array($vendorDir . '/laravel/framework/src/Illuminate'),
@@ -89,6 +96,7 @@ return array(
'Google\\' => array($vendorDir . '/google/apiclient/src'),
'Fruitcake\\Cors\\' => array($vendorDir . '/fruitcake/php-cors/src'),
'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
'Faker\\' => array($vendorDir . '/fakerphp/faker/src/Faker'),
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/src'),
@@ -96,8 +104,9 @@ return array(
'DirectoryTree\\ImapEngine\\Laravel\\' => array($vendorDir . '/directorytree/imapengine-laravel/src'),
'DirectoryTree\\ImapEngine\\' => array($vendorDir . '/directorytree/imapengine/src'),
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
'Database\\Seeders\\' => array($baseDir . '/database/seeders'),
'Database\\Factories\\' => array($baseDir . '/database/factories'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
'Database\\Seeders\\' => array($baseDir . '/database/seeders', $vendorDir . '/laravel/pint/database/seeders'),
'Database\\Factories\\' => array($baseDir . '/database/factories', $vendorDir . '/laravel/pint/database/factories'),
'DI\\' => array($vendorDir . '/php-di/php-di/src'),
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
'Carbon\\Doctrine\\' => array($vendorDir . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine'),
@@ -105,5 +114,5 @@ return array(
'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
'As247\\Flysystem\\GoogleDrive\\' => array($vendorDir . '/as247/flysystem-google-drive/src'),
'As247\\CloudStorages\\' => array($vendorDir . '/as247/cloud-storages/src'),
'App\\' => array($baseDir . '/app'),
'App\\' => array($baseDir . '/app', $vendorDir . '/laravel/pint/app'),
);
+2155 -4
View File
File diff suppressed because it is too large Load Diff
+2405 -2
View File
File diff suppressed because it is too large Load Diff
+318 -3
View File
@@ -3,11 +3,11 @@
'name' => 'laravel/laravel',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'baea18115984a1c2506c8af908424d45498b0b11',
'reference' => 'c5127da750123ed4b569cc18681360a1ba1d04af',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => false,
'dev' => true,
),
'versions' => array(
'as247/cloud-storages' => array(
@@ -46,6 +46,18 @@
'aliases' => array(),
'dev_requirement' => false,
),
'cordoval/hamcrest-php' => array(
'dev_requirement' => true,
'replaced' => array(
0 => '*',
),
),
'davedevelopment/hamcrest-php' => array(
'dev_requirement' => true,
'replaced' => array(
0 => '*',
),
),
'dflydev/dot-access-data' => array(
'pretty_version' => 'v3.0.3',
'version' => '3.0.3.0',
@@ -109,6 +121,24 @@
'aliases' => array(),
'dev_requirement' => false,
),
'fakerphp/faker' => array(
'pretty_version' => 'v1.24.1',
'version' => '1.24.1.0',
'reference' => 'e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5',
'type' => 'library',
'install_path' => __DIR__ . '/../fakerphp/faker',
'aliases' => array(),
'dev_requirement' => true,
),
'filp/whoops' => array(
'pretty_version' => '2.18.4',
'version' => '2.18.4.0',
'reference' => 'd2102955e48b9fd9ab24280a7ad12ed552752c4d',
'type' => 'library',
'install_path' => __DIR__ . '/../filp/whoops',
'aliases' => array(),
'dev_requirement' => true,
),
'firebase/php-jwt' => array(
'pretty_version' => 'v7.0.5',
'version' => '7.0.5.0',
@@ -199,6 +229,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'hamcrest/hamcrest-php' => array(
'pretty_version' => 'v2.1.1',
'version' => '2.1.1.0',
'reference' => 'f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487',
'type' => 'library',
'install_path' => __DIR__ . '/../hamcrest/hamcrest-php',
'aliases' => array(),
'dev_requirement' => true,
),
'illuminate/auth' => array(
'dev_requirement' => false,
'replaced' => array(
@@ -415,6 +454,21 @@
0 => 'v13.8.0',
),
),
'kodova/hamcrest-php' => array(
'dev_requirement' => true,
'replaced' => array(
0 => '*',
),
),
'laravel/agent-detector' => array(
'pretty_version' => 'v2.0.2',
'version' => '2.0.2.0',
'reference' => '90694b9256099591cf9e55d08c18ba7a00bf099f',
'type' => 'library',
'install_path' => __DIR__ . '/../laravel/agent-detector',
'aliases' => array(),
'dev_requirement' => true,
),
'laravel/framework' => array(
'pretty_version' => 'v13.8.0',
'version' => '13.8.0.0',
@@ -427,12 +481,39 @@
'laravel/laravel' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'baea18115984a1c2506c8af908424d45498b0b11',
'reference' => 'c5127da750123ed4b569cc18681360a1ba1d04af',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'laravel/pail' => array(
'pretty_version' => 'v1.2.6',
'version' => '1.2.6.0',
'reference' => 'aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf',
'type' => 'library',
'install_path' => __DIR__ . '/../laravel/pail',
'aliases' => array(),
'dev_requirement' => true,
),
'laravel/pao' => array(
'pretty_version' => 'v1.0.6',
'version' => '1.0.6.0',
'reference' => '02f62a64c2b60af44a418ee490fee193590d8269',
'type' => 'library',
'install_path' => __DIR__ . '/../laravel/pao',
'aliases' => array(),
'dev_requirement' => true,
),
'laravel/pint' => array(
'pretty_version' => 'v1.29.1',
'version' => '1.29.1.0',
'reference' => '0770e9b7fafd50d4586881d456d6eb41c9247a80',
'type' => 'project',
'install_path' => __DIR__ . '/../laravel/pint',
'aliases' => array(),
'dev_requirement' => true,
),
'laravel/prompts' => array(
'pretty_version' => 'v0.3.17',
'version' => '0.3.17.0',
@@ -532,6 +613,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'mockery/mockery' => array(
'pretty_version' => '1.6.12',
'version' => '1.6.12.0',
'reference' => '1f4efdd7d3beafe9807b08156dfcb176d18f1699',
'type' => 'library',
'install_path' => __DIR__ . '/../mockery/mockery',
'aliases' => array(),
'dev_requirement' => true,
),
'monolog/monolog' => array(
'pretty_version' => '3.10.0',
'version' => '3.10.0.0',
@@ -547,6 +637,15 @@
0 => '^1.0',
),
),
'myclabs/deep-copy' => array(
'pretty_version' => '1.13.4',
'version' => '1.13.4.0',
'reference' => '07d290f0c47959fd5eed98c95ee5602db07e0b6a',
'type' => 'library',
'install_path' => __DIR__ . '/../myclabs/deep-copy',
'aliases' => array(),
'dev_requirement' => true,
),
'nesbot/carbon' => array(
'pretty_version' => '3.11.4',
'version' => '3.11.4.0',
@@ -583,6 +682,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'nunomaduro/collision' => array(
'pretty_version' => 'v8.9.4',
'version' => '8.9.4.0',
'reference' => '716af8f95a470e9094cfca09ed897b023be191a5',
'type' => 'library',
'install_path' => __DIR__ . '/../nunomaduro/collision',
'aliases' => array(),
'dev_requirement' => true,
),
'nunomaduro/termwind' => array(
'pretty_version' => 'v2.4.0',
'version' => '2.4.0.0',
@@ -592,6 +700,24 @@
'aliases' => array(),
'dev_requirement' => false,
),
'phar-io/manifest' => array(
'pretty_version' => '2.0.4',
'version' => '2.0.4.0',
'reference' => '54750ef60c58e43759730615a392c31c80e23176',
'type' => 'library',
'install_path' => __DIR__ . '/../phar-io/manifest',
'aliases' => array(),
'dev_requirement' => true,
),
'phar-io/version' => array(
'pretty_version' => '3.2.1',
'version' => '3.2.1.0',
'reference' => '4f7fd7836c6f332bb2933569e566a0d6c4cbed74',
'type' => 'library',
'install_path' => __DIR__ . '/../phar-io/version',
'aliases' => array(),
'dev_requirement' => true,
),
'php-di/invoker' => array(
'pretty_version' => '2.3.7',
'version' => '2.3.7.0',
@@ -619,6 +745,60 @@
'aliases' => array(),
'dev_requirement' => false,
),
'phpunit/php-code-coverage' => array(
'pretty_version' => '12.5.6',
'version' => '12.5.6.0',
'reference' => '876099a072646c7745f673d7aeab5382c4439691',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-code-coverage',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/php-file-iterator' => array(
'pretty_version' => '6.0.1',
'version' => '6.0.1.0',
'reference' => '3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-file-iterator',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/php-invoker' => array(
'pretty_version' => '6.0.0',
'version' => '6.0.0.0',
'reference' => '12b54e689b07a25a9b41e57736dfab6ec9ae5406',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-invoker',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/php-text-template' => array(
'pretty_version' => '5.0.0',
'version' => '5.0.0.0',
'reference' => 'e1367a453f0eda562eedb4f659e13aa900d66c53',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-text-template',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/php-timer' => array(
'pretty_version' => '8.0.0',
'version' => '8.0.0.0',
'reference' => 'f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-timer',
'aliases' => array(),
'dev_requirement' => true,
),
'phpunit/phpunit' => array(
'pretty_version' => '12.5.24',
'version' => '12.5.24.0',
'reference' => 'd75dd30597caa80e72fad2ef7904601a30ef1046',
'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/phpunit',
'aliases' => array(),
'dev_requirement' => true,
),
'psr/cache' => array(
'pretty_version' => '3.0.0',
'version' => '3.0.0.0',
@@ -847,6 +1027,123 @@
'aliases' => array(),
'dev_requirement' => false,
),
'sebastian/cli-parser' => array(
'pretty_version' => '4.2.0',
'version' => '4.2.0.0',
'reference' => '90f41072d220e5c40df6e8635f5dafba2d9d4d04',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/cli-parser',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/comparator' => array(
'pretty_version' => '7.1.6',
'version' => '7.1.6.0',
'reference' => 'c769009dee98f494e0edc3fd4f4087501688f11e',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/comparator',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/complexity' => array(
'pretty_version' => '5.0.0',
'version' => '5.0.0.0',
'reference' => 'bad4316aba5303d0221f43f8cee37eb58d384bbb',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/complexity',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/diff' => array(
'pretty_version' => '7.0.0',
'version' => '7.0.0.0',
'reference' => '7ab1ea946c012266ca32390913653d844ecd085f',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/diff',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/environment' => array(
'pretty_version' => '8.1.0',
'version' => '8.1.0.0',
'reference' => 'b121608b28a13f721e76ffbbd386d08eff58f3f6',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/environment',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/exporter' => array(
'pretty_version' => '7.0.2',
'version' => '7.0.2.0',
'reference' => '016951ae10980765e4e7aee491eb288c64e505b7',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/exporter',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/global-state' => array(
'pretty_version' => '8.0.2',
'version' => '8.0.2.0',
'reference' => 'ef1377171613d09edd25b7816f05be8313f9115d',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/global-state',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/lines-of-code' => array(
'pretty_version' => '4.0.0',
'version' => '4.0.0.0',
'reference' => '97ffee3bcfb5805568d6af7f0f893678fc076d2f',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/lines-of-code',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/object-enumerator' => array(
'pretty_version' => '7.0.0',
'version' => '7.0.0.0',
'reference' => '1effe8e9b8e068e9ae228e542d5d11b5d16db894',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/object-enumerator',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/object-reflector' => array(
'pretty_version' => '5.0.0',
'version' => '5.0.0.0',
'reference' => '4bfa827c969c98be1e527abd576533293c634f6a',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/object-reflector',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/recursion-context' => array(
'pretty_version' => '7.0.1',
'version' => '7.0.1.0',
'reference' => '0b01998a7d5b1f122911a66bebcb8d46f0c82d8c',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/recursion-context',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/type' => array(
'pretty_version' => '6.0.3',
'version' => '6.0.3.0',
'reference' => 'e549163b9760b8f71f191651d22acf32d56d6d4d',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/type',
'aliases' => array(),
'dev_requirement' => true,
),
'sebastian/version' => array(
'pretty_version' => '6.0.0',
'version' => '6.0.0.0',
'reference' => '3e6ccf7657d4f0a59200564b08cead899313b53c',
'type' => 'library',
'install_path' => __DIR__ . '/../sebastian/version',
'aliases' => array(),
'dev_requirement' => true,
),
'spatie/laravel-package-tools' => array(
'pretty_version' => '1.93.0',
'version' => '1.93.0.0',
@@ -871,6 +1168,15 @@
0 => '*',
),
),
'staabm/side-effects-detector' => array(
'pretty_version' => '1.0.5',
'version' => '1.0.5.0',
'reference' => 'd8334211a140ce329c13726d4a715adbddd0a163',
'type' => 'library',
'install_path' => __DIR__ . '/../staabm/side-effects-detector',
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/clock' => array(
'pretty_version' => 'v8.0.8',
'version' => '8.0.8.0',
@@ -1162,6 +1468,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'theseer/tokenizer' => array(
'pretty_version' => '2.0.1',
'version' => '2.0.1.0',
'reference' => '7989e43bf381af0eac72e4f0ca5bcbfa81658be4',
'type' => 'library',
'install_path' => __DIR__ . '/../theseer/tokenizer',
'aliases' => array(),
'dev_requirement' => true,
),
'tijsverkoyen/css-to-inline-styles' => array(
'pretty_version' => 'v2.4.0',
'version' => '2.4.0.0',
+209
View File
@@ -0,0 +1,209 @@
# CHANGELOG
## [Unreleased](https://github.com/FakerPHP/Faker/compare/v1.24.0...1.24.1)
- Removed domain `gmail.com.au` from `Provider\en_AU\Internet` (#886)
## [2024-11-09, v1.24.0](https://github.com/FakerPHP/Faker/compare/v1.23.1..v1.24.0)
- Fix internal deprecations in Doctrine's populator by @gnutix in (#889)
- Fix mobile phone number pattern for France by @ker0x in (#859)
- PHP 8.4 Support by @Jubeki in (#904)
- Added support for PHP 8.4 (#904)
## [2023-09-29, v1.23.1](https://github.com/FakerPHP/Faker/compare/v1.23.0..v1.23.1)
- Fixed double `а` female lastName in `ru_RU/Person::name()` (#832)
- Fixed polish license plates (#685)
- Stopped using `static` in callables in `Provider\pt_BR\PhoneNumber` (#785)
- Fixed incorrect female name (#794)
- Stopped using the deprecated `MT_RAND_PHP` constant to seed the random generator on PHP 8.3 (#844)
## [2023-06-12, v1.23.0](https://github.com/FakerPHP/Faker/compare/v1.22.0..v1.23.0)
- Update `randomElements` to return random number of elements when no count is provided (#658)
## [2023-05-14, v1.22.0](https://github.com/FakerPHP/Faker/compare/v1.21.0..v1.22.0)
- Fixed `randomElements()` to accept empty iterator (#605)
- Added support for passing an `Enum` to `randomElement()` and `randomElements()` (#620)
- Started rejecting invalid arguments passed to `randomElement()` and `randomElements()` (#642)
## [2022-12-13, v1.21.0](https://github.com/FakerPHP/Faker/compare/v1.20.0..v1.21.0)
- Dropped support for PHP 7.1, 7.2, and 7.3 (#543)
- Added support for PHP 8.2 (#528)
## [2022-07-20, v1.20.0](https://github.com/FakerPHP/Faker/compare/v1.19.0..v1.20.0)
- Fixed typo in French phone number (#452)
- Fixed some Hungarian naming bugs (#451)
- Fixed bug where the NL-BE VAT generation was incorrect (#455)
- Improve Turkish phone numbers for E164 and added landline support (#460)
- Add Microsoft Edge User Agent (#464)
- Added option to set image formats on Faker\Provider\Image (#473)
- Added support for French color translations (#466)
- Support filtering timezones by country code (#480)
- Fixed typo in some greek names (#490)
- Marked the Faker\Provider\Image as deprecated
## [2022-02-02, v1.19.0](https://github.com/FakerPHP/Faker/compare/v1.18.0..v1.19.0)
- Added color extension to core (#442)
- Added conflict with `doctrine/persistence` below version `1.4`
- Fix for support on different Doctrine ORM versions (#414)
- Fix usage of `Doctrine\Persistence` dependency
- Fix CZ Person birthNumber docblock return type (#437)
- Fix is_IS Person docbock types (#439)
- Fix is_IS Address docbock type (#438)
- Fix regexify escape backslash in character class (#434)
- Removed UUID from Generator to be able to extend it (#441)
## [2022-01-23, v1.18.0](https://github.com/FakerPHP/Faker/compare/v1.17.0..v1.18.0)
- Deprecated UUID, use uuid3 to specify version (#427)
- Reset formatters when adding a new provider (#366)
- Helper methods to use our custom generators (#155)
- Set allow-plugins for Composer 2.2 (#405)
- Fix kk_KZ\Person::individualIdentificationNumber generation (#411)
- Allow for -> syntax to be used in parsing (#423)
- Person->name was missing string return type (#424)
- Generate a valid BE TAX number (#415)
- Added the UUID extension to Core (#427)
## [2021-12-05, v1.17.0](https://github.com/FakerPHP/Faker/compare/v1.16.0..v1.17.0)
- Partial PHP 8.1 compatibility (#373)
- Add payment provider for `ne_NP` locale (#375)
- Add Egyptian Arabic `ar_EG` locale (#377)
- Updated list of South African TLDs (#383)
- Fixed formatting of E.164 numbers (#380)
- Allow `symfony/deprecation-contracts` `^3.0` (#397)
## [2021-09-06, v1.16.0](https://github.com/FakerPHP/Faker/compare/v1.15.0..v1.16.0)
- Add Company extension
- Add Address extension
- Add Person extension
- Add PhoneNumber extension
- Add VersionExtension (#350)
- Stricter types in Extension\Container and Extension\GeneratorAwareExtension (#345)
- Fix deprecated property access in `nl_NL` (#348)
- Add support for `psr/container` >= 2.0 (#354)
- Add missing union types in Faker\Generator (#352)
## [2021-07-06, v1.15.0](https://github.com/FakerPHP/Faker/compare/v1.14.1..v1.15.0)
- Updated the generator phpdoc to help identify magic methods (#307)
- Prevent direct access and triggered deprecation warning for "word" (#302)
- Updated length on all global e164 numbers (#301)
- Updated last names from different source (#312)
- Don't generate birth number of '000' for Swedish personal identity (#306)
- Add job list for localization id_ID (#339)
## [2021-03-30, v1.14.1](https://github.com/FakerPHP/Faker/compare/v1.14.0..v1.14.1)
- Fix where randomNumber and randomFloat would return a 0 value (#291 / #292)
## [2021-03-29, v1.14.0](https://github.com/FakerPHP/Faker/compare/v1.13.0..v1.14.0)
- Fix for realText to ensure the text keeps closer to its boundaries (#152)
- Fix where regexify produces a random character instead of a literal dot (#135
- Deprecate zh_TW methods that only call base methods (#122)
- Add used extensions to composer.json as suggestion (#120)
- Moved TCNo and INN from calculator to localized providers (#108)
- Fix regex dot/backslash issue where a dot is replaced with a backslash as escape character (#206)
- Deprecate direct property access (#164)
- Added test to assert unique() behaviour (#233)
- Added RUC for the es_PE locale (#244)
- Test IBAN formats for Latin America (AR/PE/VE) (#260)
- Added VAT number for en_GB (#255)
- Added new districts for the ne_NP locale (#258)
- Fix for U.S. Area Code Generation (#261)
- Fix in numerify where a better random numeric value is guaranteed (#256)
- Fix e164PhoneNumber to only generate valid phone numbers with valid country codes (#264)
- Extract fixtures into separate classes (#234)
- Remove french domains that no longer exists (#277)
- Fix error that occurs when getting a polish title (#279)
- Use valid area codes for North America E164 phone numbers (#280)
- Adding support for extensions and PSR-11 (#154)
- Adding trait for GeneratorAwareExtension (#165)
- Added helper class for extension (#162)
- Added blood extension to core (#232)
- Added barcode extension to core (#252)
- Added number extension (#257)
- Various code style updates
- Added a note about our breaking change promise (#273)
## [2020-12-18, v1.13.0](https://github.com/FakerPHP/Faker/compare/v1.12.1..v1.13.0)
Several fixes and new additions in this release. A lot of cleanup has been done
on the codebase on both tests and consistency.
- Feature/pl pl license plate (#62)
- Fix greek phone numbers (#16)
- Move AT payment provider logic to de_AT (#72)
- Fix wiktionary links (#73)
- Fix AT person links (#74)
- Fix AT cities (#75)
- Deprecate at_AT providers (#78)
- Add Austrian `ssn()` to `Person` provider (#79)
- Fix typos in id_ID Address (#83)
- Austrian post codes (#86)
- Updated Polish data (#70)
- Improve Austrian social security number generation (#88)
- Move US phone numbers with extension to own method (#91)
- Add UK National Insurance number generator (#89)
- Fix en_SG phone number generator (#100)
- Remove usage of mt_rand (#87)
- Remove whitespace from beginning of el_GR phone numbers (#105)
- Building numbers can not be 0, 00, 000 (#107)
- Add 172.16/12 local IPv4 block (#121)
- Add JCB credit card type (#124)
- Remove json_decode from emoji generation (#123)
- Remove ro street address (#146)
## [2020-12-11, v1.12.1](https://github.com/FakerPHP/Faker/compare/v1.12.0..v1.12.1)
This is a security release that prevents a hacker to execute code on the server.
## [2020-11-23, v1.12.0](https://github.com/FakerPHP/Faker/compare/v1.11.0..v1.12.0)
- Fix ro_RO first and last day of year calculation offset (#65)
- Fix en_NG locale test namespaces that did not match PSR-4 (#57)
- Added Singapore NRIC/FIN provider (#56)
- Added provider for Lithuanian municipalities (#58)
- Added blood types provider (#61)
## [2020-11-15, v1.11.0](https://github.com/FakerPHP/Faker/compare/v1.10.1..v1.11.0)
- Added Provider for Swedish Municipalities
- Updates to person names in pt_BR
- Many code style changes
## [2020-10-28, v1.10.1](https://github.com/FakerPHP/Faker/compare/v1.10.0..v1.10.1)
- Updates the Danish addresses in dk_DK
- Removed offense company names in nl_NL
- Clarify changelog with original fork
- Standin replacement for LoremPixel to Placeholder.com (#11)
## [2020-10-27, v1.10.0](https://github.com/FakerPHP/Faker/compare/v1.9.1..v1.10.0)
- Support PHP 7.1-8.0
- Fix typo in de_DE Company Provider
- Fix dateTimeThisYear method
- Fix typo in de_DE jobTitleFormat
- Fix IBAN generation for CR
- Fix typos in greek first names
- Fix US job title typo
- Do not clear entity manager for doctrine orm populator
- Remove persian rude words
- Corrections to RU names
## 2020-10-27, v1.9.1
- Initial version. Same as `fzaninotto/Faker:v1.9.1`.
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2011 François Zaninotto
Portions Copyright (c) 2008 Caius Durling
Portions Copyright (c) 2008 Adam Royle
Portions Copyright (c) 2008 Fiona Burrows
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+114
View File
@@ -0,0 +1,114 @@
<p style="text-align: center"><img src="https://github.com/FakerPHP/Artwork/raw/main/src/socialcard.png" alt="Social card of FakerPHP"></p>
# Faker
[![Packagist Downloads](https://img.shields.io/packagist/dm/FakerPHP/Faker)](https://packagist.org/packages/fakerphp/faker)
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/FakerPHP/Faker/Tests/main)](https://github.com/FakerPHP/Faker/actions)
[![Type Coverage](https://shepherd.dev/github/FakerPHP/Faker/coverage.svg)](https://shepherd.dev/github/FakerPHP/Faker)
[![Code Coverage](https://codecov.io/gh/FakerPHP/Faker/branch/main/graph/badge.svg)](https://codecov.io/gh/FakerPHP/Faker)
Faker is a PHP library that generates fake data for you. Whether you need to bootstrap your database, create good-looking XML documents, fill-in your persistence to stress test it, or anonymize data taken from a production service, Faker is for you.
It's heavily inspired by Perl's [Data::Faker](https://metacpan.org/pod/Data::Faker), and by Ruby's [Faker](https://rubygems.org/gems/faker).
## Getting Started
### Installation
Faker requires PHP >= 7.4.
```shell
composer require fakerphp/faker
```
### Documentation
Full documentation can be found over on [fakerphp.github.io](https://fakerphp.github.io).
### Basic Usage
Use `Faker\Factory::create()` to create and initialize a Faker generator, which can generate data by accessing methods named after the type of data you want.
```php
<?php
require_once 'vendor/autoload.php';
// use the factory to create a Faker\Generator instance
$faker = Faker\Factory::create();
// generate data by calling methods
echo $faker->name();
// 'Vince Sporer'
echo $faker->email();
// 'walter.sophia@hotmail.com'
echo $faker->text();
// 'Numquam ut mollitia at consequuntur inventore dolorem.'
```
Each call to `$faker->name()` yields a different (random) result. This is because Faker uses `__call()` magic, and forwards `Faker\Generator->$method()` calls to `Faker\Generator->format($method, $attributes)`.
```php
<?php
for ($i = 0; $i < 3; $i++) {
echo $faker->name() . "\n";
}
// 'Cyrus Boyle'
// 'Alena Cummerata'
// 'Orlo Bergstrom'
```
## Automated refactoring
If you already used this library with its properties, they are now deprecated and needs to be replaced by their equivalent methods.
You can use the provided [Rector](https://github.com/rectorphp/rector) config file to automate the work.
Run
```bash
composer require --dev rector/rector
```
to install `rector/rector`.
Run
```bash
vendor/bin/rector process src/ --config vendor/fakerphp/faker/rector-migrate.php
```
to run `rector/rector`.
*Note:* do not forget to replace `src/` with the path to your source directory.
Alternatively, import the configuration in your `rector.php` file:
```php
<?php
declare(strict_types=1);
use Rector\Config;
return static function (Config\RectorConfig $rectorConfig): void {
$rectorConfig->import('vendor/fakerphp/faker/rector-migrate.php');
};
```
## License
Faker is released under the MIT License. See [`LICENSE`](LICENSE) for details.
## Backward compatibility promise
Faker is using [Semver](https://semver.org/). This means that versions are tagged
with MAJOR.MINOR.PATCH. Only a new major version will be allowed to break backward
compatibility (BC).
Classes marked as `@experimental` or `@internal` are not included in our backward compatibility promise.
You are also not guaranteed that the value returned from a method is always the
same. You are guaranteed that the data type will not change.
PHP 8 introduced [named arguments](https://wiki.php.net/rfc/named_params), which
increased the cost and reduces flexibility for package maintainers. The names of the
arguments for methods in Faker is not included in our BC promise.
+56
View File
@@ -0,0 +1,56 @@
{
"name": "fakerphp/faker",
"type": "library",
"description": "Faker is a PHP library that generates fake data for you.",
"keywords": [
"faker",
"fixtures",
"data"
],
"license": "MIT",
"authors": [
{
"name": "François Zaninotto"
}
],
"require": {
"php": "^7.4 || ^8.0",
"psr/container": "^1.0 || ^2.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
},
"require-dev": {
"ext-intl": "*",
"bamarni/composer-bin-plugin": "^1.4.1",
"doctrine/persistence": "^1.3 || ^2.0",
"phpunit/phpunit": "^9.5.26",
"symfony/phpunit-bridge": "^5.4.16"
},
"autoload": {
"psr-4": {
"Faker\\": "src/Faker/"
}
},
"autoload-dev": {
"psr-4": {
"Faker\\Test\\": "test/Faker/",
"Faker\\Test\\Fixture\\": "test/Fixture/"
}
},
"conflict": {
"fzaninotto/faker": "*"
},
"suggest": {
"ext-curl": "Required by Faker\\Provider\\Image to download images.",
"ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.",
"ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.",
"ext-mbstring": "Required for multibyte Unicode string functionality.",
"doctrine/orm": "Required to use Faker\\ORM\\Doctrine"
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true,
"composer/package-versions-deprecated": true
},
"sort-packages": true
}
}
+161
View File
@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
use Faker\Generator;
use Rector\Config;
use Rector\Transform;
// This file configures rector/rector to replace all deprecated property usages with their equivalent functions.
return static function (Config\RectorConfig $rectorConfig): void {
$properties = [
'address',
'amPm',
'asciify',
'biasedNumberBetween',
'boolean',
'bothify',
'buildingNumber',
'century',
'chrome',
'city',
'citySuffix',
'colorName',
'company',
'companyEmail',
'companySuffix',
'country',
'countryCode',
'countryISOAlpha3',
'creditCardDetails',
'creditCardExpirationDate',
'creditCardExpirationDateString',
'creditCardNumber',
'creditCardType',
'currencyCode',
'date',
'dateTime',
'dateTimeAD',
'dateTimeBetween',
'dateTimeInInterval',
'dateTimeThisCentury',
'dateTimeThisDecade',
'dateTimeThisMonth',
'dateTimeThisYear',
'dayOfMonth',
'dayOfWeek',
'domainName',
'domainWord',
'e164PhoneNumber',
'email',
'emoji',
'file',
'firefox',
'firstName',
'firstNameFemale',
'firstNameMale',
'freeEmail',
'freeEmailDomain',
'getDefaultTimezone',
'hexColor',
'hslColor',
'hslColorAsArray',
'iban',
'image',
'imageUrl',
'imei',
'internetExplorer',
'iosMobileToken',
'ipv4',
'ipv6',
'iso8601',
'jobTitle',
'languageCode',
'lastName',
'latitude',
'lexify',
'linuxPlatformToken',
'linuxProcessor',
'localCoordinates',
'localIpv4',
'locale',
'longitude',
'macAddress',
'macPlatformToken',
'macProcessor',
'md5',
'month',
'monthName',
'msedge',
'name',
'numerify',
'opera',
'paragraph',
'paragraphs',
'passthrough',
'password',
'phoneNumber',
'postcode',
'randomAscii',
'randomDigitNotNull',
'randomElement',
'randomElements',
'randomHtml',
'randomKey',
'randomLetter',
'realText',
'realTextBetween',
'regexify',
'rgbColor',
'rgbColorAsArray',
'rgbCssColor',
'rgbaCssColor',
'safari',
'safeColorName',
'safeEmail',
'safeEmailDomain',
'safeHexColor',
'sentence',
'sentences',
'setDefaultTimezone',
'sha1',
'sha256',
'shuffle',
'shuffleArray',
'shuffleString',
'slug',
'streetAddress',
'streetName',
'streetSuffix',
'swiftBicNumber',
'text',
'time',
'timezone',
'title',
'titleFemale',
'titleMale',
'tld',
'toLower',
'toUpper',
'unixTime',
'url',
'userAgent',
'userName',
'uuid',
'windowsPlatformToken',
'word',
'words',
'year',
];
$rectorConfig->ruleWithConfiguration(
Transform\Rector\Assign\PropertyFetchToMethodCallRector::class,
array_map(static function (string $property): Transform\ValueObject\PropertyFetchToMethodCall {
return new Transform\ValueObject\PropertyFetchToMethodCall(
Generator::class,
$property,
$property,
);
}, $properties),
);
};
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace Faker\Calculator;
/**
* Utility class for validating EAN-8 and EAN-13 numbers
*/
class Ean
{
/**
* @var string EAN validation pattern
*/
public const PATTERN = '/^(?:\d{8}|\d{13})$/';
/**
* Computes the checksum of an EAN number.
*
* @see https://en.wikipedia.org/wiki/International_Article_Number
*
* @return int
*/
public static function checksum(string $digits)
{
$sequence = (strlen($digits) + 1) === 8 ? [3, 1] : [1, 3];
$sums = 0;
foreach (str_split($digits) as $n => $digit) {
$sums += ((int) $digit) * $sequence[$n % 2];
}
return (10 - $sums % 10) % 10;
}
/**
* Checks whether the provided number is an EAN compliant number and that
* the checksum is correct.
*
* @param string $ean An EAN number
*
* @return bool
*/
public static function isValid(string $ean)
{
if (!preg_match(self::PATTERN, $ean)) {
return false;
}
return self::checksum(substr($ean, 0, -1)) === (int) substr($ean, -1);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace Faker\Calculator;
class Iban
{
/**
* Generates IBAN Checksum
*
* @return string Checksum (numeric string)
*/
public static function checksum(string $iban)
{
// Move first four digits to end and set checksum to '00'
$checkString = substr($iban, 4) . substr($iban, 0, 2) . '00';
// Replace all letters with their number equivalents
$checkString = preg_replace_callback(
'/[A-Z]/',
static function (array $matches): string {
return (string) self::alphaToNumber($matches[0]);
},
$checkString,
);
// Perform mod 97 and subtract from 98
$checksum = 98 - self::mod97($checkString);
return str_pad($checksum, 2, '0', STR_PAD_LEFT);
}
/**
* Converts letter to number
*
* @return int
*/
public static function alphaToNumber(string $char)
{
return ord($char) - 55;
}
/**
* Calculates mod97 on a numeric string
*
* @param string $number Numeric string
*
* @return int
*/
public static function mod97(string $number)
{
$checksum = (int) $number[0];
for ($i = 1, $size = strlen($number); $i < $size; ++$i) {
$checksum = (10 * $checksum + (int) $number[$i]) % 97;
}
return $checksum;
}
/**
* Checks whether an IBAN has a valid checksum
*
* @return bool
*/
public static function isValid(string $iban)
{
return self::checksum($iban) === substr($iban, 2, 2);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace Faker\Calculator;
/**
* @deprecated moved to ru_RU\Company, use {@link \Faker\Provider\ru_RU\Company}.
* @see \Faker\Provider\ru_RU\Company
*/
class Inn
{
/**
* Generates INN Checksum
*
* https://ru.wikipedia.org/wiki/%D0%98%D0%B4%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BD%D0%BE%D0%BC%D0%B5%D1%80_%D0%BD%D0%B0%D0%BB%D0%BE%D0%B3%D0%BE%D0%BF%D0%BB%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%89%D0%B8%D0%BA%D0%B0
*
* @param string $inn
*
* @return string Checksum (one digit)
*
* @deprecated use {@link \Faker\Provider\ru_RU\Company::inn10Checksum()} instead
* @see \Faker\Provider\ru_RU\Company::inn10Checksum()
*/
public static function checksum($inn)
{
return \Faker\Provider\ru_RU\Company::inn10Checksum($inn);
}
/**
* Checks whether an INN has a valid checksum
*
* @param string $inn
*
* @return bool
*
* @deprecated use {@link \Faker\Provider\ru_RU\Company::inn10IsValid()} instead
* @see \Faker\Provider\ru_RU\Company::inn10IsValid()
*/
public static function isValid($inn)
{
return \Faker\Provider\ru_RU\Company::inn10IsValid($inn);
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Faker\Calculator;
/**
* Utility class for validating ISBN-10
*/
class Isbn
{
/**
* @var string ISBN-10 validation pattern
*/
public const PATTERN = '/^\d{9}[0-9X]$/';
/**
* ISBN-10 check digit
*
* @see http://en.wikipedia.org/wiki/International_Standard_Book_Number#ISBN-10_check_digits
*
* @param string $input ISBN without check-digit
*
* @throws \LengthException When wrong input length passed
*/
public static function checksum(string $input): string
{
// We're calculating check digit for ISBN-10
// so, the length of the input should be 9
$length = 9;
if (strlen($input) !== $length) {
throw new \LengthException(sprintf('Input length should be equal to %d', $length));
}
$digits = str_split($input);
array_walk(
$digits,
static function (&$digit, $position): void {
$digit = (10 - $position) * $digit;
},
);
$result = (11 - array_sum($digits) % 11) % 11;
// 10 is replaced by X
return ($result < 10) ? (string) $result : 'X';
}
/**
* Checks whether the provided number is a valid ISBN-10 number
*
* @param string $isbn ISBN to check
*/
public static function isValid(string $isbn): bool
{
if (!preg_match(self::PATTERN, $isbn)) {
return false;
}
return self::checksum(substr($isbn, 0, -1)) === substr($isbn, -1);
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace Faker\Calculator;
/**
* Utility class for generating and validating Luhn numbers.
*
* Luhn algorithm is used to validate credit card numbers, IMEI numbers, and
* National Provider Identifier numbers.
*
* @see http://en.wikipedia.org/wiki/Luhn_algorithm
*/
class Luhn
{
/**
* @return int
*/
private static function checksum(string $number)
{
$number = (string) $number;
$length = strlen($number);
$sum = 0;
for ($i = $length - 1; $i >= 0; $i -= 2) {
$sum += $number[$i];
}
for ($i = $length - 2; $i >= 0; $i -= 2) {
$sum += array_sum(str_split($number[$i] * 2));
}
return $sum % 10;
}
/**
* @return string
*/
public static function computeCheckDigit(string $partialNumber)
{
$checkDigit = self::checksum($partialNumber . '0');
if ($checkDigit === 0) {
return '0';
}
return (string) (10 - $checkDigit);
}
/**
* Checks whether a number (partial number + check digit) is Luhn compliant
*
* @return bool
*/
public static function isValid(string $number)
{
return self::checksum($number) === 0;
}
/**
* Generate a Luhn compliant number.
*
* @return string
*/
public static function generateLuhnNumber(string $partialValue)
{
if (!preg_match('/^\d+$/', $partialValue)) {
throw new \InvalidArgumentException('Argument should be an integer.');
}
return $partialValue . Luhn::computeCheckDigit($partialValue);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace Faker\Calculator;
/**
* @deprecated moved to tr_TR\Person, use {@link \Faker\Provider\tr_TR\Person}.
* @see \Faker\Provider\tr_TR\Person
*/
class TCNo
{
/**
* Generates Turkish Identity Number Checksum
* Gets first 9 digit as prefix and calculates checksum
*
* https://en.wikipedia.org/wiki/Turkish_Identification_Number
*
* @param string $identityPrefix
*
* @return string Checksum (two digit)
*
* @deprecated use {@link \Faker\Provider\tr_TR\Person::tcNoChecksum()} instead
* @see \Faker\Provider\tr_TR\Person::tcNoChecksum()
*/
public static function checksum($identityPrefix)
{
return \Faker\Provider\tr_TR\Person::tcNoChecksum($identityPrefix);
}
/**
* Checks whether a TCNo has a valid checksum
*
* @param string $tcNo
*
* @return bool
*
* @deprecated use {@link \Faker\Provider\tr_TR\Person::tcNoIsValid()} instead
* @see \Faker\Provider\tr_TR\Person::tcNoIsValid()
*/
public static function isValid($tcNo)
{
return \Faker\Provider\tr_TR\Person::tcNoIsValid($tcNo);
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Faker;
use Faker\Extension\Extension;
/**
* This generator returns a default value for all called properties
* and methods. It works with Faker\Generator::optional().
*
* @mixin Generator
*/
class ChanceGenerator
{
private $generator;
private $weight;
protected $default;
/**
* @param Extension|Generator $generator
*/
public function __construct($generator, float $weight, $default = null)
{
$this->default = $default;
$this->generator = $generator;
$this->weight = $weight;
}
public function ext(string $id)
{
return new self($this->generator->ext($id), $this->weight, $this->default);
}
/**
* Catch and proxy all generator calls but return only valid values
*
* @param string $attribute
*
* @deprecated Use a method instead.
*/
public function __get($attribute)
{
trigger_deprecation('fakerphp/faker', '1.14', 'Accessing property "%s" is deprecated, use "%s()" instead.', $attribute, $attribute);
return $this->__call($attribute, []);
}
/**
* @param string $name
* @param array $arguments
*/
public function __call($name, $arguments)
{
if (mt_rand(1, 100) <= (100 * $this->weight)) {
return call_user_func_array([$this->generator, $name], $arguments);
}
return $this->default;
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Faker\Container;
use Faker\Extension\Extension;
/**
* A simple implementation of a container.
*
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Container implements ContainerInterface
{
/**
* @var array<string, callable|object|string>
*/
private array $definitions;
private array $services = [];
/**
* Create a container object with a set of definitions. The array value MUST
* produce an object that implements Extension.
*
* @param array<string, callable|object|string> $definitions
*/
public function __construct(array $definitions)
{
$this->definitions = $definitions;
}
/**
* Retrieve a definition from the container.
*
* @param string $id
*
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws ContainerException
* @throws NotInContainerException
*/
public function get($id): Extension
{
if (!is_string($id)) {
throw new \InvalidArgumentException(sprintf(
'First argument of %s::get() must be string',
self::class,
));
}
if (array_key_exists($id, $this->services)) {
return $this->services[$id];
}
if (!$this->has($id)) {
throw new NotInContainerException(sprintf(
'There is not service with id "%s" in the container.',
$id,
));
}
$definition = $this->definitions[$id];
$service = $this->getService($id, $definition);
if (!$service instanceof Extension) {
throw new \RuntimeException(sprintf(
'Service resolved for identifier "%s" does not implement the %s" interface.',
$id,
Extension::class,
));
}
$this->services[$id] = $service;
return $service;
}
/**
* Get the service from a definition.
*
* @param callable|object|string $definition
*/
private function getService(string $id, $definition)
{
if (is_callable($definition)) {
try {
return $definition();
} catch (\Throwable $e) {
throw new ContainerException(
sprintf('Error while invoking callable for "%s"', $id),
0,
$e,
);
}
} elseif (is_object($definition)) {
return $definition;
} elseif (is_string($definition)) {
if (class_exists($definition)) {
try {
return new $definition();
} catch (\Throwable $e) {
throw new ContainerException(sprintf('Could not instantiate class "%s"', $id), 0, $e);
}
}
throw new ContainerException(sprintf(
'Could not instantiate class "%s". Class was not found.',
$id,
));
} else {
throw new ContainerException(sprintf(
'Invalid type for definition with id "%s"',
$id,
));
}
}
/**
* Check if the container contains a given identifier.
*
* @param string $id
*
* @throws \InvalidArgumentException
*/
public function has($id): bool
{
if (!is_string($id)) {
throw new \InvalidArgumentException(sprintf(
'First argument of %s::get() must be string',
self::class,
));
}
return array_key_exists($id, $this->definitions);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Faker\Container;
use Faker\Core;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class ContainerBuilder
{
/**
* @var array<string, callable|object|string>
*/
private array $definitions = [];
/**
* @param callable|object|string $definition
*
* @throws \InvalidArgumentException
*/
public function add(string $id, $definition): self
{
if (!is_string($definition) && !is_callable($definition) && !is_object($definition)) {
throw new \InvalidArgumentException(sprintf(
'First argument to "%s::add()" must be a string, callable or object.',
self::class,
));
}
$this->definitions[$id] = $definition;
return $this;
}
public function build(): ContainerInterface
{
return new Container($this->definitions);
}
private static function defaultExtensions(): array
{
return [
Extension\BarcodeExtension::class => Core\Barcode::class,
Extension\BloodExtension::class => Core\Blood::class,
Extension\ColorExtension::class => Core\Color::class,
Extension\DateTimeExtension::class => Core\DateTime::class,
Extension\FileExtension::class => Core\File::class,
Extension\NumberExtension::class => Core\Number::class,
Extension\UuidExtension::class => Core\Uuid::class,
Extension\VersionExtension::class => Core\Version::class,
];
}
public static function withDefaultExtensions(): self
{
$instance = new self();
foreach (self::defaultExtensions() as $id => $definition) {
$instance->add($id, $definition);
}
return $instance;
}
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Faker\Container;
use Psr\Container\ContainerExceptionInterface;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class ContainerException extends \RuntimeException implements ContainerExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Faker\Container;
use Psr\Container\ContainerInterface as BaseContainerInterface;
interface ContainerInterface extends BaseContainerInterface
{
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Faker\Container;
use Psr\Container\NotFoundExceptionInterface;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class NotInContainerException extends \RuntimeException implements NotFoundExceptionInterface
{
}
+52
View File
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Calculator;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Barcode implements Extension\BarcodeExtension
{
private Extension\NumberExtension $numberExtension;
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
private function ean(int $length = 13): string
{
$code = Extension\Helper::numerify(str_repeat('#', $length - 1));
return sprintf('%s%s', $code, Calculator\Ean::checksum($code));
}
public function ean13(): string
{
return $this->ean();
}
public function ean8(): string
{
return $this->ean(8);
}
public function isbn10(): string
{
$code = Extension\Helper::numerify(str_repeat('#', 9));
return sprintf('%s%s', $code, Calculator\Isbn::checksum($code));
}
public function isbn13(): string
{
$code = '97' . $this->numberExtension->numberBetween(8, 9) . Extension\Helper::numerify(str_repeat('#', 9));
return sprintf('%s%s', $code, Calculator\Ean::checksum($code));
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Blood implements Extension\BloodExtension
{
/**
* @var string[]
*/
private array $bloodTypes = ['A', 'AB', 'B', 'O'];
/**
* @var string[]
*/
private array $bloodRhFactors = ['+', '-'];
public function bloodType(): string
{
return Extension\Helper::randomElement($this->bloodTypes);
}
public function bloodRh(): string
{
return Extension\Helper::randomElement($this->bloodRhFactors);
}
public function bloodGroup(): string
{
return sprintf(
'%s%s',
$this->bloodType(),
$this->bloodRh(),
);
}
}
+177
View File
@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
use Faker\Extension\Helper;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Color implements Extension\ColorExtension
{
private Extension\NumberExtension $numberExtension;
/**
* @var string[]
*/
private array $safeColorNames = [
'black', 'maroon', 'green', 'navy', 'olive',
'purple', 'teal', 'lime', 'blue', 'silver',
'gray', 'yellow', 'fuchsia', 'aqua', 'white',
];
/**
* @var string[]
*/
private array $allColorNames = [
'AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine',
'Azure', 'Beige', 'Bisque', 'Black', 'BlanchedAlmond',
'Blue', 'BlueViolet', 'Brown', 'BurlyWood', 'CadetBlue',
'Chartreuse', 'Chocolate', 'Coral', 'CornflowerBlue',
'Cornsilk', 'Crimson', 'Cyan', 'DarkBlue', 'DarkCyan',
'DarkGoldenRod', 'DarkGray', 'DarkGreen', 'DarkKhaki',
'DarkMagenta', 'DarkOliveGreen', 'Darkorange', 'DarkOrchid',
'DarkRed', 'DarkSalmon', 'DarkSeaGreen', 'DarkSlateBlue',
'DarkSlateGray', 'DarkTurquoise', 'DarkViolet', 'DeepPink',
'DeepSkyBlue', 'DimGray', 'DimGrey', 'DodgerBlue', 'FireBrick',
'FloralWhite', 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite',
'Gold', 'GoldenRod', 'Gray', 'Green', 'GreenYellow', 'HoneyDew',
'HotPink', 'IndianRed', 'Indigo', 'Ivory', 'Khaki', 'Lavender',
'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 'LightCoral',
'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGreen', 'LightPink',
'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray', 'LightSteelBlue',
'LightYellow', 'Lime', 'LimeGreen', 'Linen', 'Magenta', 'Maroon', 'MediumAquaMarine',
'MediumBlue', 'MediumOrchid', 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue',
'MediumSpringGreen', 'MediumTurquoise', 'MediumVioletRed', 'MidnightBlue',
'MintCream', 'MistyRose', 'Moccasin', 'NavajoWhite', 'Navy', 'OldLace', 'Olive',
'OliveDrab', 'Orange', 'OrangeRed', 'Orchid', 'PaleGoldenRod', 'PaleGreen',
'PaleTurquoise', 'PaleVioletRed', 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum',
'PowderBlue', 'Purple', 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Salmon',
'SandyBrown', 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue',
'SlateGray', 'Snow', 'SpringGreen', 'SteelBlue', 'Tan', 'Teal', 'Thistle', 'Tomato',
'Turquoise', 'Violet', 'Wheat', 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen',
];
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
/**
* @example '#fa3cc2'
*/
public function hexColor(): string
{
return '#' . str_pad(dechex($this->numberExtension->numberBetween(1, 16777215)), 6, '0', STR_PAD_LEFT);
}
/**
* @example '#ff0044'
*/
public function safeHexColor(): string
{
$color = str_pad(dechex($this->numberExtension->numberBetween(0, 255)), 3, '0', STR_PAD_LEFT);
return sprintf(
'#%s%s%s%s%s%s',
$color[0],
$color[0],
$color[1],
$color[1],
$color[2],
$color[2],
);
}
/**
* @example 'array(0,255,122)'
*
* @return int[]
*/
public function rgbColorAsArray(): array
{
$color = $this->hexColor();
return [
hexdec(substr($color, 1, 2)),
hexdec(substr($color, 3, 2)),
hexdec(substr($color, 5, 2)),
];
}
/**
* @example '0,255,122'
*/
public function rgbColor(): string
{
return implode(',', $this->rgbColorAsArray());
}
/**
* @example 'rgb(0,255,122)'
*/
public function rgbCssColor(): string
{
return sprintf(
'rgb(%s)',
$this->rgbColor(),
);
}
/**
* @example 'rgba(0,255,122,0.8)'
*/
public function rgbaCssColor(): string
{
return sprintf(
'rgba(%s,%s)',
$this->rgbColor(),
$this->numberExtension->randomFloat(1, 0, 1),
);
}
/**
* @example 'blue'
*/
public function safeColorName(): string
{
return Helper::randomElement($this->safeColorNames);
}
/**
* @example 'NavajoWhite'
*/
public function colorName(): string
{
return Helper::randomElement($this->allColorNames);
}
/**
* @example '340,50,20'
*/
public function hslColor(): string
{
return sprintf(
'%s,%s,%s',
$this->numberExtension->numberBetween(0, 360),
$this->numberExtension->numberBetween(0, 100),
$this->numberExtension->numberBetween(0, 100),
);
}
/**
* @example array(340, 50, 20)
*
* @return int[]
*/
public function hslColorAsArray(): array
{
return [
$this->numberExtension->numberBetween(0, 360),
$this->numberExtension->numberBetween(0, 100),
$this->numberExtension->numberBetween(0, 100),
];
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Coordinates implements Extension\Extension
{
private Extension\NumberExtension $numberExtension;
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
/**
* @example '77.147489'
*
* @return float Uses signed degrees format (returns a float number between -90 and 90)
*/
public function latitude(float $min = -90.0, float $max = 90.0): float
{
if ($min < -90 || $max < -90) {
throw new \LogicException('Latitude cannot be less that -90.0');
}
if ($min > 90 || $max > 90) {
throw new \LogicException('Latitude cannot be greater that 90.0');
}
return $this->randomFloat(6, $min, $max);
}
/**
* @example '86.211205'
*
* @return float Uses signed degrees format (returns a float number between -180 and 180)
*/
public function longitude(float $min = -180.0, float $max = 180.0): float
{
if ($min < -180 || $max < -180) {
throw new \LogicException('Longitude cannot be less that -180.0');
}
if ($min > 180 || $max > 180) {
throw new \LogicException('Longitude cannot be greater that 180.0');
}
return $this->randomFloat(6, $min, $max);
}
/**
* @example array('77.147489', '86.211205')
*
* @return array{latitude: float, longitude: float}
*/
public function localCoordinates(): array
{
return [
'latitude' => $this->latitude(),
'longitude' => $this->longitude(),
];
}
private function randomFloat(int $nbMaxDecimals, float $min, float $max): float
{
if ($min > $max) {
throw new \LogicException('Invalid coordinates boundaries');
}
return $this->numberExtension->randomFloat($nbMaxDecimals, $min, $max);
}
}
+217
View File
@@ -0,0 +1,217 @@
<?php
namespace Faker\Core;
use Faker\Extension\DateTimeExtension;
use Faker\Extension\GeneratorAwareExtension;
use Faker\Extension\GeneratorAwareExtensionTrait;
use Faker\Extension\Helper;
/**
* @experimental This class is experimental and does not fall under our BC promise
*
* @since 1.20.0
*/
final class DateTime implements DateTimeExtension, GeneratorAwareExtension
{
use GeneratorAwareExtensionTrait;
/**
* @var string[]
*/
private array $centuries = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI'];
private ?string $defaultTimezone = null;
/**
* Get the POSIX-timestamp of a DateTime, int or string.
*
* @param \DateTime|float|int|string $until
*
* @return false|int
*/
private function getTimestamp($until = 'now')
{
if (is_numeric($until)) {
return (int) $until;
}
if ($until instanceof \DateTime) {
return $until->getTimestamp();
}
return strtotime(empty($until) ? 'now' : $until);
}
/**
* Get a DateTime created based on a POSIX-timestamp.
*
* @param int $timestamp the UNIX / POSIX-compatible timestamp
*/
private function getTimestampDateTime(int $timestamp): \DateTime
{
return new \DateTime('@' . $timestamp);
}
private function resolveTimezone(?string $timezone): string
{
if ($timezone !== null) {
return $timezone;
}
return null === $this->defaultTimezone ? date_default_timezone_get() : $this->defaultTimezone;
}
/**
* Internal method to set the timezone on a DateTime object.
*/
private function setTimezone(\DateTime $dateTime, ?string $timezone): \DateTime
{
$timezone = $this->resolveTimezone($timezone);
return $dateTime->setTimezone(new \DateTimeZone($timezone));
}
public function dateTime($until = 'now', ?string $timezone = null): \DateTime
{
return $this->setTimezone(
$this->getTimestampDateTime($this->unixTime($until)),
$timezone,
);
}
public function dateTimeAD($until = 'now', ?string $timezone = null): \DateTime
{
$min = (PHP_INT_SIZE > 4) ? -62135597361 : -PHP_INT_MAX;
return $this->setTimezone(
$this->getTimestampDateTime($this->generator->numberBetween($min, $this->getTimestamp($until))),
$timezone,
);
}
public function dateTimeBetween($from = '-30 years', $until = 'now', ?string $timezone = null): \DateTime
{
$start = $this->getTimestamp($from);
$end = $this->getTimestamp($until);
if ($start > $end) {
throw new \InvalidArgumentException('"$from" must be anterior to "$until".');
}
$timestamp = $this->generator->numberBetween($start, $end);
return $this->setTimezone(
$this->getTimestampDateTime($timestamp),
$timezone,
);
}
public function dateTimeInInterval($from = '-30 years', string $interval = '+5 days', ?string $timezone = null): \DateTime
{
$intervalObject = \DateInterval::createFromDateString($interval);
$datetime = $from instanceof \DateTime ? $from : new \DateTime($from);
$other = (clone $datetime)->add($intervalObject);
$begin = min($datetime, $other);
$end = $datetime === $begin ? $other : $datetime;
return $this->dateTimeBetween($begin, $end, $timezone);
}
public function dateTimeThisWeek($until = 'sunday this week', ?string $timezone = null): \DateTime
{
return $this->dateTimeBetween('monday this week', $until, $timezone);
}
public function dateTimeThisMonth($until = 'last day of this month', ?string $timezone = null): \DateTime
{
return $this->dateTimeBetween('first day of this month', $until, $timezone);
}
public function dateTimeThisYear($until = 'last day of december', ?string $timezone = null): \DateTime
{
return $this->dateTimeBetween('first day of january', $until, $timezone);
}
public function dateTimeThisDecade($until = 'now', ?string $timezone = null): \DateTime
{
$year = floor(date('Y') / 10) * 10;
return $this->dateTimeBetween("first day of january $year", $until, $timezone);
}
public function dateTimeThisCentury($until = 'now', ?string $timezone = null): \DateTime
{
$year = floor(date('Y') / 100) * 100;
return $this->dateTimeBetween("first day of january $year", $until, $timezone);
}
public function date(string $format = 'Y-m-d', $until = 'now'): string
{
return $this->dateTime($until)->format($format);
}
public function time(string $format = 'H:i:s', $until = 'now'): string
{
return $this->date($format, $until);
}
public function unixTime($until = 'now'): int
{
return $this->generator->numberBetween(0, $this->getTimestamp($until));
}
public function iso8601($until = 'now'): string
{
return $this->date(\DateTime::ISO8601, $until);
}
public function amPm($until = 'now'): string
{
return $this->date('a', $until);
}
public function dayOfMonth($until = 'now'): string
{
return $this->date('d', $until);
}
public function dayOfWeek($until = 'now'): string
{
return $this->date('l', $until);
}
public function month($until = 'now'): string
{
return $this->date('m', $until);
}
public function monthName($until = 'now'): string
{
return $this->date('F', $until);
}
public function year($until = 'now'): string
{
return $this->date('Y', $until);
}
public function century(): string
{
return Helper::randomElement($this->centuries);
}
public function timezone(?string $countryCode = null): string
{
if ($countryCode) {
$timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::PER_COUNTRY, $countryCode);
} else {
$timezones = \DateTimeZone::listIdentifiers();
}
return Helper::randomElement($timezones);
}
}
+564
View File
@@ -0,0 +1,564 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class File implements Extension\FileExtension
{
/**
* MIME types from the apache.org file. Some types are truncated.
*
* @var array Map of MIME types => file extension(s)
*
* @see http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
*/
private array $mimeTypes = [
'application/atom+xml' => 'atom',
'application/ecmascript' => 'ecma',
'application/emma+xml' => 'emma',
'application/epub+zip' => 'epub',
'application/java-archive' => 'jar',
'application/java-vm' => 'class',
'application/javascript' => 'js',
'application/json' => 'json',
'application/jsonml+json' => 'jsonml',
'application/lost+xml' => 'lostxml',
'application/mathml+xml' => 'mathml',
'application/mets+xml' => 'mets',
'application/mods+xml' => 'mods',
'application/mp4' => 'mp4s',
'application/msword' => ['doc', 'dot'],
'application/octet-stream' => [
'bin',
'dms',
'lrf',
'mar',
'so',
'dist',
'distz',
'pkg',
'bpk',
'dump',
'elc',
'deploy',
],
'application/ogg' => 'ogx',
'application/omdoc+xml' => 'omdoc',
'application/pdf' => 'pdf',
'application/pgp-encrypted' => 'pgp',
'application/pgp-signature' => ['asc', 'sig'],
'application/pkix-pkipath' => 'pkipath',
'application/pkixcmp' => 'pki',
'application/pls+xml' => 'pls',
'application/postscript' => ['ai', 'eps', 'ps'],
'application/pskc+xml' => 'pskcxml',
'application/rdf+xml' => 'rdf',
'application/reginfo+xml' => 'rif',
'application/rss+xml' => 'rss',
'application/rtf' => 'rtf',
'application/sbml+xml' => 'sbml',
'application/vnd.adobe.air-application-installer-package+zip' => 'air',
'application/vnd.adobe.xdp+xml' => 'xdp',
'application/vnd.adobe.xfdf' => 'xfdf',
'application/vnd.ahead.space' => 'ahead',
'application/vnd.dart' => 'dart',
'application/vnd.data-vision.rdz' => 'rdz',
'application/vnd.dece.data' => ['uvf', 'uvvf', 'uvd', 'uvvd'],
'application/vnd.dece.ttml+xml' => ['uvt', 'uvvt'],
'application/vnd.dece.unspecified' => ['uvx', 'uvvx'],
'application/vnd.dece.zip' => ['uvz', 'uvvz'],
'application/vnd.denovo.fcselayout-link' => 'fe_launch',
'application/vnd.dna' => 'dna',
'application/vnd.dolby.mlp' => 'mlp',
'application/vnd.dpgraph' => 'dpg',
'application/vnd.dreamfactory' => 'dfac',
'application/vnd.ds-keypoint' => 'kpxx',
'application/vnd.dvb.ait' => 'ait',
'application/vnd.dvb.service' => 'svc',
'application/vnd.dynageo' => 'geo',
'application/vnd.ecowin.chart' => 'mag',
'application/vnd.enliven' => 'nml',
'application/vnd.epson.esf' => 'esf',
'application/vnd.epson.msf' => 'msf',
'application/vnd.epson.quickanime' => 'qam',
'application/vnd.epson.salt' => 'slt',
'application/vnd.epson.ssf' => 'ssf',
'application/vnd.ezpix-album' => 'ez2',
'application/vnd.ezpix-package' => 'ez3',
'application/vnd.fdf' => 'fdf',
'application/vnd.fdsn.mseed' => 'mseed',
'application/vnd.fdsn.seed' => ['seed', 'dataless'],
'application/vnd.flographit' => 'gph',
'application/vnd.fluxtime.clip' => 'ftc',
'application/vnd.hal+xml' => 'hal',
'application/vnd.hydrostatix.sof-data' => 'sfd-hdstx',
'application/vnd.ibm.minipay' => 'mpy',
'application/vnd.ibm.secure-container' => 'sc',
'application/vnd.iccprofile' => ['icc', 'icm'],
'application/vnd.igloader' => 'igl',
'application/vnd.immervision-ivp' => 'ivp',
'application/vnd.kde.karbon' => 'karbon',
'application/vnd.kde.kchart' => 'chrt',
'application/vnd.kde.kformula' => 'kfo',
'application/vnd.kde.kivio' => 'flw',
'application/vnd.kde.kontour' => 'kon',
'application/vnd.kde.kpresenter' => ['kpr', 'kpt'],
'application/vnd.kde.kspread' => 'ksp',
'application/vnd.kde.kword' => ['kwd', 'kwt'],
'application/vnd.kenameaapp' => 'htke',
'application/vnd.kidspiration' => 'kia',
'application/vnd.kinar' => ['kne', 'knp'],
'application/vnd.koan' => ['skp', 'skd', 'skt', 'skm'],
'application/vnd.kodak-descriptor' => 'sse',
'application/vnd.las.las+xml' => 'lasxml',
'application/vnd.llamagraphics.life-balance.desktop' => 'lbd',
'application/vnd.llamagraphics.life-balance.exchange+xml' => 'lbe',
'application/vnd.lotus-1-2-3' => '123',
'application/vnd.lotus-approach' => 'apr',
'application/vnd.lotus-freelance' => 'pre',
'application/vnd.lotus-notes' => 'nsf',
'application/vnd.lotus-organizer' => 'org',
'application/vnd.lotus-screencam' => 'scm',
'application/vnd.mozilla.xul+xml' => 'xul',
'application/vnd.ms-artgalry' => 'cil',
'application/vnd.ms-cab-compressed' => 'cab',
'application/vnd.ms-excel' => [
'xls',
'xlm',
'xla',
'xlc',
'xlt',
'xlw',
],
'application/vnd.ms-excel.addin.macroenabled.12' => 'xlam',
'application/vnd.ms-excel.sheet.binary.macroenabled.12' => 'xlsb',
'application/vnd.ms-excel.sheet.macroenabled.12' => 'xlsm',
'application/vnd.ms-excel.template.macroenabled.12' => 'xltm',
'application/vnd.ms-fontobject' => 'eot',
'application/vnd.ms-htmlhelp' => 'chm',
'application/vnd.ms-ims' => 'ims',
'application/vnd.ms-lrm' => 'lrm',
'application/vnd.ms-officetheme' => 'thmx',
'application/vnd.ms-pki.seccat' => 'cat',
'application/vnd.ms-pki.stl' => 'stl',
'application/vnd.ms-powerpoint' => ['ppt', 'pps', 'pot'],
'application/vnd.ms-powerpoint.addin.macroenabled.12' => 'ppam',
'application/vnd.ms-powerpoint.presentation.macroenabled.12' => 'pptm',
'application/vnd.ms-powerpoint.slide.macroenabled.12' => 'sldm',
'application/vnd.ms-powerpoint.slideshow.macroenabled.12' => 'ppsm',
'application/vnd.ms-powerpoint.template.macroenabled.12' => 'potm',
'application/vnd.ms-project' => ['mpp', 'mpt'],
'application/vnd.ms-word.document.macroenabled.12' => 'docm',
'application/vnd.ms-word.template.macroenabled.12' => 'dotm',
'application/vnd.ms-works' => ['wps', 'wks', 'wcm', 'wdb'],
'application/vnd.ms-wpl' => 'wpl',
'application/vnd.ms-xpsdocument' => 'xps',
'application/vnd.mseq' => 'mseq',
'application/vnd.musician' => 'mus',
'application/vnd.oasis.opendocument.chart' => 'odc',
'application/vnd.oasis.opendocument.chart-template' => 'otc',
'application/vnd.oasis.opendocument.database' => 'odb',
'application/vnd.oasis.opendocument.formula' => 'odf',
'application/vnd.oasis.opendocument.formula-template' => 'odft',
'application/vnd.oasis.opendocument.graphics' => 'odg',
'application/vnd.oasis.opendocument.graphics-template' => 'otg',
'application/vnd.oasis.opendocument.image' => 'odi',
'application/vnd.oasis.opendocument.image-template' => 'oti',
'application/vnd.oasis.opendocument.presentation' => 'odp',
'application/vnd.oasis.opendocument.presentation-template' => 'otp',
'application/vnd.oasis.opendocument.spreadsheet' => 'ods',
'application/vnd.oasis.opendocument.spreadsheet-template' => 'ots',
'application/vnd.oasis.opendocument.text' => 'odt',
'application/vnd.oasis.opendocument.text-master' => 'odm',
'application/vnd.oasis.opendocument.text-template' => 'ott',
'application/vnd.oasis.opendocument.text-web' => 'oth',
'application/vnd.olpc-sugar' => 'xo',
'application/vnd.oma.dd2+xml' => 'dd2',
'application/vnd.openofficeorg.extension' => 'oxt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
'application/vnd.openxmlformats-officedocument.presentationml.slide' => 'sldx',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx',
'application/vnd.openxmlformats-officedocument.presentationml.template' => 'potx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => 'xltx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx',
'application/vnd.pvi.ptid1' => 'ptid',
'application/vnd.quark.quarkxpress' => [
'qxd',
'qxt',
'qwd',
'qwt',
'qxl',
'qxb',
],
'application/vnd.realvnc.bed' => 'bed',
'application/vnd.recordare.musicxml' => 'mxl',
'application/vnd.recordare.musicxml+xml' => 'musicxml',
'application/vnd.rig.cryptonote' => 'cryptonote',
'application/vnd.rim.cod' => 'cod',
'application/vnd.rn-realmedia' => 'rm',
'application/vnd.rn-realmedia-vbr' => 'rmvb',
'application/vnd.route66.link66+xml' => 'link66',
'application/vnd.sailingtracker.track' => 'st',
'application/vnd.seemail' => 'see',
'application/vnd.sema' => 'sema',
'application/vnd.semd' => 'semd',
'application/vnd.semf' => 'semf',
'application/vnd.shana.informed.formdata' => 'ifm',
'application/vnd.shana.informed.formtemplate' => 'itp',
'application/vnd.shana.informed.interchange' => 'iif',
'application/vnd.shana.informed.package' => 'ipk',
'application/vnd.simtech-mindmapper' => ['twd', 'twds'],
'application/vnd.smaf' => 'mmf',
'application/vnd.stepmania.stepchart' => 'sm',
'application/vnd.sun.xml.calc' => 'sxc',
'application/vnd.sun.xml.calc.template' => 'stc',
'application/vnd.sun.xml.draw' => 'sxd',
'application/vnd.sun.xml.draw.template' => 'std',
'application/vnd.sun.xml.impress' => 'sxi',
'application/vnd.sun.xml.impress.template' => 'sti',
'application/vnd.sun.xml.math' => 'sxm',
'application/vnd.sun.xml.writer' => 'sxw',
'application/vnd.sun.xml.writer.global' => 'sxg',
'application/vnd.sun.xml.writer.template' => 'stw',
'application/vnd.sus-calendar' => ['sus', 'susp'],
'application/vnd.svd' => 'svd',
'application/vnd.symbian.install' => ['sis', 'sisx'],
'application/vnd.syncml+xml' => 'xsm',
'application/vnd.syncml.dm+wbxml' => 'bdm',
'application/vnd.syncml.dm+xml' => 'xdm',
'application/vnd.tao.intent-module-archive' => 'tao',
'application/vnd.tcpdump.pcap' => ['pcap', 'cap', 'dmp'],
'application/vnd.tmobile-livetv' => 'tmo',
'application/vnd.trid.tpt' => 'tpt',
'application/vnd.triscape.mxs' => 'mxs',
'application/vnd.trueapp' => 'tra',
'application/vnd.ufdl' => ['ufd', 'ufdl'],
'application/vnd.uiq.theme' => 'utz',
'application/vnd.umajin' => 'umj',
'application/vnd.unity' => 'unityweb',
'application/vnd.uoml+xml' => 'uoml',
'application/vnd.vcx' => 'vcx',
'application/vnd.visio' => ['vsd', 'vst', 'vss', 'vsw'],
'application/vnd.visionary' => 'vis',
'application/vnd.vsf' => 'vsf',
'application/vnd.wap.wbxml' => 'wbxml',
'application/vnd.wap.wmlc' => 'wmlc',
'application/vnd.wap.wmlscriptc' => 'wmlsc',
'application/vnd.webturbo' => 'wtb',
'application/vnd.wolfram.player' => 'nbp',
'application/vnd.wordperfect' => 'wpd',
'application/vnd.wqd' => 'wqd',
'application/vnd.wt.stf' => 'stf',
'application/vnd.xara' => 'xar',
'application/vnd.xfdl' => 'xfdl',
'application/voicexml+xml' => 'vxml',
'application/widget' => 'wgt',
'application/winhlp' => 'hlp',
'application/wsdl+xml' => 'wsdl',
'application/wspolicy+xml' => 'wspolicy',
'application/x-7z-compressed' => '7z',
'application/x-bittorrent' => 'torrent',
'application/x-blorb' => ['blb', 'blorb'],
'application/x-bzip' => 'bz',
'application/x-cdlink' => 'vcd',
'application/x-cfs-compressed' => 'cfs',
'application/x-chat' => 'chat',
'application/x-chess-pgn' => 'pgn',
'application/x-conference' => 'nsc',
'application/x-cpio' => 'cpio',
'application/x-csh' => 'csh',
'application/x-debian-package' => ['deb', 'udeb'],
'application/x-dgc-compressed' => 'dgc',
'application/x-director' => [
'dir',
'dcr',
'dxr',
'cst',
'cct',
'cxt',
'w3d',
'fgd',
'swa',
],
'application/x-font-ttf' => ['ttf', 'ttc'],
'application/x-font-type1' => ['pfa', 'pfb', 'pfm', 'afm'],
'application/x-font-woff' => 'woff',
'application/x-freearc' => 'arc',
'application/x-futuresplash' => 'spl',
'application/x-gca-compressed' => 'gca',
'application/x-glulx' => 'ulx',
'application/x-gnumeric' => 'gnumeric',
'application/x-gramps-xml' => 'gramps',
'application/x-gtar' => 'gtar',
'application/x-hdf' => 'hdf',
'application/x-install-instructions' => 'install',
'application/x-iso9660-image' => 'iso',
'application/x-java-jnlp-file' => 'jnlp',
'application/x-latex' => 'latex',
'application/x-lzh-compressed' => ['lzh', 'lha'],
'application/x-mie' => 'mie',
'application/x-mobipocket-ebook' => ['prc', 'mobi'],
'application/x-ms-application' => 'application',
'application/x-ms-shortcut' => 'lnk',
'application/x-ms-wmd' => 'wmd',
'application/x-ms-wmz' => 'wmz',
'application/x-ms-xbap' => 'xbap',
'application/x-msaccess' => 'mdb',
'application/x-msbinder' => 'obd',
'application/x-mscardfile' => 'crd',
'application/x-msclip' => 'clp',
'application/x-msdownload' => ['exe', 'dll', 'com', 'bat', 'msi'],
'application/x-msmediaview' => [
'mvb',
'm13',
'm14',
],
'application/x-msmetafile' => ['wmf', 'wmz', 'emf', 'emz'],
'application/x-rar-compressed' => 'rar',
'application/x-research-info-systems' => 'ris',
'application/x-sh' => 'sh',
'application/x-shar' => 'shar',
'application/x-shockwave-flash' => 'swf',
'application/x-silverlight-app' => 'xap',
'application/x-sql' => 'sql',
'application/x-stuffit' => 'sit',
'application/x-stuffitx' => 'sitx',
'application/x-subrip' => 'srt',
'application/x-sv4cpio' => 'sv4cpio',
'application/x-sv4crc' => 'sv4crc',
'application/x-t3vm-image' => 't3',
'application/x-tads' => 'gam',
'application/x-tar' => 'tar',
'application/x-tcl' => 'tcl',
'application/x-tex' => 'tex',
'application/x-tex-tfm' => 'tfm',
'application/x-texinfo' => ['texinfo', 'texi'],
'application/x-tgif' => 'obj',
'application/x-ustar' => 'ustar',
'application/x-wais-source' => 'src',
'application/x-x509-ca-cert' => ['der', 'crt'],
'application/x-xfig' => 'fig',
'application/x-xliff+xml' => 'xlf',
'application/x-xpinstall' => 'xpi',
'application/x-xz' => 'xz',
'application/x-zmachine' => 'z1',
'application/xaml+xml' => 'xaml',
'application/xcap-diff+xml' => 'xdf',
'application/xenc+xml' => 'xenc',
'application/xhtml+xml' => ['xhtml', 'xht'],
'application/xml' => ['xml', 'xsl'],
'application/xml-dtd' => 'dtd',
'application/xop+xml' => 'xop',
'application/xproc+xml' => 'xpl',
'application/xslt+xml' => 'xslt',
'application/xspf+xml' => 'xspf',
'application/xv+xml' => ['mxml', 'xhvml', 'xvml', 'xvm'],
'application/yang' => 'yang',
'application/yin+xml' => 'yin',
'application/zip' => 'zip',
'audio/adpcm' => 'adp',
'audio/basic' => ['au', 'snd'],
'audio/midi' => ['mid', 'midi', 'kar', 'rmi'],
'audio/mp4' => 'mp4a',
'audio/mpeg' => [
'mpga',
'mp2',
'mp2a',
'mp3',
'm2a',
'm3a',
],
'audio/ogg' => ['oga', 'ogg', 'spx'],
'audio/vnd.dece.audio' => ['uva', 'uvva'],
'audio/vnd.rip' => 'rip',
'audio/webm' => 'weba',
'audio/x-aac' => 'aac',
'audio/x-aiff' => ['aif', 'aiff', 'aifc'],
'audio/x-caf' => 'caf',
'audio/x-flac' => 'flac',
'audio/x-matroska' => 'mka',
'audio/x-mpegurl' => 'm3u',
'audio/x-ms-wax' => 'wax',
'audio/x-ms-wma' => 'wma',
'audio/x-pn-realaudio' => ['ram', 'ra'],
'audio/x-pn-realaudio-plugin' => 'rmp',
'audio/x-wav' => 'wav',
'audio/xm' => 'xm',
'image/bmp' => 'bmp',
'image/cgm' => 'cgm',
'image/g3fax' => 'g3',
'image/gif' => 'gif',
'image/ief' => 'ief',
'image/jpeg' => ['jpeg', 'jpg', 'jpe'],
'image/ktx' => 'ktx',
'image/png' => 'png',
'image/prs.btif' => 'btif',
'image/sgi' => 'sgi',
'image/svg+xml' => ['svg', 'svgz'],
'image/tiff' => ['tiff', 'tif'],
'image/vnd.adobe.photoshop' => 'psd',
'image/vnd.dece.graphic' => ['uvi', 'uvvi', 'uvg', 'uvvg'],
'image/vnd.dvb.subtitle' => 'sub',
'image/vnd.djvu' => ['djvu', 'djv'],
'image/vnd.dwg' => 'dwg',
'image/vnd.dxf' => 'dxf',
'image/vnd.fastbidsheet' => 'fbs',
'image/vnd.fpx' => 'fpx',
'image/vnd.fst' => 'fst',
'image/vnd.fujixerox.edmics-mmr' => 'mmr',
'image/vnd.fujixerox.edmics-rlc' => 'rlc',
'image/vnd.ms-modi' => 'mdi',
'image/vnd.ms-photo' => 'wdp',
'image/vnd.net-fpx' => 'npx',
'image/vnd.wap.wbmp' => 'wbmp',
'image/vnd.xiff' => 'xif',
'image/webp' => 'webp',
'image/x-3ds' => '3ds',
'image/x-cmu-raster' => 'ras',
'image/x-cmx' => 'cmx',
'image/x-freehand' => ['fh', 'fhc', 'fh4', 'fh5', 'fh7'],
'image/x-icon' => 'ico',
'image/x-mrsid-image' => 'sid',
'image/x-pcx' => 'pcx',
'image/x-pict' => ['pic', 'pct'],
'image/x-portable-anymap' => 'pnm',
'image/x-portable-bitmap' => 'pbm',
'image/x-portable-graymap' => 'pgm',
'image/x-portable-pixmap' => 'ppm',
'image/x-rgb' => 'rgb',
'image/x-tga' => 'tga',
'image/x-xbitmap' => 'xbm',
'image/x-xpixmap' => 'xpm',
'image/x-xwindowdump' => 'xwd',
'message/rfc822' => ['eml', 'mime'],
'model/iges' => ['igs', 'iges'],
'model/mesh' => ['msh', 'mesh', 'silo'],
'model/vnd.collada+xml' => 'dae',
'model/vnd.dwf' => 'dwf',
'model/vnd.gdl' => 'gdl',
'model/vnd.gtw' => 'gtw',
'model/vnd.mts' => 'mts',
'model/vnd.vtu' => 'vtu',
'model/vrml' => ['wrl', 'vrml'],
'model/x3d+binary' => 'x3db',
'model/x3d+vrml' => 'x3dv',
'model/x3d+xml' => 'x3d',
'text/cache-manifest' => 'appcache',
'text/calendar' => ['ics', 'ifb'],
'text/css' => 'css',
'text/csv' => 'csv',
'text/html' => ['html', 'htm'],
'text/n3' => 'n3',
'text/plain' => [
'txt',
'text',
'conf',
'def',
'list',
'log',
'in',
],
'text/prs.lines.tag' => 'dsc',
'text/richtext' => 'rtx',
'text/sgml' => ['sgml', 'sgm'],
'text/tab-separated-values' => 'tsv',
'text/troff' => [
't',
'tr',
'roff',
'man',
'me',
'ms',
],
'text/turtle' => 'ttl',
'text/uri-list' => ['uri', 'uris', 'urls'],
'text/vcard' => 'vcard',
'text/vnd.curl' => 'curl',
'text/vnd.curl.dcurl' => 'dcurl',
'text/vnd.curl.scurl' => 'scurl',
'text/vnd.curl.mcurl' => 'mcurl',
'text/vnd.dvb.subtitle' => 'sub',
'text/vnd.fly' => 'fly',
'text/vnd.fmi.flexstor' => 'flx',
'text/vnd.graphviz' => 'gv',
'text/vnd.in3d.3dml' => '3dml',
'text/vnd.in3d.spot' => 'spot',
'text/vnd.sun.j2me.app-descriptor' => 'jad',
'text/vnd.wap.wml' => 'wml',
'text/vnd.wap.wmlscript' => 'wmls',
'text/x-asm' => ['s', 'asm'],
'text/x-fortran' => ['f', 'for', 'f77', 'f90'],
'text/x-java-source' => 'java',
'text/x-opml' => 'opml',
'text/x-pascal' => ['p', 'pas'],
'text/x-nfo' => 'nfo',
'text/x-setext' => 'etx',
'text/x-sfv' => 'sfv',
'text/x-uuencode' => 'uu',
'text/x-vcalendar' => 'vcs',
'text/x-vcard' => 'vcf',
'video/3gpp' => '3gp',
'video/3gpp2' => '3g2',
'video/h261' => 'h261',
'video/h263' => 'h263',
'video/h264' => 'h264',
'video/jpeg' => 'jpgv',
'video/jpm' => ['jpm', 'jpgm'],
'video/mj2' => 'mj2',
'video/mp4' => 'mp4',
'video/mpeg' => ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'],
'video/ogg' => 'ogv',
'video/quicktime' => ['qt', 'mov'],
'video/vnd.dece.hd' => ['uvh', 'uvvh'],
'video/vnd.dece.mobile' => ['uvm', 'uvvm'],
'video/vnd.dece.pd' => ['uvp', 'uvvp'],
'video/vnd.dece.sd' => ['uvs', 'uvvs'],
'video/vnd.dece.video' => ['uvv', 'uvvv'],
'video/vnd.dvb.file' => 'dvb',
'video/vnd.fvt' => 'fvt',
'video/vnd.mpegurl' => ['mxu', 'm4u'],
'video/vnd.ms-playready.media.pyv' => 'pyv',
'video/vnd.uvvu.mp4' => ['uvu', 'uvvu'],
'video/vnd.vivo' => 'viv',
'video/webm' => 'webm',
'video/x-f4v' => 'f4v',
'video/x-fli' => 'fli',
'video/x-flv' => 'flv',
'video/x-m4v' => 'm4v',
'video/x-matroska' => ['mkv', 'mk3d', 'mks'],
'video/x-mng' => 'mng',
'video/x-ms-asf' => ['asf', 'asx'],
'video/x-ms-vob' => 'vob',
'video/x-ms-wm' => 'wm',
'video/x-ms-wmv' => 'wmv',
'video/x-ms-wmx' => 'wmx',
'video/x-ms-wvx' => 'wvx',
'video/x-msvideo' => 'avi',
'video/x-sgi-movie' => 'movie',
];
public function mimeType(): string
{
return array_rand($this->mimeTypes, 1);
}
public function extension(): string
{
$extension = $this->mimeTypes[array_rand($this->mimeTypes, 1)];
return is_array($extension) ? $extension[array_rand($extension, 1)] : $extension;
}
public function filePath(): string
{
return tempnam(sys_get_temp_dir(), 'faker');
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Number implements Extension\NumberExtension
{
public function numberBetween(int $min = 0, int $max = 2147483647): int
{
$int1 = min($min, $max);
$int2 = max($min, $max);
return mt_rand($int1, $int2);
}
public function randomDigit(): int
{
return $this->numberBetween(0, 9);
}
public function randomDigitNot(int $except): int
{
$result = $this->numberBetween(0, 8);
if ($result >= $except) {
++$result;
}
return $result;
}
public function randomDigitNotZero(): int
{
return $this->numberBetween(1, 9);
}
public function randomFloat(?int $nbMaxDecimals = null, float $min = 0, ?float $max = null): float
{
if (null === $nbMaxDecimals) {
$nbMaxDecimals = $this->randomDigit();
}
if (null === $max) {
$max = $this->randomNumber();
if ($min > $max) {
$max = $min;
}
}
if ($min > $max) {
$tmp = $min;
$min = $max;
$max = $tmp;
}
return round($min + $this->numberBetween() / mt_getrandmax() * ($max - $min), $nbMaxDecimals);
}
public function randomNumber(?int $nbDigits = null, bool $strict = false): int
{
if (null === $nbDigits) {
$nbDigits = $this->randomDigitNotZero();
}
$max = 10 ** $nbDigits - 1;
if ($max > mt_getrandmax()) {
throw new \InvalidArgumentException('randomNumber() can only generate numbers up to mt_getrandmax()');
}
if ($strict) {
return $this->numberBetween(10 ** ($nbDigits - 1), $max);
}
return $this->numberBetween(0, $max);
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Faker\Core;
use Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Uuid implements Extension\UuidExtension
{
private Extension\NumberExtension $numberExtension;
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
public function uuid3(): string
{
// fix for compatibility with 32bit architecture; each mt_rand call is restricted to 32bit
// two such calls will cause 64bits of randomness regardless of architecture
$seed = $this->numberExtension->numberBetween(0, 2147483647) . '#' . $this->numberExtension->numberBetween(0, 2147483647);
// Hash the seed and convert to a byte array
$val = md5($seed, true);
$byte = array_values(unpack('C16', $val));
// extract fields from byte array
$tLo = ($byte[0] << 24) | ($byte[1] << 16) | ($byte[2] << 8) | $byte[3];
$tMi = ($byte[4] << 8) | $byte[5];
$tHi = ($byte[6] << 8) | $byte[7];
$csLo = $byte[9];
$csHi = $byte[8] & 0x3f | (1 << 7);
// correct byte order for big edian architecture
if (pack('L', 0x6162797A) == pack('N', 0x6162797A)) {
$tLo = (($tLo & 0x000000ff) << 24) | (($tLo & 0x0000ff00) << 8)
| (($tLo & 0x00ff0000) >> 8) | (($tLo & 0xff000000) >> 24);
$tMi = (($tMi & 0x00ff) << 8) | (($tMi & 0xff00) >> 8);
$tHi = (($tHi & 0x00ff) << 8) | (($tHi & 0xff00) >> 8);
}
// apply version number
$tHi &= 0x0fff;
$tHi |= (3 << 12);
// cast to string
return sprintf(
'%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x',
$tLo,
$tMi,
$tHi,
$csHi,
$csLo,
$byte[10],
$byte[11],
$byte[12],
$byte[13],
$byte[14],
$byte[15],
);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Faker\Core;
use Faker\Extension;
use Faker\Provider\DateTime;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Version implements Extension\VersionExtension
{
private Extension\NumberExtension $numberExtension;
/**
* @var string[]
*/
private array $semverCommonPreReleaseIdentifiers = ['alpha', 'beta', 'rc'];
public function __construct(?Extension\NumberExtension $numberExtension = null)
{
$this->numberExtension = $numberExtension ?: new Number();
}
/**
* Represents v2.0.0 of the semantic versioning: https://semver.org/spec/v2.0.0.html
*/
public function semver(bool $preRelease = false, bool $build = false): string
{
return sprintf(
'%d.%d.%d%s%s',
$this->numberExtension->numberBetween(0, 9),
$this->numberExtension->numberBetween(0, 99),
$this->numberExtension->numberBetween(0, 99),
$preRelease && $this->numberExtension->numberBetween(0, 1) === 1 ? '-' . $this->semverPreReleaseIdentifier() : '',
$build && $this->numberExtension->numberBetween(0, 1) === 1 ? '+' . $this->semverBuildIdentifier() : '',
);
}
/**
* Common pre-release identifier
*/
private function semverPreReleaseIdentifier(): string
{
$ident = Extension\Helper::randomElement($this->semverCommonPreReleaseIdentifiers);
if ($this->numberExtension->numberBetween(0, 1) !== 1) {
return $ident;
}
return $ident . '.' . $this->numberExtension->numberBetween(1, 99);
}
/**
* Common random build identifier
*/
private function semverBuildIdentifier(): string
{
if ($this->numberExtension->numberBetween(0, 1) === 1) {
// short git revision syntax: https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection
return substr(sha1(Extension\Helper::lexify('??????')), 0, 7);
}
// date syntax
return DateTime::date('YmdHis');
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace Faker;
/**
* This generator returns a default value for all called properties
* and methods.
*
* @mixin Generator
*
* @deprecated Use ChanceGenerator instead
*/
class DefaultGenerator
{
protected $default;
public function __construct($default = null)
{
trigger_deprecation('fakerphp/faker', '1.16', 'Class "%s" is deprecated, use "%s" instead.', __CLASS__, ChanceGenerator::class);
$this->default = $default;
}
public function ext()
{
return $this;
}
/**
* @param string $attribute
*
* @deprecated Use a method instead.
*/
public function __get($attribute)
{
trigger_deprecation('fakerphp/faker', '1.14', 'Accessing property "%s" is deprecated, use "%s()" instead.', $attribute, $attribute);
return $this->default;
}
/**
* @param string $method
* @param array $attributes
*/
public function __call($method, $attributes)
{
return $this->default;
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace Faker;
class Documentor
{
protected $generator;
public function __construct(Generator $generator)
{
$this->generator = $generator;
}
/**
* @return array
*/
public function getFormatters()
{
$formatters = [];
$providers = array_reverse($this->generator->getProviders());
$providers[] = new Provider\Base($this->generator);
foreach ($providers as $provider) {
$providerClass = get_class($provider);
$formatters[$providerClass] = [];
$refl = new \ReflectionObject($provider);
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflmethod) {
if ($reflmethod->getDeclaringClass()->getName() == 'Faker\Provider\Base' && $providerClass != 'Faker\Provider\Base') {
continue;
}
$methodName = $reflmethod->name;
if ($reflmethod->isConstructor()) {
continue;
}
$parameters = [];
foreach ($reflmethod->getParameters() as $reflparameter) {
$parameter = '$' . $reflparameter->getName();
if ($reflparameter->isDefaultValueAvailable()) {
$parameter .= ' = ' . var_export($reflparameter->getDefaultValue(), true);
}
$parameters[] = $parameter;
}
$parameters = $parameters ? '(' . implode(', ', $parameters) . ')' : '';
try {
$example = $this->generator->format($methodName);
} catch (\InvalidArgumentException $e) {
$example = '';
}
if (is_array($example)) {
$example = "array('" . implode("', '", $example) . "')";
} elseif ($example instanceof \DateTime) {
$example = "DateTime('" . $example->format('Y-m-d H:i:s') . "')";
} elseif ($example instanceof Generator || $example instanceof UniqueGenerator) { // modifier
$example = '';
} else {
$example = var_export($example, true);
}
$formatters[$providerClass][$methodName . $parameters] = $example;
}
}
return $formatters;
}
}
@@ -0,0 +1,39 @@
<?php
namespace Faker\Extension;
/**
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface AddressExtension extends Extension
{
/**
* @example '791 Crist Parks, Sashabury, IL 86039-9874'
*/
public function address(): string;
/**
* Randomly return a real city name.
*/
public function city(): string;
/**
* @example 86039-9874
*/
public function postcode(): string;
/**
* @example 'Crist Parks'
*/
public function streetName(): string;
/**
* @example '791 Crist Parks'
*/
public function streetAddress(): string;
/**
* Randomly return a building number.
*/
public function buildingNumber(): string;
}
@@ -0,0 +1,41 @@
<?php
namespace Faker\Extension;
/**
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface BarcodeExtension extends Extension
{
/**
* Get a random EAN13 barcode.
*
* @example '4006381333931'
*/
public function ean13(): string;
/**
* Get a random EAN8 barcode.
*
* @example '73513537'
*/
public function ean8(): string;
/**
* Get a random ISBN-10 code
*
* @see http://en.wikipedia.org/wiki/International_Standard_Book_Number
*
* @example '4881416324'
*/
public function isbn10(): string;
/**
* Get a random ISBN-13 code
*
* @see http://en.wikipedia.org/wiki/International_Standard_Book_Number
*
* @example '9790404436093'
*/
public function isbn13(): string;
}
@@ -0,0 +1,30 @@
<?php
namespace Faker\Extension;
/**
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface BloodExtension extends Extension
{
/**
* Get an actual blood type
*
* @example 'AB'
*/
public function bloodType(): string;
/**
* Get a random resis value
*
* @example '+'
*/
public function bloodRh(): string;
/**
* Get a full blood group
*
* @example 'AB+'
*/
public function bloodGroup(): string;
}
@@ -0,0 +1,63 @@
<?php
namespace Faker\Extension;
/**
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface ColorExtension extends Extension
{
/**
* @example '#fa3cc2'
*/
public function hexColor(): string;
/**
* @example '#ff0044'
*/
public function safeHexColor(): string;
/**
* @example 'array(0,255,122)'
*
* @return int[]
*/
public function rgbColorAsArray(): array;
/**
* @example '0,255,122'
*/
public function rgbColor(): string;
/**
* @example 'rgb(0,255,122)'
*/
public function rgbCssColor(): string;
/**
* @example 'rgba(0,255,122,0.8)'
*/
public function rgbaCssColor(): string;
/**
* @example 'blue'
*/
public function safeColorName(): string;
/**
* @example 'NavajoWhite'
*/
public function colorName(): string;
/**
* @example '340,50,20'
*/
public function hslColor(): string;
/**
* @example array(340, 50, 20)
*
* @return int[]
*/
public function hslColorAsArray(): array;
}
@@ -0,0 +1,21 @@
<?php
namespace Faker\Extension;
/**
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface CompanyExtension extends Extension
{
/**
* @example 'Acme Ltd'
*/
public function company(): string;
/**
* @example 'Ltd'
*/
public function companySuffix(): string;
public function jobTitle(): string;
}
@@ -0,0 +1,14 @@
<?php
namespace Faker\Extension;
/**
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface CountryExtension extends Extension
{
/**
* @example 'Japan'
*/
public function country(): string;
}
@@ -0,0 +1,242 @@
<?php
namespace Faker\Extension;
/**
* FakerPHP extension for Date-related randomization.
*
* Functions accepting a date string use the `strtotime()` function internally.
*
* @experimental
*
* @since 1.20.0
*/
interface DateTimeExtension
{
/**
* Get a DateTime object between January 1, 1970, and `$until` (defaults to "now").
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*
* @example DateTime('2005-08-16 20:39:21')
*/
public function dateTime($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a DateTime object for a date between January 1, 0001, and now.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @example DateTime('1265-03-22 21:15:52')
*
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeAD($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a DateTime object a random date between `$from` and `$until`.
* Accepts date strings that can be recognized by `strtotime()`.
*
* @param \DateTime|string $from defaults to 30 years ago
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeBetween($from = '-30 years', $until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a DateTime object based on a random date between `$from` and an interval.
* Accepts date string that can be recognized by `strtotime()`.
*
* @param \DateTime|int|string $from defaults to 30 years ago
* @param string $interval defaults to 5 days after
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeInInterval($from = '-30 years', string $interval = '+5 days', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current week.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone zone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisWeek($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current month.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisMonth($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current year.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisYear($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current decade.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisDecade($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date time object somewhere inside the current century.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
* @param string|null $timezone timezone for generated date, fallback to `DateTime::$defaultTimezone` and `date_default_timezone_get()`.
*
* @see \DateTimeZone
* @see http://php.net/manual/en/timezones.php
* @see http://php.net/manual/en/function.date-default-timezone-get.php
*/
public function dateTimeThisCentury($until = 'now', ?string $timezone = null): \DateTime;
/**
* Get a date string between January 1, 1970, and `$until`.
*
* @param string $format DateTime format
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @see https://www.php.net/manual/en/datetime.format.php
*/
public function date(string $format = 'Y-m-d', $until = 'now'): string;
/**
* Get a time string (24h format by default).
*
* @param string $format DateTime format
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @see https://www.php.net/manual/en/datetime.format.php
*/
public function time(string $format = 'H:i:s', $until = 'now'): string;
/**
* Get a UNIX (POSIX-compatible) timestamp between January 1, 1970, and `$until`.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*/
public function unixTime($until = 'now'): int;
/**
* Get a date string according to the ISO-8601 standard.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*/
public function iso8601($until = 'now'): string;
/**
* Get a string containing either "am" or "pm".
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example 'am'
*/
public function amPm($until = 'now'): string;
/**
* Get a localized random day of the month.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example '16'
*/
public function dayOfMonth($until = 'now'): string;
/**
* Get a localized random day of the week.
*
* Uses internal DateTime formatting, hence PHP's internal locale will be used (change using `setlocale()`).
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example 'Tuesday'
*
* @see setlocale
* @see https://www.php.net/manual/en/function.setlocale.php Set a different output language
*/
public function dayOfWeek($until = 'now'): string;
/**
* Get a random month (numbered).
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example '7'
*/
public function month($until = 'now'): string;
/**
* Get a random month.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @see setlocale
* @see https://www.php.net/manual/en/function.setlocale.php Set a different output language
*
* @example 'September'
*/
public function monthName($until = 'now'): string;
/**
* Get a random year between 1970 and `$until`.
*
* @param \DateTime|int|string $until maximum timestamp, defaults to "now"
*
* @example '1987'
*/
public function year($until = 'now'): string;
/**
* Get a random century, formatted as Roman numerals.
*
* @example 'XVII'
*/
public function century(): string;
/**
* Get a random timezone, uses `\DateTimeZone::listIdentifiers()` internally.
*
* @param string|null $countryCode two-letter ISO 3166-1 compatible country code
*
* @example 'Europe/Rome'
*/
public function timezone(?string $countryCode = null): string;
}
+14
View File
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Faker\Extension;
/**
* An extension is the only way to add new functionality to Faker.
*
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface Extension
{
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Faker\Extension;
/**
* @experimental This class is experimental and does not fall under our BC promise
*/
final class ExtensionNotFound extends \LogicException
{
}
@@ -0,0 +1,28 @@
<?php
namespace Faker\Extension;
/**
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface FileExtension extends Extension
{
/**
* Get a random MIME type
*
* @example 'video/avi'
*/
public function mimeType(): string;
/**
* Get a random file extension (without a dot)
*
* @example avi
*/
public function extension(): string;
/**
* Get a full path to a new real file on the system.
*/
public function filePath(): string;
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Faker\Extension;
use Faker\Generator;
/**
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface GeneratorAwareExtension extends Extension
{
/**
* This method MUST be implemented in such a way as to retain the
* immutability of the extension, and MUST return an instance that has the
* new Generator.
*/
public function withGenerator(Generator $generator): Extension;
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Faker\Extension;
use Faker\Generator;
/**
* A helper trait to be used with GeneratorAwareExtension.
*/
trait GeneratorAwareExtensionTrait
{
/**
* @var Generator|null
*/
private $generator;
/**
* @return static
*/
public function withGenerator(Generator $generator): Extension
{
$instance = clone $this;
$instance->generator = $generator;
return $instance;
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace Faker\Extension;
/**
* A class with some methods that may make building extensions easier.
*
* @experimental This class is experimental and does not fall under our BC promise
*/
final class Helper
{
/**
* Returns a random element from a passed array.
*/
public static function randomElement(array $array)
{
if ($array === []) {
return null;
}
return $array[array_rand($array, 1)];
}
/**
* Replaces all hash sign ('#') occurrences with a random number
* Replaces all percentage sign ('%') occurrences with a non-zero number.
*
* @param string $string String that needs to bet parsed
*/
public static function numerify(string $string): string
{
// instead of using randomDigit() several times, which is slow,
// count the number of hashes and generate once a large number
$toReplace = [];
if (($pos = strpos($string, '#')) !== false) {
for ($i = $pos, $last = strrpos($string, '#', $pos) + 1; $i < $last; ++$i) {
if ($string[$i] === '#') {
$toReplace[] = $i;
}
}
}
if ($nbReplacements = count($toReplace)) {
$maxAtOnce = strlen((string) mt_getrandmax()) - 1;
$numbers = '';
$i = 0;
while ($i < $nbReplacements) {
$size = min($nbReplacements - $i, $maxAtOnce);
$numbers .= str_pad((string) mt_rand(0, 10 ** $size - 1), $size, '0', STR_PAD_LEFT);
$i += $size;
}
for ($i = 0; $i < $nbReplacements; ++$i) {
$string[$toReplace[$i]] = $numbers[$i];
}
}
return self::replaceWildcard($string, '%', static function () {
return mt_rand(1, 9);
});
}
/**
* Replaces all question mark ('?') occurrences with a random letter.
*
* @param string $string String that needs to bet parsed
*/
public static function lexify(string $string): string
{
return self::replaceWildcard($string, '?', static function () {
return chr(mt_rand(97, 122));
});
}
/**
* Replaces hash signs ('#') and question marks ('?') with random numbers and letters
* An asterisk ('*') is replaced with either a random number or a random letter.
*
* @param string $string String that needs to bet parsed
*/
public static function bothify(string $string): string
{
$string = self::replaceWildcard($string, '*', static function () {
return mt_rand(0, 1) === 1 ? '#' : '?';
});
return self::lexify(self::numerify($string));
}
private static function replaceWildcard(string $string, string $wildcard, callable $callback): string
{
if (($pos = strpos($string, $wildcard)) === false) {
return $string;
}
for ($i = $pos, $last = strrpos($string, $wildcard, $pos) + 1; $i < $last; ++$i) {
if ($string[$i] === $wildcard) {
$string[$i] = call_user_func($callback);
}
}
return $string;
}
}
@@ -0,0 +1,53 @@
<?php
namespace Faker\Extension;
/**
* @experimental This interface is experimental and does not fall under our BC promise
*/
interface NumberExtension extends Extension
{
/**
* Returns a random number between $int1 and $int2 (any order)
*
* @param int $min default to 0
* @param int $max defaults to 32 bit max integer, ie 2147483647
*
* @example 79907610
*/
public function numberBetween(int $min, int $max): int;
/**
* Returns a random number between 0 and 9
*/
public function randomDigit(): int;
/**
* Generates a random digit, which cannot be $except
*/
public function randomDigitNot(int $except): int;
/**
* Returns a random number between 1 and 9
*/
public function randomDigitNotZero(): int;
/**
* Return a random float number
*
* @example 48.8932
*/
public function randomFloat(?int $nbMaxDecimals, float $min, ?float $max): float;
/**
* Returns a random integer with 0 to $nbDigits digits.
*
* The maximum value returned is mt_getrandmax()
*
* @param int|null $nbDigits Defaults to a random number between 1 and 9
* @param bool $strict Whether the returned number should have exactly $nbDigits
*
* @example 79907610
*/
public function randomNumber(?int $nbDigits, bool $strict): int;
}

Some files were not shown because too many files have changed in this diff Show More