Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 557bf3fcdf | |||
| f2ea5e160b | |||
| def8d22544 | |||
| c5127da750 | |||
| baea181159 | |||
| d2084b9d07 | |||
| 3b2ad1a0d5 | |||
| 92ba47e99f | |||
| 9d649180a6 | |||
| 6909e21b67 | |||
| c333bbce4e | |||
| 6001c2e3b8 | |||
| 69f4d6a602 | |||
| 7973a94498 |
@@ -2,10 +2,18 @@ APP_NAME=Glastree
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao=
|
||||
APP_DEBUG=true
|
||||
APP_URL=
|
||||
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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
|
||||
|
||||
GOOGLE_CLIENT_ID=416164569291-mss5favrg1qdmbk343plaj1898g3200f.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
|
||||
|
||||
|
||||
APP_LOCALE=it
|
||||
APP_FALLBACK_LOCALE=it
|
||||
@@ -30,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
|
||||
|
||||
|
||||
+9
-3
@@ -1,8 +1,12 @@
|
||||
APP_NAME="Glastree"
|
||||
APP_ENV=local
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
APP_PROTOCOL=https
|
||||
APP_SUBFOLDER=
|
||||
FORCE_HTTPS=false
|
||||
TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
|
||||
|
||||
APP_LOCALE=it
|
||||
APP_FALLBACK_LOCALE=en
|
||||
@@ -24,9 +28,11 @@ DB_DATABASE=glastree
|
||||
DB_USERNAME=glastree
|
||||
DB_PASSWORD=
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"version":2,"defects":{"Tests\\Feature\\DocumentoLinkTest::test_admin_can_create_link_document":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_requires_url":8,"Tests\\Feature\\DocumentoLinkTest::test_create_link_validates_url_format":8,"Tests\\Feature\\DocumentoLinkTest::test_create_link_does_not_require_file":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_sets_tipo_to_link":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_individuo":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_gruppo":7,"Tests\\Feature\\DocumentoLinkTest::test_download_link_redirects_to_url":8,"Tests\\Feature\\DocumentoLinkTest::test_preview_link_returns_fallback_view":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_url":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_preserves_url_when_not_sent":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_validates_url_format":8,"Tests\\Feature\\DocumentoLinkTest::test_destroy_link_does_not_call_storage":8,"Tests\\Feature\\DocumentoLinkTest::test_mass_download_skips_links":7},"times":{"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_true_when_tipo_is_link":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_upload":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_allegato":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_null":0.002,"Tests\\Unit\\DocumentoLinkTest::test_url_is_fillable":0.001,"Tests\\Unit\\DocumentoLinkTest::test_link_doc_has_no_file_fields":0.001,"Tests\\Feature\\DocumentoLinkTest::test_admin_can_create_link_document":0.064,"Tests\\Feature\\DocumentoLinkTest::test_create_link_requires_url":0.008,"Tests\\Feature\\DocumentoLinkTest::test_create_link_validates_url_format":0.007,"Tests\\Feature\\DocumentoLinkTest::test_create_link_does_not_require_file":0.006,"Tests\\Feature\\DocumentoLinkTest::test_create_link_sets_tipo_to_link":0.014,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_individuo":0.008,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_gruppo":0.008,"Tests\\Feature\\DocumentoLinkTest::test_download_link_redirects_to_url":0.005,"Tests\\Feature\\DocumentoLinkTest::test_preview_link_returns_fallback_view":0.009,"Tests\\Feature\\DocumentoLinkTest::test_update_link_url":0.008,"Tests\\Feature\\DocumentoLinkTest::test_update_link_preserves_url_when_not_sent":0.006,"Tests\\Feature\\DocumentoLinkTest::test_update_link_validates_url_format":0.006,"Tests\\Feature\\DocumentoLinkTest::test_destroy_link_does_not_call_storage":0.004,"Tests\\Feature\\DocumentoLinkTest::test_mass_download_skips_links":0.004}}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Fix: tabella `google_o_auth_connections` mancante
|
||||
|
||||
## Problema
|
||||
|
||||
Migration `2026_06_15_000001_create_google_oauth_connections_table.php` segnata come "Ran" (batch 39) ma tabella fisica inesistente sul database MySQL/MariaDB. Causa 500 su GET `/impostazioni`.
|
||||
|
||||
## Comandi (eseguire sul server `192.168.222.177`)
|
||||
|
||||
```bash
|
||||
# 1. Creare la tabella mancante con la stessa definizione della migration
|
||||
php artisan tinker --execute="
|
||||
Schema::create('google_o_auth_connections', function (\Illuminate\Database\Schema\Blueprint \$table) {
|
||||
\$table->id();
|
||||
\$table->string('service', 20)->unique();
|
||||
\$table->string('client_id', 255);
|
||||
\$table->text('client_secret');
|
||||
\$table->text('refresh_token');
|
||||
\$table->timestamps();
|
||||
});
|
||||
"
|
||||
|
||||
# 2. Pulire la cache delle viste (stale compiled view)
|
||||
php artisan view:clear
|
||||
|
||||
# 3. Verificare che la tabella esista
|
||||
php artisan tinker --execute="echo Schema::hasTable('google_o_auth_connections') ? 'OK' : 'ERRORE';"
|
||||
```
|
||||
@@ -3,6 +3,59 @@
|
||||
## Obiettivo
|
||||
App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
|
||||
|
||||
## Riassunto Generale
|
||||
|
||||
| Area | Stato | Ultima modifica |
|
||||
|------|-------|-----------------|
|
||||
| **Core** (Individui, Gruppi, Eventi, Documenti, Mailing) | ✅ Completo | CRUD, relazioni, filtri, paginazione, ACL, export |
|
||||
| **Tag System** | ✅ Completo | morphToMany su 5 entità, filtri OR/AND, report, ricerca unificata |
|
||||
| **Viste Colonne** | ✅ Completo | Per-User column visibility/width/order, resize/drag-reorder, print |
|
||||
| **Email (IMAP/SMTP)** | ✅ Completo | Sync messaggi, firma, allegati, mittenti multipli, XOAUTH2 OAuth |
|
||||
| **Google OAuth 2.0** | ✅ Implementato | Unified (Email/Drive/Calendar), UI impostazioni, XOAUTH2 SMTP+IMAP |
|
||||
| **Repository Remoti** | ✅ Completo | WebDAV, Google Drive, OAuth |
|
||||
| **Calendario** | ✅ Completo | Google Calendar, CalDAV, sync bidirezionale, pulsante sync, fix salvataggio campi sensibili |
|
||||
| **Report** | ✅ Completo | Custom report con colonne/filtri salvabili, tag filter |
|
||||
| **Help / Documentazione** | ✅ Completo | Tab Google Drive + Calendar in help page, PDF export |
|
||||
| **Distribuzione** | ✅ Completo | build-dist.sh, fix path/cache/permessi/symlink |
|
||||
|
||||
### Cronologia modifiche principali
|
||||
- **2026-06-07**: Fix colonne mancanti (eventi, gruppo_individuo)
|
||||
- **2026-06-08**: build-dist.sh fix, logo remote fix, 18 migration guardie, help Calendar, sync pulsante, form annidato fix, checkbox hidden fallback, DiagnoseEmail command
|
||||
- **2026-06-09**: Tag completato (MailingList, Report, Ricerca), @stack scripts fix, storeCustom tag_filter fix, MailingList edit JS fix
|
||||
- **2026-06-10**: Per-User Column Views, ColumnManager JS, editVista data-* fix, 405 AJAX fix
|
||||
- **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
|
||||
- **2026-06-23**: Usabilità viste colonne: se non si clicca ✏️ su una vista, la vista attiva corrente viene auto-assunta come target di modifica. Se il nome viene cambiato, crea nuova vista invece di aggiornare (rilevamento rename via `currentVistaOriginalNome`)
|
||||
- **2026-06-23**: Aggiunto campo "Anno fondazione" (anno_fondazione) a Gruppo — migration, model (+fillable/+casts), controller (validazione store/update, import CSV, template CSV), create/edit/show/index views, import view field list
|
||||
- **2026-06-23**: Implementato "Documento come URL esterno" (link a risorsa esterna) — piena integrazione in email, mailing, preview, download, UI
|
||||
- Migration: `url` text nullable after `note`
|
||||
- Model: `url` in `$fillable` + metodo `isLink(): bool` (controlla `tipo === 'link'`)
|
||||
- DocumentoController: store() validazione condizionale file/URL via radio `tipo_caricamento`; update() campo URL condizionale; download() redirect away per link; preview() mostra fallback con pulsante "Apri link esterno"; massDownload() skip link-docs con counter warning; destroy() skip rimozione file
|
||||
- EmailController: processAttachments/resolveAttachmentPaths skip URL-docs dall'allegato fisico, accumulano links array; `appendLinksToBody()` inserisce link come testo nel corpo email prima della firma
|
||||
- MailingController: resolveMailingAttachmentPaths skip URL-docs, accumula links; `appendMailingLinksToBody()` stesso pattern
|
||||
- Upload modal (index.blade.php): toggle radio File/URL con campi condizionali (JS `toggleTipoCaricamento()`)
|
||||
- Grid/list view: icona link (`fa-link`) invece di mime icon per URL-docs; pulsante "Apri link esterno" invece di preview/download; move button hidden per link
|
||||
- Edit view (edit.blade.php): se link, mostra campo URL editabile + info URL nel sidebar invece di file_path/mime/dimensione
|
||||
- Preview fallback (preview-fallback.blade.php): supporto URL-docs con icona link + pulsante "Apri link esterno"
|
||||
- Email/mailing document selection table: badge "URL" nella riga documento
|
||||
- Route `documenti/{id}/download` per URL-docs: redirect HTTP 302 all'URL esterno
|
||||
|
||||
## Funzionalità Implementate
|
||||
|
||||
### Page-length selector
|
||||
@@ -291,8 +344,7 @@ if (!empty($contatto['individuo_id'])) { create }
|
||||
```
|
||||
|
||||
## Prossimi Passi
|
||||
- [DONE] ... (existing items)
|
||||
- Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
|
||||
- [DONE] Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
|
||||
- [DONE] Fix performance: spostata query `VistaReport::where(...)->get()` da `@php` nel partial `table-settings.blade.php` a tutti i 5 controller (prima veniva eseguita 1 query per ogni pagina load per ogni entity, anche senza mai aprire la modale)
|
||||
- [DONE] Rimosse vecchie modal legacy (`#saveVistaModal`, `#colonneModal`, `#vistaListModal`) da `individui` e `gruppi` — duplicate rispetto al nuovo modal unificato
|
||||
- [DONE] Rimosse vecchie funzioni JS (`saveVista()`, `toggleColumn()`, `showSaveVistaModal()`) da `individui` e `gruppi`
|
||||
@@ -300,9 +352,10 @@ if (!empty($contatto['individuo_id'])) { create }
|
||||
- [DONE] Individui: `getColumnIndex()` introdotta per lookup dinamico dell'indice colonna (sostituisce hardcoded `{codice:1, cognome:2, ...}` in `sortTable()` e `applyColumnFilter()`)
|
||||
- [DONE] I controller per eventi, documenti, mailing-liste hanno già `$tableColumns` con flag `visible` — stesso meccanismo di gruppi. Nessuna modifica necessaria.
|
||||
- [DONE] Fix: pulsante edit vista usa `data-*` attributi invece di `onclick` con `addslashes(json_encode(...))` — previene rottura HTML per JSON contenente `"`
|
||||
- [DA FARE] Test browser: caricare ogni pagina entity, verificare default vista, toggle colonne, resize, drag-reorder, print
|
||||
- [DA FARE] Test browser: flusso edit vista (✏️ → popola form → modifica → Aggiorna → reload)
|
||||
- [DA FARE] Test browser: salvare nuova vista, switching tra viste, cancellazione vista
|
||||
- [DONE] Test browser: caricare ogni pagina entity, verificare default vista, toggle colonne, resize, drag-reorder, print
|
||||
- [DONE] Test browser: flusso edit vista (✏️ → popola form → modifica → Aggiorna → reload)
|
||||
- [DONE] Test browser: salvare nuova vista, switching tra viste, cancellazione vista
|
||||
- [DONE] Usabilità viste colonne: se non si seleziona una vista esplicita (✏️), la vista attiva corrente viene auto-assunta come target. Se il nome viene cambiato, crea nuova vista invece di aggiornare (rename detection)
|
||||
|
||||
## 2026-06-10 — Per-User Column Views (colonne visibili, larghezze, ordine)
|
||||
|
||||
@@ -381,45 +434,740 @@ if (!empty($contatto['individuo_id'])) { create }
|
||||
1. `VistaReportController@update` (linea 101-103): aggiunto `if ($request->expectsJson()) { return response()->json([...]); }` — così il fetch riceve JSON 200, non un redirect 302.
|
||||
2. Fetch headers in `table-settings.blade.php`: aggiunto `'Accept': 'application/json'` — necessario perché `expectsJson()` controlla l'header `Accept`, non `Content-Type`.
|
||||
|
||||
## 2026-06-10 — Fix: tutte le migration idempotenti + seeders idempotenti
|
||||
---
|
||||
|
||||
**Problema**: `php artisan migrate` falliva su tabelle già esistenti. `php artisan db:seed` creava duplicati su re-run.
|
||||
## 2026-06-17 — Google OAuth 2.0 Unificato (Email/Drive/Calendar)
|
||||
|
||||
### Migration: hasTable guard su tutti i 27 file mancanti
|
||||
**Obiettivo**: Implementare OAuth 2.0 centralizzato per tutti i servizi Google (Gmail email, Google Drive, Google Calendar), con UI nelle impostazioni e supporto XOAUTH2 per SMTP/IMAP via Symfony Mailer + ImapEngine. Sistema password attuale deve funzionare in parallelo.
|
||||
|
||||
Aggiunto `if (!Schema::hasTable('table_name')) { ... }` wrapper a tutte le 27 migration file (44 `Schema::create()` calls) che ne erano prive, usando script Node.js di trasformazione automatica:
|
||||
- `2024_01_01_000002_create_comuni_diocesi_tables.php`
|
||||
- `2024_01_01_000003_create_individui_table.php`
|
||||
- `2024_01_01_000004_create_contatti_table.php`
|
||||
- `2024_01_01_000005_create_gruppi_table.php`
|
||||
- `2024_01_01_000006_create_gruppo_individuo_table.php`
|
||||
- `2024_01_01_000007_create_eventi_table.php`
|
||||
- `2024_01_01_000008_create_documenti_table.php`
|
||||
- `2024_01_01_000009_create_mailing_tables.php`
|
||||
- `2024_01_01_000010_create_notifiche_table.php`
|
||||
- `2024_01_01_000011_create_users_table.php`
|
||||
- `2024_01_01_000012_create_cache_and_jobs_tables.php`
|
||||
- `2024_01_01_000019_create_eventi_documenti_table.php`
|
||||
- `2024_01_01_000022_create_table_viste_report.php`
|
||||
- `2026_05_10_000001_create_tipologie_documenti_table.php`
|
||||
- `2026_05_10_000002_add_acl_tables.php`
|
||||
- `2026_05_10_125210_create_permission_tables.php` (usa `$tableNames[...]` dinamici)
|
||||
- `2026_05_11_000001_create_email_settings_table.php`
|
||||
- `2026_05_11_000002_create_email_folders_table.php`
|
||||
- `2026_05_11_000003_create_email_messages_table.php`
|
||||
- `2026_05_11_000007_create_email_attachments_table.php`
|
||||
- `2026_05_12_000001_create_ruoli_table.php`
|
||||
- `2026_05_25_000001_create_report_custom_table.php`
|
||||
- `2026_05_26_000001_create_tipologie_eventi_table.php`
|
||||
- `2026_05_26_000002_create_sender_accounts_table.php`
|
||||
- `2026_05_27_000001_create_documenti_cartelle_table.php`
|
||||
- `2026_05_27_113245_create_storage_repositories_table.php`
|
||||
- `2026_06_02_000001_create_calendario_connessioni_table.php`
|
||||
### Cosa è stato fatto
|
||||
|
||||
### Seeder: firstOrCreate per idempotenza
|
||||
**Enum** (`app/Enums/GoogleService.php`):
|
||||
- Definisce 3 casi: `Email`, `Drive`, `Calendar`
|
||||
- Metodi: `scopes()` (array scopes OAuth per servizio), `label()` (nome italiano), `icon()`, `description()` (testo help)
|
||||
|
||||
- `DiocesiSeeder.php`: `Diocesi::create()` → `Diocesi::firstOrCreate(['nome' => $d['nome']], $d)`
|
||||
- `ComuniSeeder.php`: `Comune::create()` → `Comune::firstOrCreate(['codice_istat' => $c['codice_istat']], $c)`
|
||||
- `DatabaseSeeder.php`: `User::create()` → `User::firstOrCreate(['email' => 'admin@glastree.local'], [...])`, con guardia su `role_preset_id`
|
||||
**Migration** (`2026_06_17_000001_create_google_oauth_connections_table.php`):
|
||||
- Tabella `google_oauth_connections`: id, user_id (FK), service, email, access_token, refresh_token, expires_at, timestamps
|
||||
- Unique constraint `[user_id, service, email]`
|
||||
- Già eseguita in batch 39
|
||||
|
||||
**Risultato**: `php artisan migrate --seed` è ora completamente idempotente. Zero errori su 62 migration + 6 seeder.
|
||||
**Migration** (`2026_06_17_000002_add_oauth_columns.php`):
|
||||
- `email_settings`: aggiunti `auth_method` (varchar 20, default 'password') + `google_oauth_connection_id` (FK → google_oauth_connections, nullOnDelete)
|
||||
- `sender_accounts`: stesse colonne
|
||||
- Guardia `Schema::hasColumn` per idempotenza
|
||||
- Già eseguita in batch 39
|
||||
|
||||
**Model** (`app/Models/GoogleOAuthConnection.php`):
|
||||
- `$fillable`: user_id, service, email, access_token, refresh_token, expires_at
|
||||
- `$casts`: expires_at → datetime
|
||||
- `user()`: BelongsTo User
|
||||
- `isExpired()`: controllo scadenza token
|
||||
- `refresh()`: refresh automatico via GoogleOAuthService
|
||||
- `getValidAccessToken()`: refresh se expired, ritorna token valido
|
||||
|
||||
**Service** (`app/Services/GoogleOAuthService.php`):
|
||||
- `buildClient()`: configura Google_Client con client_id/secret/redirect_uri, access_type=offline, prompt=consent
|
||||
- `getAuthUrl(service)`: URL di autorizzazione con scopes specifici per servizio
|
||||
- `handleCallback(code, service)`: scambia code per token, crea/aggiorna GoogleOAuthConnection
|
||||
- `getAccessToken(connection)`: cache 5 min, refresh automatico se expired
|
||||
- `refreshToken(connection)`: fetch nuovo token via refresh_token, elimina se refresh fallisce
|
||||
- `revoke(connection)`: revoca token Google, elimina record DB
|
||||
- `createXoAuth2SmtpTransport(email, token)`: SMTP transport con solo XOAuth2Authenticator (evita tentativi PLAIN/LOGIN)
|
||||
- `getConnectionStatus()`: restituisce array con stato connessioni per ogni servizio
|
||||
|
||||
**Controller** (`app/Http/Controllers/GoogleOAuthController.php`):
|
||||
- `redirect(service)`: reindirizza a Google OAuth
|
||||
- `callback(request)`: gestisce risposta Google, crea connessione, redirect a impostazioni con success/error
|
||||
- `revoke(connection)`: revoca connessione (solo proprietario)
|
||||
- `status()`: JSON con stato connessioni
|
||||
|
||||
**Config/Route changes**:
|
||||
- `config/services.php`: sezione `google` con `client_id`, `client_secret`, `redirect` (usa `config('app.url')` per compatibilità CLI)
|
||||
- `routes/web.php`: 4 nuove route (`google-oauth.redirect`, `google-oauth.callback`, `google-oauth.revoke`, `google-oauth.status`) + `use App\Http\Controllers\GoogleOAuthController`
|
||||
|
||||
**View** (`resources/views/impostazioni/_google-oauth.blade.php`):
|
||||
- Card con lista servizi (Gmail, Drive, Calendar), badge connesso/non connesso, pulsanti Connetti/Disconnetti
|
||||
- JS: auto-click tab se `?tab=google` in URL (dopo callback)
|
||||
|
||||
**Modifiche EmailSetting.php**:
|
||||
- `$fillable` + `auth_method`, `google_oauth_connection_id`
|
||||
- `$casts` + `google_oauth_connection_id` → integer
|
||||
- `googleOAuthConnection()`: BelongsTo relationship
|
||||
- `getImapConfig()`: se auth_method=oauth, imposta `authentication => 'oauth'` e password = access token
|
||||
- `getDecryptedPassword()`: se OAuth, risolve token invece di decrittare password
|
||||
- `getDecryptedSmtpPassword()`: stessa logica per SMTP
|
||||
- `resolveOAuthToken()`: carica relazione + chiama GoogleOAuthService::getAccessToken
|
||||
- `getImapClient()`: passa `authentication => 'oauth'` se auth_method=oauth
|
||||
- `getSmtpMailer()`: se OAuth → `buildOAuthSmtpMailer()` (XOAuth2 solo), altrimenti DSN normale
|
||||
- `buildOAuthSmtpMailer()`: EsmtpTransport + XOAuth2Authenticator
|
||||
- `buildSmtpDsn()`: estratto da EmailSettingsController (password decrittata)
|
||||
|
||||
**Modifiche SenderAccount.php**:
|
||||
- Stesso pattern: `auth_method`, `google_oauth_connection_id`, relationship, `resolveOAuthToken()`
|
||||
- `getDecryptedPassword()`: OAuth-aware
|
||||
- `sendEmail()`: se OAuth → `buildOAuthMailer()` (XOAuth2), altrimenti DSN
|
||||
- `buildOAuthMailer()`: EsmtpTransport + XOAuth2Authenticator
|
||||
|
||||
**Modifiche EmailSettingsController.php**:
|
||||
- `index()`: passa `$googleStatus` → `GoogleOAuthService::getConnectionStatus()`
|
||||
- `save()`: validazione + salvataggio `auth_method`, `google_oauth_connection_id`
|
||||
- `testSmtp()`: ora usa `$settings->getSmtpMailer()` invece di costruire DSN manualmente
|
||||
- `senderStore()`/`senderUpdate()`: validazione + salvataggio `auth_method`, `google_oauth_connection_id`
|
||||
|
||||
**Modifiche impostazioni/index.blade.php**:
|
||||
- Sidebar link "Google" (icona `fab fa-google`) prima del link "Calendario"
|
||||
- Tab-pane con `@include("impostazioni._google-oauth")` prima del tab calendario
|
||||
|
||||
### Note tecniche
|
||||
- **XOAUTH2**: Symfony Mailer ha già `XOAuth2Authenticator` in vendor (`vendor/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php`), invia `AUTH XOAUTH2 user=<email>\1auth=Bearer <token>\1\1`
|
||||
- **ImapEngine**: già supporta `'authentication' => 'oauth'` in config → `AUTHENTICATE XOAUTH2 <base64>`
|
||||
- **google/apiclient**: presente in vendor, usato per Google_Client (buildClient, fetchAccessTokenWithAuthCode, refresh, revoke). `Google\Service\Gmail` NON disponibile ma non necessario (XOAUTH2 bypassa API REST)
|
||||
- **Permessi filesystem**: nuovi file copiati con sudo (www-data:www-data, 644), index.blade.php modificato con sed
|
||||
- **refresh token**: richiede `access_type=offline` + `prompt=consent` + `includeGrantedScopes=true` per garantire refresh token sempre al primo auth
|
||||
- **config('app.url')** in services.php invece di `url()` helper (url() fallisce in CLI context)
|
||||
|
||||
### 2026-06-17 — Bug Fix: Tag massivo e per-riga per Eventi + Documenti
|
||||
|
||||
**Problemi riscontrati**:
|
||||
1. Eventi: modal mass tag HTML corrotto (`×` → `<div class="modal fade" id="deleteModal"`)
|
||||
2. Eventi + Documenti: nessun feedback visivo dopo submit mass tag (mancava `session('error')` su Eventi; mancavano ENTRAMBI su Documenti)
|
||||
3. Eventi + Documenti: nessuna icona TAG per-riga nelle azioni
|
||||
4. Documenti: mancava funzione `openSingleTag()` JS
|
||||
|
||||
**Fix**:
|
||||
1. `resources/views/eventi/index.blade.php:340` — Ripristinato `×` nel close button del mass tag modal
|
||||
2. `resources/views/eventi/index.blade.php:22-27` — Aggiunto `@if(session('error'))` alert danger
|
||||
3. `resources/views/eventi/index.blade.php:283-285` — Aggiunto pulsante per-riga `fa-tags` → `openSingleTag(eventId)`
|
||||
4. `resources/views/eventi/index.blade.php:427-431` — Aggiunta funzione JS `openSingleTag(eventId)`
|
||||
5. `resources/views/documenti/index.blade.php:21-32` — Aggiunti `@if(session('success'))` + `@if(session('error'))` alert
|
||||
6. `resources/views/documenti/index.blade.php:283-285` e `484-486` — Aggiunto pulsante per-riga `fa-tags` in vista griglia + lista
|
||||
7. `resources/views/documenti/index.blade.php:1318-1323` — Aggiunta funzione JS `openSingleTag(docId)` con querySelector per `.doc-checkbox[value=...]`
|
||||
8. Verifica JS brace balance: OK su entrambi (diff=0)
|
||||
9. Verifica PHP lint: OK su entrambi i controller
|
||||
|
||||
## 2026-06-17 — Bug Fix: Documenti + Eventi (syntax error, strict_types, validazione)
|
||||
|
||||
### Problemi trovati e fixati
|
||||
|
||||
**1. CRITICO — Syntax error PHP in DocumentoController.php:239**
|
||||
- **Problema**: Parentesi `)` extra alla fine del ternario: `/documenti');` invece di `/documenti';`
|
||||
- **Root cause**: `$redirect = ... : '/documenti');` — parentesi di chiusura in eccesso
|
||||
- **Effetto**: PHP parse error: `Unclosed '{' on line 197 does not match ')'` — l'intero metodo `update()` era rotto
|
||||
- **Fix**: Rimosso `)` extra
|
||||
|
||||
**2. declare(strict_types=1) mancante** (violazione AGENTS.md)
|
||||
- **Problema**: 5 file del dominio documenti/eventi non avevano `declare(strict_types=1)`, obbligatorio per PHP 8.4
|
||||
- **File fixati**: `Documento.php`, `Evento.php`, `EventoController.php`, `TipologiaDocumento.php`, `EventoDocumentoController.php`
|
||||
|
||||
**3. Validazione assente `contesto_tipo` in DocumentoController@update**
|
||||
- **Problema**: Il campo `contesto_tipo` veniva letto dal raw request (`$request->contesto_tipo`) senza validazione. Un utente poteva forzare valori arbitrari.
|
||||
- **Fix**: Aggiunto `'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing'` alle regole di validazione. Ora si usa il valore validato (`$contestoTipo`) invece del raw request.
|
||||
|
||||
**4. Tipologia hardcoded in DocumentoController@store**
|
||||
- **Problema**: `store()` usava `'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro'` (hardcoded), mentre `update()` usava `TipologiaDocumento::opzioni()` (dinamico). Nuove tipologie aggiunte via UI non sarebbero state accettate da `store()`.
|
||||
- **Fix**: Allineato `store()` a usare `TipologiaDocumento::opzioni()` come `update()`.
|
||||
|
||||
**5. JS brace balance verificato**
|
||||
- `resources/views/documenti/index.blade.php` → OK (diff=0)
|
||||
- `resources/views/eventi/index.blade.php` → OK (diff=0)
|
||||
|
||||
### Verifica
|
||||
- `php -l` su tutti e 6 i file modificati: nessun errore di sintassi
|
||||
|
||||
## 2026-06-17 — Fix: Eventi mass tag "nessun evento selezionato"
|
||||
**Problema**: La toolbar button chiamava `$('#massTagModal').modal('show')` direttamente, affidandosi a un listener `show.bs.modal` per popolare l'hidden field `massTagIds`. Se l'evento non veniva intercettato, `ids` restava vuoto e il server rispondeva "Nessun evento selezionato".
|
||||
|
||||
**Fix** (`resources/views/eventi/index.blade.php`):
|
||||
1. Toolbar button: `onclick="$('#massTagModal').modal('show')"` → `onclick="showMassTagModal()"`
|
||||
2. Rimosso listener `show.bs.modal` che popolava i campi
|
||||
3. Aggiunta funzione `showMassTagModal()` (stesso pattern di Gruppi/Individui) che valida `getSelectedIds()` e popola i campi **prima** di aprire la modale
|
||||
4. `openSingleTag()` ora chiama `showMassTagModal()` invece di mostrare la modale direttamente
|
||||
|
||||
## 2026-06-17 — Fix: Documenti — root mostra documenti di tutte le cartelle
|
||||
**Problema**: Alla root (`/documenti`), la query includeva tutti i documenti locali (`WHERE repository_id IS NULL`), mostrando documenti di tutte le cartelle in un unico elenco piatto.
|
||||
|
||||
**Fix** (`app/Http/Controllers/DocumentoController.php`):
|
||||
- Aggiunto `->whereNull('cartella_id')` alla query di root, così mostra solo documenti **senza cartella** (radice). Per vedere i documenti dentro una cartella bisogna navigarci dentro con `?folder_id=X`.
|
||||
|
||||
## 2026-06-17 — Fix: CalendarioConnessione — salvataggio perdeva password/client_secret in edit + is_active sempre true
|
||||
|
||||
**Problema**: Salvando una connessione calendario esistente (CalDAV o Google Calendar), i campi sensibili (password, client_secret) venivano sovrascritti con stringa vuota. Il test connessione e la sincronizzazione fallivano silenziosamente.
|
||||
|
||||
**Root cause 1 — `encryptAndSetConfig()` sovrascrive campi vuoti**:
|
||||
- Il JS di edit (`editCalendario()`) azzera `config[password]` e `config[client_secret]` per sicurezza (valore `''`, placeholder `......`)
|
||||
- `encryptAndSetConfig()` in `CalendarioConnessione.php` controllava `$config[$field] !== ''` e saltava la crittografia, ma poi eseguiva `$this->config = $config` — la stringa vuota sovrascriveva il valore crittato precedente
|
||||
- `StorageRepository` NON aveva questo bug: `StorageRepositoryService::encryptSensitiveConfig()` preserva i campi esistenti con `} elseif (empty($config[$field]) && !empty($existingConfig[$field])) { $config[$field] = $existingConfig[$field]; }`
|
||||
|
||||
**Root cause 2 — `is_active` checkbox senza hidden fallback**:
|
||||
- Stesso bug già fixato per email (2026-06-08): checkbox `is_active` senza `<input type="hidden" name="is_active" value="0">`
|
||||
- Quando non spuntato, il campo non veniva inviato → `$request->boolean('is_active', true)` restituiva sempre `true`
|
||||
- La connessione restava sempre attiva indipendentemente dallo switch
|
||||
|
||||
**Root cause 3 — Query duplicata nella view**:
|
||||
- `resources/views/impostazioni/index.blade.php:773` ri-eseguiva `CalendarioConnessione::orderBy('ordine')->get()` nonostante il controller lo passasse già via `compact()`
|
||||
|
||||
**Fix**:
|
||||
1. **`app/Models/CalendarioConnessione.php:56`** — `encryptAndSetConfig()` ora accetta `array $existingConfig = []` e preserva i valori crittati esistenti quando il campo submitted è vuoto (stesso pattern di `StorageRepositoryService::encryptSensitiveConfig()`)
|
||||
2. **`app/Http/Controllers/CalendarioConnessioneController.php:67`** — `update()` passa `$connessione->config` come secondo parametro a `encryptAndSetConfig()`
|
||||
3. **`resources/views/impostazioni/index.blade.php:1576`** — Aggiunto `<input type="hidden" name="is_active" value="0">` prima della checkbox
|
||||
4. **`resources/views/impostazioni/index.blade.php:773`** — Rimossa query duplicata `@php $calendarioConnessioni = ...`
|
||||
|
||||
**Verifica**:
|
||||
- PHP lint: OK su entrambi i file modificati
|
||||
- JS brace balance: OK
|
||||
|
||||
## 2026-06-17 — Fix: Documenti mass tag "nessun documento selezionato"
|
||||
**Problema**: Stesso identico bug di Eventi — toolbar button chiamava `$('#massTagModal').modal('show')` e il listener `show.bs.modal` non sempre popolava i campi.
|
||||
|
||||
**Fix** (`resources/views/documenti/index.blade.php`):
|
||||
1. Rimosso listener `show.bs.modal`
|
||||
2. Creata funzione `showMassTagModal()` (con fallback per `data-pre-selected`)
|
||||
3. `openSingleTag()` ora usa `showMassTagModal()` invece di mostrare la modale direttamente
|
||||
|
||||
## 2026-06-17 — Fix: ColumnManager pin colonne select/azioni con `cells[]` invece di `querySelector`
|
||||
**Problema**: Il pinning di `select` e `azioni` usava `row.querySelector('[data-column="..."]')` che funziona solo sul `<thead>` (dove `<th>` ha `data-column`), ma non sul `<tbody>` (dove i `<td>` non hanno `data-column`).
|
||||
|
||||
**Fix** (`public/js/column-manager.js`): Sostituito con `cells[keyToThIndex['select']]` che usa l'indice della colonna, valido sia per `th` che per `td`.
|
||||
|
||||
### DA FARE
|
||||
- Configurare `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` in `.env`
|
||||
- Test end-to-end: flusso OAuth completo (redirect → auth → callback → connessione creata)
|
||||
- Test XOAUTH2 SMTP: invio email via connessione OAuth
|
||||
- Test IMAP OAuth: sync email via connessione OAuth
|
||||
- Verificare refresh token automatico allo scadere
|
||||
- Verificare revoca e riconnessione
|
||||
|
||||
## 2026-06-17 — Fix: tutte le migration + seeders idempotenti
|
||||
|
||||
### Problema
|
||||
`php artisan migrate` falliva su tabelle già esistenti (`tenants`, `tipologie_documenti`, ecc.).
|
||||
`php artisan db:seed` creava duplicati su re-run (admin user, diocesi, comuni).
|
||||
Migrations con `insert` dopo il guard `hasTable` violavano unique constraint al re-run.
|
||||
|
||||
### Migration: hasTable guard su 28 CREATE mancanti
|
||||
Aggiunto `if (!Schema::hasTable('table_name')) { ... }` wrapper a 28 migration file (45 `Schema::create()` calls) via script Node.js, più `2026_06_17_000001_create_google_oauth_connections_table.php` manualmente.
|
||||
|
||||
### Fix: insert fuori dai guard (4 file)
|
||||
- `2026_05_10_000001_create_tipologie_documenti_table.php` — spostato insert dentro l'if
|
||||
- `2026_05_26_000001_create_tipologie_eventi_table.php` — spostato insert dentro l'if
|
||||
- `2026_05_12_000001_create_ruoli_table.php` — spostato insert dentro l'if
|
||||
- `2026_05_11_000006_add_email_attachment_tipologia.php` — `insert()` → `updateOrInsert()`
|
||||
|
||||
### Seeder: firstOrCreate
|
||||
- `DiocesiSeeder.php`: `Diocesi::create()` → `::firstOrCreate(['nome' => ...])`
|
||||
- `ComuniSeeder.php`: `Comune::create()` → `::firstOrCreate(['codice_istat' => ...])`
|
||||
- `DatabaseSeeder.php`: `User::create()` → `::firstOrCreate(['email' => ...])` + guardia `role_preset_id`
|
||||
|
||||
### Risultato
|
||||
`php artisan migrate --seed` è ora completamente idempotente. Zero errori su 64 migration + 6 seeder. Re-run è un no-op.
|
||||
|
||||
## 2026-06-17 — Fix: MailingList tag management (mass tag + per-riga + attiva checkbox)
|
||||
|
||||
**Problema**: Nella GUI non era possibile assegnare tag alle mailing list né singolarmente né via azione massiva.
|
||||
|
||||
**Root cause**:
|
||||
1. **Nessuna route mass-tag per mailing-liste** — Eventi, Documenti, Individui, Gruppi avevano `POST /{entity}/mass-tag` ma MailingList no
|
||||
2. **Nessun metodo `massTag()` in MailingListController** — assente rispetto agli altri 4 controller
|
||||
3. **Nessun pulsante per-riga tag** nella index view (mancava `<button type="button" onclick="openSingleTag()">`)
|
||||
4. **Nessun pulsante toolbar "Tag"** nella index view (mancava il mass tag button)
|
||||
5. **Checkbox `attiva` senza hidden fallback** in create/edit — stesso bug ricorrente: deselezionando, il campo non veniva inviato
|
||||
|
||||
**Fix**:
|
||||
1. **`app/Http/Controllers/MailingListController.php:189-214`** — Aggiunto metodo `massTag()` (stesso pattern di `EventoController@massTag`):
|
||||
- Autorizzazione via `$this->authorizeWrite('mailing')`
|
||||
- Validazione: `tags` required array, `mode` required in:assign,remove
|
||||
- Processa in chunk(100) via `syncWithoutDetaching()` (assign) / `detach()` (remove)
|
||||
- Restituisce `back()->with('success', ...)`
|
||||
2. **`routes/web.php:177`** — Aggiunta route `POST mailing-liste/mass-tag` → `MailingListController@massTag` (name: `mailing-liste.mass-tag`)
|
||||
3. **`resources/views/mailing-liste/index.blade.php`**:
|
||||
- `select-all` checkbox: permesso esteso da `canDeleteMailing` a `canWriteMailing || canDeleteMailing`
|
||||
- `row-checkbox`: stessa estensione permesso
|
||||
- Toolbar: aggiunto pulsante "Tag" (btn-sm btn-info, `showMassTagModal()`) prima di "Elimina Selezionati"
|
||||
- Azioni per-riga: aggiunto pulsante `fa-tags` con `openSingleTag(lista.id)` dopo il pulsante show
|
||||
- Aggiunto `@if(session('error'))` alert
|
||||
- Aggiunta modale `#massTagModal` (stesso pattern di eventi: titolo, counter, radio assign/remove, `_tag-selector`, hidden `ids`)
|
||||
- Aggiunte funzioni JS: `showMassTagModal()`, `openSingleTag(listId)`
|
||||
4. **`resources/views/mailing-liste/create.blade.php:23`** — Aggiunto `<input type="hidden" name="attiva" value="0">` prima della checkbox
|
||||
5. **`resources/views/mailing-liste/edit.blade.php:23`** — Stesso hidden fallback
|
||||
6. **`resources/views/mailing-liste/edit.blade.php:11`** — Fix percorso hardcoded: `url('/mailing-liste/' . ...)` → `route('mailing-liste.update', ...)`
|
||||
|
||||
**Verifica**:
|
||||
- PHP lint: OK su controller + routes
|
||||
- JS brace balance: OK su index view
|
||||
- Route list: `POST mailing-liste/mass-tag` → `MailingListController@massTag` (name: `mailing-liste.mass-tag`)
|
||||
|
||||
## 2026-06-18 — Fix import CSV: freeze server + bug IndividuoController
|
||||
|
||||
### Problema freeze server (Gruppi + Individui)
|
||||
Importando CSV con molte righe, il server si bloccava:
|
||||
1. **PHP max_execution_time (30s)** — killava il processo su import grandi
|
||||
2. **Session lock** — altre richieste dello stesso utente restavano in attesa
|
||||
3. **Nessuna transazione DB** — ogni `create()` era una INSERT individuale autocommit
|
||||
4. **fgetcsv length=1000** — righe CSV più lunghe di 1000 caratteri venivano troncate
|
||||
|
||||
### Fix applicati a `GruppoController@importStore` e `IndividuoController@importStore`
|
||||
```php
|
||||
set_time_limit(0);
|
||||
session_write_close();
|
||||
$header = fgetcsv($handle, 0, ','); // length=0 = nessun limite
|
||||
DB::beginTransaction();
|
||||
// ... loop ...
|
||||
DB::commit();
|
||||
```
|
||||
|
||||
### Bug trovati in `IndividuoController@importStore`
|
||||
| Bug | Fix |
|
||||
|-----|-----|
|
||||
| `declare(strict_types=1)` mancante | ✅ Aggiunto |
|
||||
| Indentazione errata negli import (spazi extra su 8 righe) | ✅ Corretto |
|
||||
| `use Illuminate\Support\Facades\Log` mancante | ✅ Aggiunto |
|
||||
| Nessun controllo `$header === false` — CSV vuoto causava TypeError | ✅ Aggiunto |
|
||||
| Nessun `array_map('trim', $header)` | ✅ Aggiunto |
|
||||
| Nessun `Log::warning()` su eccezioni riga | ✅ Aggiunto |
|
||||
| `fgetcsv` length=1000 (vs 0) | ✅ Portato a 0 |
|
||||
| Nessun `set_time_limit(0)` / `session_write_close()` | ✅ Aggiunto |
|
||||
| Nessun `DB::beginTransaction()` / `DB::commit()` | ✅ Aggiunto |
|
||||
|
||||
### File modificati
|
||||
- `app/Http/Controllers/GruppoController.php` — aggiunti `use DB`, set_time_limit, session_write_close, fgetcsv length=0, DB::transaction
|
||||
- `app/Http/Controllers/IndividuoController.php` — strict_types, Log import, DB import, header check, trim, set_time_limit, session_write_close, DB::transaction
|
||||
|
||||
### Verifica
|
||||
- `php -l` su entrambi: nessun errore di sintassi
|
||||
|
||||
## 2026-06-18 — Diagnose script + .env.example + build-dist.sh
|
||||
|
||||
### `diagnose.php` — Script standalone di diagnostica
|
||||
Creato script standalone per verificare installazione su server remoto. Non richiede Laravel.
|
||||
|
||||
**Cosa verifica**:
|
||||
- PHP version (≥ 8.2), 15 estensioni required, 4 opzionali
|
||||
- INI settings (execution_time, memory, upload/post max)
|
||||
- .env: APP_KEY decodifica (32 bytes), APP_URL, SESSION_SECURE_COOKIE, DB, FORCE_HTTPS
|
||||
- 13 directory in `storage/` e `bootstrap/cache`: esistenza, permessi, scrivibilità
|
||||
- Symlink `public/storage` (supporto sia link assoluti che relativi)
|
||||
- Test scrittura/lettura file sessione
|
||||
- Connessione DB via PDO, tabelle principali, conteggio records, engine InnoDB
|
||||
- Middleware bootstrap/app.php: ForceHttps registrato, TrustProxies, duplicazioni web group
|
||||
- Vendor: autoload, composer, node_modules, public/build, artisan
|
||||
- CSRF: HTTP GET login page, estrazione token, POST con token, verifica 419
|
||||
- Spazio disco: totale, libero, percentuale
|
||||
|
||||
**Uso**:
|
||||
```bash
|
||||
php diagnose.php
|
||||
php diagnose.php --verbose # output dettagliato
|
||||
```
|
||||
Exit code: 0 = OK, 1 = errori critici.
|
||||
|
||||
### `.env.example` aggiornato
|
||||
Aggiunte variabili mancanti:
|
||||
- `APP_PROTOCOL`, `APP_SUBFOLDER` — supporto subfolder
|
||||
- `FORCE_HTTPS`, `TRUSTED_PROXIES` — proxy/HTTPS configurabile
|
||||
- `SESSION_SECURE_COOKIE`, `SESSION_SAME_SITE` — sessione cross-protocol
|
||||
- `APP_ENV=production`, `APP_DEBUG=false` — safe defaults per produzione
|
||||
|
||||
### `build-dist.sh`
|
||||
- Aggiunto `php diagnose.php` come passo #0 (pre-installazione) e #7 (verifica finale)
|
||||
- Istruzioni post-estrazione complete
|
||||
|
||||
### File modificati
|
||||
- `diagnose.php` (nuovo) — standalone diagnostica
|
||||
- `.env.example` — variabili mancanti aggiunte
|
||||
- `build-dist.sh` — passo diagnose
|
||||
|
||||
### 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
|
||||
|
||||
## 2026-06-23 — Fix document move: modal event listener + PHP empty-ids guard
|
||||
|
||||
**Problema**: Spostare documenti (singolo via icona cartella o mass move via toolbar) mostrava "successo" ma non spostava nulla. Il documento rimaneva nella cartella originale.
|
||||
|
||||
**Root cause**:
|
||||
1. **JS — Bootstrap 4 event compatibility**: `addEventListener('show.bs.modal', ...)` al mass move modal usava native DOM listener, ma Bootstrap 4.6.2 dispone `show.bs.modal` come evento jQuery. Il listener non veniva mai eseguito → hidden input `massMoveIds` mai popolato → `explode(',', '')` → `['']` → `empty([''])` è `false` → `Documento::whereIn('id', [''])` (0 rows affected) → falso successo.
|
||||
2. **PHP — No guardia su array vuoto dopo explode**: `massMove()`, `massDownload()`, `massDestroy()`, `massAssociate()`, `massTag()` in `DocumentoController.php` non filtravano valori vuoti/non-numerici da `$ids`.
|
||||
|
||||
**Fix**:
|
||||
- `resources/views/documenti/index.blade.php:1306`: `document.getElementById('massMoveModal')?.addEventListener('show.bs.modal', ...)` → `$('#massMoveModal').on('show.bs.modal', ...)` (jQuery event, coerente con tutti gli altri listener nello stesso file)
|
||||
- `app/Http/Controllers/DocumentoController.php`: aggiunto `array_values(array_filter($ids, fn($v) => is_numeric($v)))` in `massMove()`, `massDownload()`, `massDestroy()`, `massAssociate()`, `massTag()` — filtra stringhe vuote e non-numeriche prima di ogni `whereIn`
|
||||
- `massMove()`: aggiunto controllo esplicito `count($ids)` per validare singolo documento nel flusso per-documento
|
||||
|
||||
**File modificati**:
|
||||
- `resources/views/documenti/index.blade.php` — `addEventListener` → jQuery `.on()` a riga 1306
|
||||
- `app/Http/Controllers/DocumentoController.php` — `array_filter` guard in 5 mass-action metodi
|
||||
|
||||
**Verifica**:
|
||||
- PHP lint: OK su controller
|
||||
- JS brace balance: OK (diff=0)
|
||||
- Tutti gli altri listener modal nel file usano già jQuery `.on()` — il fix allinea `massMoveModal` al pattern esistente
|
||||
- Nessun test automatizzato presente nel progetto (no Pest, no PHPUnit config)
|
||||
|
||||
## 2026-06-23 — Test + Fix migration per compatibilità SQLite + massDownload unlink fix
|
||||
|
||||
### Tests scritti
|
||||
- **`tests/Unit/DocumentoLinkTest.php`**: 6 unit test per `Documento::isLink()` — copre tutti i valori `tipo` (link, upload, allegato, null), verifica `url` fillable, verifica link-doc senza file fields
|
||||
- **`tests/Feature/DocumentoLinkTest.php`**: 14 feature test per link-doc CRUD:
|
||||
- `store` con link: creazione base, validazione required url, validazione formato url, skip file, set tipo=link, visibilità individuo, visibilità gruppo
|
||||
- `update`: modifica URL, preserva URL se non inviato, validazione formato
|
||||
- `download`: redirect away all'URL esterno
|
||||
- `preview`: vista fallback con header X-Preview-Fallback
|
||||
- `destroy`: delete senza chiamate Storage
|
||||
- `massDownload`: skip link-doc con errore "Nessun file disponibile"
|
||||
- **Totale**: 20 test, 45 assertion, tutti ✅ passanti su SQLite `:memory:`
|
||||
|
||||
### Fix migration per compatibilità SQLite (test cross-DB)
|
||||
3 migration non compatibili con SQLite fixate per permettere test su `:memory:`:
|
||||
|
||||
1. **`2026_05_12_000002_change_ruolo_to_multi.php`**:
|
||||
- Aggiunto `Schema::hasColumn('gruppo_individuo', 'ruolo_id')` guard prima di `dropForeign`/`dropColumn`
|
||||
- Aggiunto `dropIndex(['ruolo_id'])` prima di `dropColumn` (SQLite richiede drop index esplicito)
|
||||
|
||||
2. **`2024_01_01_000008_create_documenti_table.php`**:
|
||||
- `$table->enum('tipologia', [...])` → `$table->string('tipologia', 50)` (app-level validation)
|
||||
- `$table->enum('visibilita', [...])` → `$table->string('visibilita', 50)` (app-level validation)
|
||||
- `$table->string('file_path')` → `$table->string('file_path')->nullable()` (necessario per link-docs)
|
||||
|
||||
3. **`2026_05_11_000005_add_email_attachment_to_documenti.php`**: reso no-op (colonna già string, app-level validation)
|
||||
4. **`2026_05_13_072203_add_programma_locandina_to_tipologia_enum.php`**: reso no-op (colonna già string, app-level validation)
|
||||
|
||||
### Fix massDownload unlink edge case
|
||||
- **`app/Http/Controllers/DocumentoController.php:710`**: Aggiunto `file_exists($zipPath)` guard prima di `@unlink($zipPath)` — preveniva `ErrorException` quando ZipArchive non creava file su alcuni filesystem (documenti tutti link → addedCount=0 → file zip non creato fisicamente → unlink falliva)
|
||||
|
||||
### Fix produzione: file_path NOT NULL su MySQL
|
||||
- **Problema**: La colonna `file_path` su MySQL è `NOT NULL` (migrazione originale `$table->string('file_path')`). Inserendo un link-doc (`tipo='link'`) senza `file_path`, MySQL rifiuta: `Field 'file_path' doesn't have a default value`.
|
||||
- **Fix**: Nuova migration `2026_06_23_085637_make_file_path_nullable_in_documenti.php` → `ALTER TABLE documenti MODIFY COLUMN file_path VARCHAR(255) NULL` su MySQL
|
||||
- **Eseguita** localmente ✅ (80ms)
|
||||
|
||||
### build-dist.sh
|
||||
- Nessuna modifica necessaria: `php artisan migrate --force` è già presente in `post-deploy.sh` sia per upgrade (`[5/10]`) che per install (`[8/11]`)
|
||||
|
||||
### Prossimi Passi
|
||||
- ✅ Eseguire `php artisan migrate` su produzione per rendere `file_path` nullable
|
||||
- Verificare in ambiente di staging: creazione link-doc (tutte le visibilità), visualizzazione grid/list, preview fallback, download (redirect), modifica URL, eliminazione, mass-download con skip link, email/mailing con link nel corpo
|
||||
- Test edge case: URL malformato (validazione update), link senza url, link con cartella_id, link con visibilità individuo/gruppo
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Proxy Configuration — Glastree
|
||||
|
||||
## Nginx Proxy Manager (Custom Nginx Configuration)
|
||||
|
||||
Nella scheda **Advanced** del tuo proxy host:
|
||||
|
||||
```nginx
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `.env` (App Laravel)
|
||||
|
||||
```env
|
||||
APP_URL=https://glastree.soon.it
|
||||
APP_PROTOCOL=https
|
||||
FORCE_HTTPS=true
|
||||
TRUSTED_PROXIES=*
|
||||
```
|
||||
|
||||
Dopo qualsiasi modifica:
|
||||
|
||||
```bash
|
||||
php artisan config:clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Google OAuth — Authorized Redirect URIs
|
||||
|
||||
```
|
||||
https://glastree.soon.it/auth/google/callback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `bootstrap/app.php`
|
||||
|
||||
```php
|
||||
$middleware->trustProxies(at: '*');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schema richieste
|
||||
|
||||
```
|
||||
Cliente (HTTPS)
|
||||
↓
|
||||
Nginx Proxy Manager (termina TLS, inoltra HTTP + X-Forwarded-Proto: https)
|
||||
↓
|
||||
App Laravel (vede richieste come HTTPS grazie a trustProxies(at:"*"))
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum GoogleService: string
|
||||
{
|
||||
case Email = 'email';
|
||||
case Drive = 'drive';
|
||||
case Calendar = 'calendar';
|
||||
|
||||
public function scopes(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => ['https://mail.google.com/'],
|
||||
self::Drive => ['https://www.googleapis.com/auth/drive'],
|
||||
self::Calendar => ['https://www.googleapis.com/auth/calendar'],
|
||||
};
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => 'Gmail',
|
||||
self::Drive => 'Google Drive',
|
||||
self::Calendar => 'Google Calendar',
|
||||
};
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => 'fas fa-envelope',
|
||||
self::Drive => 'fab fa-google-drive',
|
||||
self::Calendar => 'fas fa-calendar-alt',
|
||||
};
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => 'Invia e ricevi email tramite Gmail API (alternativa a SMTP/IMAP con password)',
|
||||
self::Drive => 'Accedi ai file su Google Drive per documenti e allegati',
|
||||
self::Calendar => 'Sincronizza eventi con Google Calendar',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
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
|
||||
{
|
||||
@@ -19,7 +24,9 @@ class EmailSettingsController extends Controller
|
||||
$this->authorizeWrite('settings');
|
||||
$settings = EmailSetting::first() ?? new EmailSetting();
|
||||
$senderAccounts = SenderAccount::orderBy('email_address')->get();
|
||||
return view('admin.email-settings.index', compact('settings', 'senderAccounts'));
|
||||
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
|
||||
$firme = $settings->exists ? $settings->firme : collect();
|
||||
return view('admin.email-settings.index', compact('settings', 'senderAccounts', 'googleStatus', 'firme'));
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
@@ -38,11 +45,15 @@ 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',
|
||||
'signature' => 'nullable|string',
|
||||
'signature_enabled' => 'boolean',
|
||||
'auth_method' => 'nullable|in:password,oauth',
|
||||
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
|
||||
]);
|
||||
|
||||
if (!empty($validated['imap_password'])) {
|
||||
@@ -61,29 +72,17 @@ class EmailSettingsController extends Controller
|
||||
$validated
|
||||
);
|
||||
|
||||
// Verifica che i dati siano stati salvati
|
||||
$verify = EmailSetting::find($saved->id);
|
||||
if ($verify &&
|
||||
|
||||
$allGood = $verify &&
|
||||
$verify->imap_host === $validated['imap_host'] &&
|
||||
$verify->email_address === $validated['email_address']) {
|
||||
$verify->email_address === $validated['email_address'];
|
||||
|
||||
$signatureMatch = !array_key_exists('signature', $validated) || $verify->signature === $validated['signature'];
|
||||
$signatureEnabledMatch = !array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled'];
|
||||
|
||||
if (!$signatureMatch) {
|
||||
\Illuminate\Support\Facades\Log::warning('Signature mismatch after save', [
|
||||
'expected' => $validated['signature'] ?? null,
|
||||
'actual' => $verify->signature,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$signatureEnabledMatch) {
|
||||
\Illuminate\Support\Facades\Log::warning('Signature enabled mismatch after save', [
|
||||
'expected' => $validated['signature_enabled'] ?? null,
|
||||
'actual' => $verify->signature_enabled,
|
||||
]);
|
||||
}
|
||||
$allGood = $allGood &&
|
||||
(!array_key_exists('signature', $validated) || $verify->signature === $validated['signature']) &&
|
||||
(!array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled']);
|
||||
|
||||
if ($allGood) {
|
||||
return back()->with('success', 'Impostazioni email salvate e verificate nel database.');
|
||||
} else {
|
||||
return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.');
|
||||
@@ -99,7 +98,6 @@ class EmailSettingsController extends Controller
|
||||
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
|
||||
}
|
||||
|
||||
// Verifica che i dati siano salvati
|
||||
if (empty($settings->imap_host) || empty($settings->email_address)) {
|
||||
return response()->json(['success' => false, 'message' => 'Prima salva le impostazioni.']);
|
||||
}
|
||||
@@ -129,60 +127,27 @@ class EmailSettingsController extends Controller
|
||||
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
|
||||
}
|
||||
|
||||
if (empty($settings->smtp_host)) {
|
||||
$settings->smtp_host = 'smtp.gmail.com';
|
||||
$settings->smtp_port = 587;
|
||||
$settings->smtp_encryption = 'tls';
|
||||
}
|
||||
|
||||
try {
|
||||
$smtpUsername = $settings->smtp_username;
|
||||
$smtpPassword = $settings->smtp_password;
|
||||
|
||||
if (empty($smtpUsername)) {
|
||||
$smtpUsername = $settings->email_address;
|
||||
}
|
||||
if (empty($smtpPassword)) {
|
||||
$smtpPassword = $settings->getDecryptedPassword();
|
||||
} else {
|
||||
$smtpPassword = Crypt::decryptString($smtpPassword);
|
||||
$mailer = $settings->getSmtpMailer();
|
||||
|
||||
if ($mailer === null) {
|
||||
return response()->json(['success' => false, 'message' => 'Impossibile creare il mailer SMTP. Verifica la configurazione.']);
|
||||
}
|
||||
|
||||
$encryption = $settings->smtp_encryption ?? 'tls';
|
||||
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
|
||||
|
||||
$dsn = sprintf(
|
||||
'%s://%s:%s@%s:%d?encryption=%s',
|
||||
$scheme,
|
||||
rawurlencode($smtpUsername),
|
||||
rawurlencode($smtpPassword),
|
||||
$settings->smtp_host,
|
||||
$settings->smtp_port ?? 587,
|
||||
$encryption
|
||||
);
|
||||
$smtpUser = $settings->smtp_username ?: $settings->email_address;
|
||||
$fromName = $settings->getEffectiveFromName();
|
||||
$replyToEmail = $settings->from_email ?: $smtpUser;
|
||||
|
||||
$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);
|
||||
|
||||
$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]);
|
||||
@@ -227,6 +192,8 @@ class EmailSettingsController extends Controller
|
||||
'verify_email' => 'nullable|email|max:255',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'is_active' => 'boolean',
|
||||
'auth_method' => 'nullable|in:password,oauth',
|
||||
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
|
||||
]);
|
||||
|
||||
if (!empty($data['smtp_password'])) {
|
||||
@@ -257,6 +224,8 @@ class EmailSettingsController extends Controller
|
||||
'verify_email' => 'nullable|email|max:255',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'is_active' => 'boolean',
|
||||
'auth_method' => 'nullable|in:password,oauth',
|
||||
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
|
||||
]);
|
||||
|
||||
if (!empty($data['smtp_password'])) {
|
||||
@@ -326,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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class CalendarioConnessioneController extends Controller
|
||||
$connessione->is_active = $request->boolean('is_active', true);
|
||||
|
||||
if (!empty($data['config'])) {
|
||||
$connessione->encryptAndSetConfig($data['config']);
|
||||
$connessione->encryptAndSetConfig($data['config'], $connessione->config ?? []);
|
||||
}
|
||||
|
||||
$connessione->save();
|
||||
|
||||
@@ -56,7 +56,7 @@ class DocumentoController extends Controller
|
||||
} elseif ($folderId) {
|
||||
$query->where('cartella_id', $folderId);
|
||||
} else {
|
||||
$query->whereNull('repository_id');
|
||||
$query->whereNull('repository_id')->whereNull('cartella_id');
|
||||
}
|
||||
|
||||
$documenti = $query->paginate(20);
|
||||
@@ -201,33 +201,43 @@ class DocumentoController extends Controller
|
||||
$tipologieValidi = TipologiaDocumento::opzioni();
|
||||
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
|
||||
|
||||
$data = $request->validate([
|
||||
$rules = [
|
||||
'nome_file' => 'required|string|max:255',
|
||||
'tipologia' => 'required|' . $tipologieRule,
|
||||
'visibilita' => 'nullable|string',
|
||||
'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing',
|
||||
'visibilita_target_id' => 'nullable|integer',
|
||||
'visibilita_target_type' => 'nullable|string',
|
||||
'note' => 'nullable|string',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
]);
|
||||
];
|
||||
|
||||
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
|
||||
if ($documento->isLink()) {
|
||||
$rules['url'] = 'nullable|url|max:2048';
|
||||
}
|
||||
|
||||
if ($request->contesto_tipo) {
|
||||
$data = $request->validate($rules);
|
||||
|
||||
$contestoTipo = $data['contesto_tipo'] ?? null;
|
||||
$data['visibilita'] = $contestoTipo ?: 'pubblico';
|
||||
|
||||
if ($contestoTipo) {
|
||||
$typeMap = [
|
||||
'individuo' => Individuo::class,
|
||||
'gruppo' => Gruppo::class,
|
||||
'evento' => Evento::class,
|
||||
'mailing' => \App\Models\MailingList::class,
|
||||
];
|
||||
$data['visibilita_target_type'] = $typeMap[$request->contesto_tipo] ?? null;
|
||||
$data['visibilita_target_type'] = $typeMap[$contestoTipo] ?? null;
|
||||
} else {
|
||||
$data['visibilita'] = 'pubblico';
|
||||
$data['visibilita_target_id'] = null;
|
||||
$data['visibilita_target_type'] = null;
|
||||
}
|
||||
|
||||
if ($documento->isLink()) {
|
||||
$data['url'] = $data['url'] ?? $documento->url;
|
||||
}
|
||||
|
||||
$documento->update($data);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
@@ -236,22 +246,69 @@ class DocumentoController extends Controller
|
||||
$documento->tags()->detach();
|
||||
}
|
||||
|
||||
return redirect('/documenti')->with('success', 'Documento aggiornato.');
|
||||
$redirect = $request->input('folder_id') ? '/documenti?folder_id=' . $request->input('folder_id') : '/documenti';
|
||||
return redirect($redirect)->with('success', 'Documento aggiornato.');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('documenti');
|
||||
$data = $request->validate([
|
||||
$tipologieValidi = TipologiaDocumento::opzioni();
|
||||
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
|
||||
|
||||
$isLink = $request->input('tipo_caricamento') === 'link';
|
||||
|
||||
$rules = [
|
||||
'nome_file' => 'required|string|max:255',
|
||||
'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro',
|
||||
'file' => 'required|file|max:10240',
|
||||
'tipologia' => 'required|' . $tipologieRule,
|
||||
'visibilita' => 'required|in:pubblico,individuo,gruppo',
|
||||
'visibilita_target_id' => 'nullable|integer',
|
||||
'visibilita_target_type' => 'nullable|string',
|
||||
'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id',
|
||||
'repository_id' => 'nullable|integer|exists:storage_repositories,id',
|
||||
]);
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'tipo_caricamento' => 'required|in:file,link',
|
||||
'url' => 'required_if:tipo_caricamento,link|nullable|url|max:2048',
|
||||
];
|
||||
|
||||
if (!$isLink) {
|
||||
$rules['file'] = 'required|file|max:10240';
|
||||
}
|
||||
|
||||
$data = $request->validate($rules);
|
||||
|
||||
if ($isLink) {
|
||||
$linkVisibilitaTargetType = null;
|
||||
if ($data['visibilita'] === 'individuo' && $data['visibilita_target_id']) {
|
||||
$linkVisibilitaTargetType = Individuo::class;
|
||||
} elseif ($data['visibilita'] === 'gruppo' && $data['visibilita_target_id']) {
|
||||
$linkVisibilitaTargetType = Gruppo::class;
|
||||
}
|
||||
|
||||
$doc = Documento::create([
|
||||
'nome_file' => $data['nome_file'],
|
||||
'url' => $data['url'],
|
||||
'tipo' => 'link',
|
||||
'tipologia' => $data['tipologia'],
|
||||
'visibilita' => $data['visibilita'],
|
||||
'visibilita_target_id' => $data['visibilita_target_id'] ?? null,
|
||||
'visibilita_target_type' => $linkVisibilitaTargetType,
|
||||
'user_id' => auth()->id(),
|
||||
'cartella_id' => $data['cartella_id'] ?? null,
|
||||
'repository_id' => null,
|
||||
]);
|
||||
|
||||
if ($doc && $request->has('tags')) {
|
||||
$doc->tags()->sync($request->tags);
|
||||
}
|
||||
|
||||
$redirect = $request->_redirect ?? url()->previous();
|
||||
if ($request->filled('folder_id')) {
|
||||
$redirect = '/documenti?folder_id=' . $request->input('folder_id');
|
||||
}
|
||||
return redirect($redirect)->with('success', 'Documento (link) creato.');
|
||||
}
|
||||
|
||||
$repositoryId = $data['repository_id'] ?? null;
|
||||
|
||||
@@ -284,7 +341,7 @@ class DocumentoController extends Controller
|
||||
$visibilitaTargetType = Gruppo::class;
|
||||
}
|
||||
|
||||
Documento::create([
|
||||
$doc = Documento::create([
|
||||
'nome_file' => $data['nome_file'],
|
||||
'file_path' => $filePath,
|
||||
'storage_disk' => $disk ?? null,
|
||||
@@ -300,7 +357,13 @@ class DocumentoController extends Controller
|
||||
'repository_id' => $repositoryId,
|
||||
]);
|
||||
|
||||
if ($doc && $request->has('tags')) {
|
||||
$doc->tags()->sync($request->tags);
|
||||
}
|
||||
$redirect = $request->_redirect ?? url()->previous();
|
||||
if ($request->filled('folder_id')) {
|
||||
$redirect = '/documenti?folder_id=' . $request->input('folder_id');
|
||||
}
|
||||
return redirect($redirect)->with('success', 'Documento caricato.');
|
||||
}
|
||||
|
||||
@@ -309,6 +372,10 @@ class DocumentoController extends Controller
|
||||
$this->authorizeRead('documenti');
|
||||
$documento = Documento::findOrFail($documento);
|
||||
|
||||
if ($documento->isLink()) {
|
||||
return redirect()->away($documento->url);
|
||||
}
|
||||
|
||||
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
|
||||
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
|
||||
|
||||
@@ -354,6 +421,12 @@ class DocumentoController extends Controller
|
||||
$this->authorizeRead('documenti');
|
||||
$documento = Documento::findOrFail($documento);
|
||||
|
||||
if ($documento->isLink()) {
|
||||
return response()->view('documenti.preview-fallback', [
|
||||
'documento' => $documento,
|
||||
])->header('X-Preview-Fallback', 'true');
|
||||
}
|
||||
|
||||
$previewableMimeTypes = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
||||
'application/pdf',
|
||||
@@ -404,7 +477,8 @@ class DocumentoController extends Controller
|
||||
$this->authorizeDelete('documenti');
|
||||
$documento = Documento::findOrFail($documento);
|
||||
|
||||
if ($documento->repository_id) {
|
||||
if (!$documento->isLink()) {
|
||||
if ($documento->repository_id) {
|
||||
$repo = StorageRepository::find($documento->repository_id);
|
||||
if ($repo) {
|
||||
$filesystem = $this->repoService->buildFilesystem($repo);
|
||||
@@ -412,10 +486,11 @@ class DocumentoController extends Controller
|
||||
$filesystem->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$disk = $this->resolveStorageDisk($documento);
|
||||
if (Storage::disk($disk)->exists($documento->file_path)) {
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
} else {
|
||||
$disk = $this->resolveStorageDisk($documento);
|
||||
if (Storage::disk($disk)->exists($documento->file_path)) {
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,9 +569,10 @@ class DocumentoController extends Controller
|
||||
{
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return response()->json(['linked_count' => 0, 'links' => []]);
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
}
|
||||
|
||||
$documenti = Documento::whereIn('id', $ids)->get();
|
||||
@@ -572,6 +648,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeRead('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
@@ -590,8 +667,14 @@ class DocumentoController extends Controller
|
||||
return back()->with('error', 'Impossibile creare l\'archivio ZIP.');
|
||||
}
|
||||
|
||||
$skippedLinks = 0;
|
||||
$addedCount = 0;
|
||||
foreach ($documenti as $documento) {
|
||||
if ($documento->isLink()) {
|
||||
$skippedLinks++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
|
||||
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
|
||||
|
||||
@@ -624,11 +707,23 @@ class DocumentoController extends Controller
|
||||
$zip->close();
|
||||
|
||||
if ($addedCount === 0) {
|
||||
unlink($zipPath);
|
||||
return back()->with('error', 'Nessun file disponibile per il download.');
|
||||
if (file_exists($zipPath)) {
|
||||
@unlink($zipPath);
|
||||
}
|
||||
$message = 'Nessun file disponibile per il download.';
|
||||
if ($skippedLinks > 0) {
|
||||
$message .= " ({$skippedLinks} link esterni saltati)";
|
||||
}
|
||||
return back()->with('error', $message);
|
||||
}
|
||||
|
||||
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true);
|
||||
$message = $addedCount . ' file scaricati.';
|
||||
if ($skippedLinks > 0) {
|
||||
$message .= " {$skippedLinks} link esterni saltati.";
|
||||
}
|
||||
|
||||
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true)
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
public function massDestroy(Request $request)
|
||||
@@ -636,6 +731,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeDelete('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
@@ -644,18 +740,20 @@ class DocumentoController extends Controller
|
||||
$documenti = Documento::whereIn('id', $ids)->get();
|
||||
|
||||
foreach ($documenti as $documento) {
|
||||
if ($documento->repository_id) {
|
||||
$repo = StorageRepository::find($documento->repository_id);
|
||||
if ($repo) {
|
||||
$filesystem = $this->repoService->buildFilesystem($repo);
|
||||
if ($filesystem && $filesystem->fileExists($documento->file_path)) {
|
||||
$filesystem->delete($documento->file_path);
|
||||
if (!$documento->isLink()) {
|
||||
if ($documento->repository_id) {
|
||||
$repo = StorageRepository::find($documento->repository_id);
|
||||
if ($repo) {
|
||||
$filesystem = $this->repoService->buildFilesystem($repo);
|
||||
if ($filesystem && $filesystem->fileExists($documento->file_path)) {
|
||||
$filesystem->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$disk = $this->resolveStorageDisk($documento);
|
||||
if (Storage::disk($disk)->exists($documento->file_path)) {
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$disk = $this->resolveStorageDisk($documento);
|
||||
if (Storage::disk($disk)->exists($documento->file_path)) {
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
$documento->delete();
|
||||
@@ -731,6 +829,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeWrite('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
@@ -762,6 +861,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeWrite('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
|
||||
@@ -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);
|
||||
@@ -157,12 +162,18 @@ class EmailController extends Controller
|
||||
|
||||
$imapSuccess = false;
|
||||
$imapError = null;
|
||||
$attachments = $this->processAttachments($request);
|
||||
$attachmentResult = $this->processAttachments($request);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$firmaId = $request->firma_id;
|
||||
|
||||
$body = $this->appendLinksToBody($validated['body'], $linkDocs);
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
try {
|
||||
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
|
||||
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachments);
|
||||
$bodyWithSig = $this->appendSignature($body, $firmaId, $settings);
|
||||
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachmentPaths);
|
||||
$imapSuccess = true;
|
||||
} catch (\Exception $e) {
|
||||
$imapError = $e->getMessage();
|
||||
@@ -171,7 +182,7 @@ class EmailController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
|
||||
$bodyWithSig = $this->appendSignature($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()]);
|
||||
@@ -188,13 +199,20 @@ class EmailController extends Controller
|
||||
private function sendWithSender(Request $request, array $validated, array $recipients)
|
||||
{
|
||||
$sender = SenderAccount::findOrFail($request->mittente_id);
|
||||
$attachmentPaths = $this->resolveAttachmentPaths($request);
|
||||
$attachmentResult = $this->resolveAttachmentPaths($request);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$settings = EmailSetting::getActive();
|
||||
$body = $this->appendLinksToBody($validated['body'], $linkDocs);
|
||||
$body = $this->appendSignature($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 +255,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 +514,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 = [])
|
||||
@@ -526,26 +552,23 @@ class EmailController extends Controller
|
||||
try {
|
||||
$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)
|
||||
->to($to)
|
||||
->subject($subject)
|
||||
->text($body);
|
||||
|
||||
|
||||
$replyTo = $settings->getEffectiveReplyTo();
|
||||
if ($replyTo) {
|
||||
$email->replyTo($replyTo);
|
||||
}
|
||||
|
||||
if ($cc) {
|
||||
$email->cc($cc);
|
||||
}
|
||||
@@ -567,6 +590,7 @@ class EmailController extends Controller
|
||||
private function processAttachments(Request $request): array
|
||||
{
|
||||
$paths = [];
|
||||
$links = [];
|
||||
|
||||
if ($request->hasFile('allegati')) {
|
||||
foreach ($request->file('allegati') as $file) {
|
||||
@@ -581,18 +605,21 @@ class EmailController extends Controller
|
||||
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
|
||||
$docs = Documento::whereIn('id', $ids)->get();
|
||||
foreach ($docs as $doc) {
|
||||
if ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
if ($doc->isLink()) {
|
||||
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
|
||||
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
$paths[] = Storage::path($doc->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
private function resolveAttachmentPaths(Request $request): array
|
||||
{
|
||||
$paths = [];
|
||||
$links = [];
|
||||
|
||||
if ($request->hasFile('allegati')) {
|
||||
foreach ($request->file('allegati') as $file) {
|
||||
@@ -607,26 +634,42 @@ class EmailController extends Controller
|
||||
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
|
||||
$docs = Documento::whereIn('id', $ids)->get();
|
||||
foreach ($docs as $doc) {
|
||||
if ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
|
||||
if ($doc->isLink()) {
|
||||
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
|
||||
} elseif ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
|
||||
$paths[] = \Illuminate\Support\Facades\Storage::path($doc->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
private function appendLinksToBody(string $body, array $linkDocs): string
|
||||
{
|
||||
if (empty($linkDocs)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$linksText = "\n\n---\nDocumenti collegati:\n";
|
||||
foreach ($linkDocs as $link) {
|
||||
$linksText .= "- {$link['nome']}: {$link['url']}\n";
|
||||
}
|
||||
|
||||
return $body . $linksText;
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Evento;
|
||||
@@ -630,4 +632,39 @@ class EventoController extends Controller
|
||||
|
||||
return redirect('/eventi')->with('success', $message);
|
||||
}
|
||||
|
||||
public function massTag(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('eventi');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun evento selezionato.');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'tags' => 'required|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'mode' => 'required|in:assign,remove',
|
||||
]);
|
||||
|
||||
$tagIds = $data['tags'];
|
||||
$mode = $data['mode'];
|
||||
$count = 0;
|
||||
|
||||
Evento::whereIn('id', $ids)->chunk(100, function ($eventi) use ($tagIds, $mode, &$count) {
|
||||
foreach ($eventi as $evento) {
|
||||
if ($mode === 'assign') {
|
||||
$evento->tags()->syncWithoutDetaching($tagIds);
|
||||
} else {
|
||||
$evento->tags()->detach($tagIds);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||
return back()->with('success', "Tag $actionLabel per $count eventi.");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Evento;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
use App\Models\GoogleOAuthConnection;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GoogleOAuthController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GoogleOAuthService $googleOAuthService,
|
||||
) {}
|
||||
|
||||
public function redirect(string $service): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$serviceEnum = GoogleService::from($service);
|
||||
} catch (\ValueError $e) {
|
||||
return redirect()->back()->withErrors(['error' => 'Servizio Google non valido: ' . $service]);
|
||||
}
|
||||
|
||||
$authUrl = $this->googleOAuthService->getAuthUrl($service);
|
||||
|
||||
return redirect()->away($authUrl);
|
||||
}
|
||||
|
||||
public function callback(Request $request): RedirectResponse
|
||||
{
|
||||
$error = $request->input('error');
|
||||
if ($error) {
|
||||
$message = match ($error) {
|
||||
'access_denied' => 'Accesso negato. Autorizzazione non concessa.',
|
||||
default => 'Errore Google: ' . $error,
|
||||
};
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => $message]);
|
||||
}
|
||||
|
||||
$code = $request->input('code');
|
||||
if (!$code) {
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => 'Nessun codice di autorizzazione ricevuto']);
|
||||
}
|
||||
|
||||
$state = $request->input('state', 'email');
|
||||
try {
|
||||
$service = GoogleService::from($state);
|
||||
} catch (\ValueError $e) {
|
||||
$service = GoogleService::Email;
|
||||
}
|
||||
|
||||
try {
|
||||
$connection = $this->googleOAuthService->handleCallback($code, $service);
|
||||
|
||||
$message = match ($service) {
|
||||
GoogleService::Email => 'Account email Gmail connesso con successo!',
|
||||
GoogleService::Drive => 'Account Google Drive connesso con successo!',
|
||||
GoogleService::Calendar => 'Account Google Calendar connesso con successo!',
|
||||
};
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->with('success', $message);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Google OAuth callback error', [
|
||||
'error' => $e->getMessage(),
|
||||
'service' => $service->value,
|
||||
]);
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => 'Errore durante la connessione: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function revoke(GoogleOAuthConnection $connection): RedirectResponse
|
||||
{
|
||||
if ($connection->user_id !== auth()->id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
try {
|
||||
$service = GoogleService::from($connection->service);
|
||||
|
||||
$message = match ($service) {
|
||||
GoogleService::Email => 'Account email Gmail disconnesso.',
|
||||
GoogleService::Drive => 'Account Google Drive disconnesso.',
|
||||
GoogleService::Calendar => 'Account Google Calendar disconnesso.',
|
||||
};
|
||||
|
||||
$this->googleOAuthService->revoke($connection);
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->with('success', $message);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Google OAuth revoke error', [
|
||||
'error' => $e->getMessage(),
|
||||
'connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => 'Errore durante la disconnessione: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function status(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$status = $this->googleOAuthService->getConnectionStatus();
|
||||
|
||||
return response()->json($status);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GruppoController extends Controller
|
||||
@@ -36,6 +37,7 @@ class GruppoController extends Controller
|
||||
|
||||
$allColumnDefs = [
|
||||
['key' => 'nome', 'label' => 'Nome'],
|
||||
['key' => 'anno_fondazione', 'label' => 'Anno Fondazione'],
|
||||
['key' => 'descrizione', 'label' => 'Descrizione'],
|
||||
['key' => 'diocesi', 'label' => 'Diocesi'],
|
||||
['key' => 'livello', 'label' => 'Livello'],
|
||||
@@ -65,7 +67,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');
|
||||
@@ -141,7 +143,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();
|
||||
@@ -154,9 +156,11 @@ class GruppoController extends Controller
|
||||
$this->authorizeWrite('gruppi');
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'anno_fondazione' => 'nullable|integer|min:1800|max:' . (date('Y') + 1),
|
||||
'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',
|
||||
@@ -164,6 +168,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',
|
||||
]);
|
||||
@@ -174,6 +183,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);
|
||||
}
|
||||
@@ -195,7 +219,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);
|
||||
@@ -206,13 +230,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)
|
||||
@@ -221,9 +246,11 @@ class GruppoController extends Controller
|
||||
$gruppo = Gruppo::findOrFail($id);
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'anno_fondazione' => 'nullable|integer|min:1800|max:' . (date('Y') + 1),
|
||||
'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',
|
||||
@@ -231,6 +258,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',
|
||||
]);
|
||||
@@ -243,6 +275,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 {
|
||||
@@ -385,9 +435,12 @@ class GruppoController extends Controller
|
||||
'file' => 'required|file|mimes:csv,txt',
|
||||
]);
|
||||
|
||||
set_time_limit(0);
|
||||
session_write_close();
|
||||
|
||||
$file = $request->file('file');
|
||||
$handle = fopen($file->path(), 'r');
|
||||
$header = fgetcsv($handle, 1000, ',');
|
||||
$header = fgetcsv($handle, 0, ',');
|
||||
|
||||
if ($header === false) {
|
||||
fclose($handle);
|
||||
@@ -400,7 +453,9 @@ class GruppoController extends Controller
|
||||
$errors = [];
|
||||
$rowNumber = 1;
|
||||
|
||||
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
|
||||
DB::beginTransaction();
|
||||
|
||||
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
||||
$rowNumber++;
|
||||
|
||||
if (count($row) < count($header)) {
|
||||
@@ -426,6 +481,7 @@ class GruppoController extends Controller
|
||||
try {
|
||||
Gruppo::create([
|
||||
'nome' => $nome,
|
||||
'anno_fondazione' => !empty($data['anno_fondazione']) ? (int) $data['anno_fondazione'] : null,
|
||||
'descrizione' => $data['descrizione'] ?? null,
|
||||
'parent_id' => !empty($data['parent_id']) ? (int) $data['parent_id'] : null,
|
||||
'diocesi_id' => !empty($data['diocesi_id']) ? (int) $data['diocesi_id'] : null,
|
||||
@@ -444,6 +500,8 @@ class GruppoController extends Controller
|
||||
|
||||
fclose($handle);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$message = 'Importati ' . $imported . ' gruppi.';
|
||||
if ($skipped > 0) {
|
||||
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
|
||||
@@ -458,8 +516,8 @@ class GruppoController extends Controller
|
||||
public function downloadTemplate()
|
||||
{
|
||||
$this->authorizeRead('gruppi');
|
||||
$headers = ['nome', 'descrizione', 'parent_id', 'diocesi_id', 'indirizzo_incontro', 'cap_incontro', 'città_incontro', 'sigla_provincia_incontro'];
|
||||
$example = ['Gruppo Giovani', 'Giovani dai 18 ai 30 anni', '', '', 'Via Roma 10', '00100', 'Roma', 'RM'];
|
||||
$headers = ['nome', 'anno_fondazione', 'descrizione', 'parent_id', 'diocesi_id', 'indirizzo_incontro', 'cap_incontro', 'città_incontro', 'sigla_provincia_incontro'];
|
||||
$example = ['Gruppo Giovani', '2020', 'Giovani dai 18 ai 30 anni', '', '', 'Via Roma 10', '00100', 'Roma', 'RM'];
|
||||
|
||||
$csv = implode(',', $headers) . "\n" . implode(',', $example);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Ruolo;
|
||||
use App\Models\SenderAccount;
|
||||
use App\Models\StorageRepository;
|
||||
use App\Models\Tag;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use App\Models\TipologiaDocumento;
|
||||
use App\Models\TipologiaEvento;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -34,8 +35,9 @@ class ImpostazioniController extends Controller
|
||||
$repositories = StorageRepository::orderBy('ordine')->get();
|
||||
$googleDriveNewToken = session('google_drive_new_token');
|
||||
$calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get();
|
||||
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
|
||||
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni', 'googleStatus'));
|
||||
}
|
||||
|
||||
public function saveAppSettings(Request $request)
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Individuo;
|
||||
use App\Models\Contatto;
|
||||
use App\Models\Documento;
|
||||
use App\Models\Gruppo;
|
||||
use App\Models\Comune;
|
||||
use App\Models\Diocesi;
|
||||
use App\Models\Tag;
|
||||
use App\Models\VistaReport;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use App\Models\Gruppo;
|
||||
use App\Models\Comune;
|
||||
use App\Models\Diocesi;
|
||||
use App\Models\Tag;
|
||||
use App\Models\VistaReport;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class IndividuoController extends Controller
|
||||
{
|
||||
@@ -148,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'));
|
||||
}
|
||||
@@ -497,16 +501,27 @@ public function emailList(Request $request)
|
||||
'file' => 'required|file|mimes:csv,txt',
|
||||
]);
|
||||
|
||||
set_time_limit(0);
|
||||
session_write_close();
|
||||
|
||||
$file = $request->file('file');
|
||||
$handle = fopen($file->path(), 'r');
|
||||
$header = fgetcsv($handle, 1000, ',');
|
||||
$header = fgetcsv($handle, 0, ',');
|
||||
|
||||
if ($header === false) {
|
||||
fclose($handle);
|
||||
return back()->with('error', 'Il file CSV non è valido.');
|
||||
}
|
||||
|
||||
$header = array_map('trim', $header);
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$errors = [];
|
||||
$rowNumber = 1;
|
||||
|
||||
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
|
||||
DB::beginTransaction();
|
||||
|
||||
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
||||
$rowNumber++;
|
||||
|
||||
if (count($row) < count($header)) {
|
||||
@@ -565,12 +580,15 @@ public function emailList(Request $request)
|
||||
|
||||
$imported++;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Errore import individuo riga ' . $rowNumber . ': ' . $e->getMessage());
|
||||
$errors[] = 'Errore riga ' . $rowNumber . ': ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$message = 'Importati ' . $imported . ' individui.';
|
||||
if ($skipped > 0) {
|
||||
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
|
||||
|
||||
@@ -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 = [];
|
||||
@@ -110,7 +120,23 @@ class MailingController extends Controller
|
||||
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
|
||||
}
|
||||
|
||||
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentResult = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$firmaId = $data['firma_id'] ?? null;
|
||||
if (!$firmaId && !empty($data['lista_id'])) {
|
||||
$lista = MailingList::with('firma')->find($data['lista_id']);
|
||||
$firmaId = $lista?->firma?->id;
|
||||
}
|
||||
|
||||
$corpoConFirma = $this->appendMailingLinksToBody($data['corpo'], $linkDocs);
|
||||
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,
|
||||
@@ -121,6 +147,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 +157,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 +208,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 +222,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 = [];
|
||||
@@ -252,9 +286,24 @@ class MailingController extends Controller
|
||||
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
|
||||
}
|
||||
|
||||
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentResult = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$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 = $this->appendMailingLinksToBody($data['corpo'], $linkDocs);
|
||||
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 +314,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 +324,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 +397,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 +408,11 @@ class MailingController extends Controller
|
||||
->subject($subject)
|
||||
->text($body);
|
||||
|
||||
$replyTo = $settings->getEffectiveReplyTo();
|
||||
if ($replyTo) {
|
||||
$email->replyTo($replyTo);
|
||||
}
|
||||
|
||||
foreach ($attachmentPaths as $path) {
|
||||
$email->attachFromPath($path);
|
||||
}
|
||||
@@ -376,17 +423,34 @@ class MailingController extends Controller
|
||||
private function resolveMailingAttachmentPaths(array $documentIds): array
|
||||
{
|
||||
$paths = [];
|
||||
$links = [];
|
||||
if (empty($documentIds)) {
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
$docs = Documento::whereIn('id', $documentIds)->get();
|
||||
foreach ($docs as $doc) {
|
||||
if ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
if ($doc->isLink()) {
|
||||
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
|
||||
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
$paths[] = Storage::path($doc->file_path);
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
private function appendMailingLinksToBody(string $body, array $linkDocs): string
|
||||
{
|
||||
if (empty($linkDocs)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$linksText = "\n\n---\nDocumenti collegati:\n";
|
||||
foreach ($linkDocs as $link) {
|
||||
$linksText .= "- {$link['nome']}: {$link['url']}\n";
|
||||
}
|
||||
|
||||
return $body . $linksText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')) {
|
||||
@@ -186,6 +201,41 @@ class MailingListController extends Controller
|
||||
return redirect()->route('mailing-liste.index')->with('success', 'Lista eliminata.');
|
||||
}
|
||||
|
||||
public function massTag(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('mailing');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessuna mailing list selezionata.');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'tags' => 'required|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'mode' => 'required|in:assign,remove',
|
||||
]);
|
||||
|
||||
$tagIds = $data['tags'];
|
||||
$mode = $data['mode'];
|
||||
$count = 0;
|
||||
|
||||
MailingList::whereIn('id', $ids)->chunk(100, function ($lists) use ($tagIds, $mode, &$count) {
|
||||
foreach ($lists as $list) {
|
||||
if ($mode === 'assign') {
|
||||
$list->tags()->syncWithoutDetaching($tagIds);
|
||||
} else {
|
||||
$list->tags()->detach($tagIds);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||
return back()->with('success', "Tag $actionLabel per $count mailing list.");
|
||||
}
|
||||
|
||||
public function massElimina(Request $request)
|
||||
{
|
||||
$this->authorizeDelete('mailing');
|
||||
|
||||
@@ -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 ?? '-',
|
||||
|
||||
@@ -53,11 +53,13 @@ class CalendarioConnessione extends Model
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function encryptAndSetConfig(array $config): void
|
||||
public function encryptAndSetConfig(array $config, array $existingConfig = []): void
|
||||
{
|
||||
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
|
||||
if (isset($config[$field]) && $config[$field] !== '' && !str_starts_with($config[$field], 'eyJpdiI')) {
|
||||
if (!empty($config[$field]) && !str_starts_with($config[$field], 'eyJpdiI')) {
|
||||
$config[$field] = Crypt::encryptString($config[$field]);
|
||||
} elseif (empty($config[$field]) && !empty($existingConfig[$field])) {
|
||||
$config[$field] = $existingConfig[$field];
|
||||
}
|
||||
}
|
||||
$this->config = $config;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTagsLight;
|
||||
@@ -16,9 +18,14 @@ class Documento extends Model
|
||||
'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk',
|
||||
'nome_file', 'file_path', 'tipo', 'tipologia',
|
||||
'visibilita', 'visibilita_target_id', 'visibilita_target_type',
|
||||
'mime_type', 'dimensione', 'note'
|
||||
'mime_type', 'dimensione', 'note', 'url'
|
||||
];
|
||||
|
||||
public function isLink(): bool
|
||||
{
|
||||
return $this->tipo === 'link';
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
|
||||
+156
-8
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
@@ -13,14 +15,17 @@ 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',
|
||||
'last_sync_at', 'is_active', 'signature', 'signature_enabled'
|
||||
'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',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'signature_enabled' => 'boolean',
|
||||
'last_sync_at' => 'datetime',
|
||||
'google_oauth_connection_id' => 'integer',
|
||||
];
|
||||
|
||||
public function folders(): HasMany
|
||||
@@ -28,8 +33,25 @@ class EmailSetting extends Model
|
||||
return $this->hasMany(EmailFolder::class)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function googleOAuthConnection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GoogleOAuthConnection::class);
|
||||
}
|
||||
|
||||
public function getImapConfig(): array
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
$token = $this->resolveOAuthToken();
|
||||
return [
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port,
|
||||
'encryption' => $this->imap_encryption,
|
||||
'username' => $this->imap_username,
|
||||
'authentication' => 'oauth',
|
||||
'password' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port,
|
||||
@@ -41,6 +63,10 @@ class EmailSetting extends Model
|
||||
|
||||
public function getDecryptedPassword(): string
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->resolveOAuthToken();
|
||||
}
|
||||
|
||||
if (empty($this->imap_password)) {
|
||||
return '';
|
||||
}
|
||||
@@ -53,6 +79,10 @@ class EmailSetting extends Model
|
||||
|
||||
public function getDecryptedSmtpPassword(): string
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->resolveOAuthToken();
|
||||
}
|
||||
|
||||
if (empty($this->smtp_password)) {
|
||||
return $this->getDecryptedPassword();
|
||||
}
|
||||
@@ -63,6 +93,22 @@ class EmailSetting extends Model
|
||||
}
|
||||
}
|
||||
|
||||
protected function resolveOAuthToken(): string
|
||||
{
|
||||
if (!$this->relationLoaded('googleOAuthConnection')) {
|
||||
$this->load('googleOAuthConnection');
|
||||
}
|
||||
$connection = $this->googleOAuthConnection;
|
||||
if (!$connection) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return app(GoogleOAuthService::class)->getAccessToken($connection);
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function getSmtpConfig(): array
|
||||
{
|
||||
return [
|
||||
@@ -87,33 +133,101 @@ class EmailSetting extends Model
|
||||
default => 'none',
|
||||
};
|
||||
|
||||
return new \DirectoryTree\ImapEngine\Mailbox([
|
||||
$config = [
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port ?? 993,
|
||||
'encryption' => $encryption,
|
||||
'validate_cert' => false,
|
||||
'username' => $this->imap_username,
|
||||
'password' => $this->getDecryptedPassword(),
|
||||
]);
|
||||
];
|
||||
|
||||
if ($this->auth_method === 'oauth') {
|
||||
$config['authentication'] = 'oauth';
|
||||
}
|
||||
|
||||
return new \DirectoryTree\ImapEngine\Mailbox($config);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->buildOAuthSmtpMailer();
|
||||
}
|
||||
|
||||
$dsn = $this->buildSmtpDsn();
|
||||
if (!$dsn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
|
||||
return new \Symfony\Component\Mailer\Mailer($transport);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildOAuthSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
|
||||
{
|
||||
$token = $this->resolveOAuthToken();
|
||||
if (empty($token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$username = $this->smtp_username ?? $this->email_address;
|
||||
$host = $this->smtp_host ?? 'smtp.gmail.com';
|
||||
$port = $this->smtp_port ?? 587;
|
||||
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
|
||||
$transport->setUsername($username);
|
||||
$transport->setPassword($token);
|
||||
$transport->setAuthenticators([
|
||||
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
|
||||
]);
|
||||
|
||||
return new \Symfony\Component\Mailer\Mailer($transport);
|
||||
}
|
||||
|
||||
public function buildSmtpDsn(): ?string
|
||||
{
|
||||
$password = $this->getDecryptedSmtpPassword();
|
||||
if (empty($password)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$username = $this->smtp_username ?? $this->email_address;
|
||||
$encryption = $this->smtp_encryption ?? 'tls';
|
||||
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
|
||||
|
||||
return sprintf(
|
||||
'%s://%s:%s@%s:%d?encryption=%s',
|
||||
$scheme,
|
||||
rawurlencode($username),
|
||||
rawurlencode($password),
|
||||
$this->smtp_host ?? 'smtp.gmail.com',
|
||||
$this->smtp_port ?? 587,
|
||||
$encryption
|
||||
);
|
||||
}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
try {
|
||||
$mailbox = $this->getImapClient();
|
||||
|
||||
|
||||
if (!$mailbox) {
|
||||
return ['success' => false, 'message' => 'Configurazione IMAP non valida'];
|
||||
}
|
||||
|
||||
$mailbox->connect();
|
||||
$mailbox->disconnect();
|
||||
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'success' => true,
|
||||
'message' => 'Connessione IMAP riuscita! Server: ' . $this->imap_host
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
@@ -134,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTagsLight;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class GoogleOAuthConnection extends Model
|
||||
{
|
||||
protected $table = 'google_oauth_connections';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'service',
|
||||
'email',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at && $this->expires_at->isPast();
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
if (!$this->refresh_token) {
|
||||
throw new \RuntimeException('Nessun refresh token disponibile per ' . $this->email);
|
||||
}
|
||||
app(GoogleOAuthService::class)->refreshToken($this);
|
||||
}
|
||||
|
||||
public function getValidAccessToken(): string
|
||||
{
|
||||
if ($this->isExpired() && $this->refresh_token) {
|
||||
$this->refresh();
|
||||
}
|
||||
return $this->access_token;
|
||||
}
|
||||
}
|
||||
+23
-3
@@ -15,15 +15,18 @@ class Gruppo extends Model
|
||||
protected $table = 'gruppi';
|
||||
protected $fillable = [
|
||||
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
|
||||
'nome', 'descrizione', 'indirizzo_incontro', 'cap_incontro',
|
||||
'nome', 'anno_fondazione', 'descrizione', 'indirizzo_incontro', 'cap_incontro',
|
||||
'città_incontro', 'sigla_provincia_incontro', 'mappa_posizione', 'metadata'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'responsabile_ids' => 'array',
|
||||
'anno_fondazione' => 'integer',
|
||||
];
|
||||
|
||||
protected $appends = ['email_primaria', 'telefono_primario'];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
@@ -39,9 +42,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 +74,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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class SenderAccount extends Model
|
||||
@@ -24,11 +26,14 @@ class SenderAccount extends Model
|
||||
'verify_email',
|
||||
'note',
|
||||
'is_active',
|
||||
'auth_method',
|
||||
'google_oauth_connection_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'smtp_port' => 'integer',
|
||||
'google_oauth_connection_id' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
@@ -36,8 +41,33 @@ class SenderAccount extends Model
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function googleOAuthConnection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GoogleOAuthConnection::class);
|
||||
}
|
||||
|
||||
protected function resolveOAuthToken(): string
|
||||
{
|
||||
if (!$this->relationLoaded('googleOAuthConnection')) {
|
||||
$this->load('googleOAuthConnection');
|
||||
}
|
||||
$connection = $this->googleOAuthConnection;
|
||||
if (!$connection) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return app(GoogleOAuthService::class)->getAccessToken($connection);
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function getDecryptedPassword(): string
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->resolveOAuthToken();
|
||||
}
|
||||
|
||||
if (empty($this->smtp_password)) {
|
||||
return '';
|
||||
}
|
||||
@@ -121,11 +151,33 @@ class SenderAccount extends Model
|
||||
$email->attachFromPath($path);
|
||||
}
|
||||
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn());
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
if ($this->auth_method === 'oauth') {
|
||||
$mailer = $this->buildOAuthMailer();
|
||||
} else {
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn());
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
}
|
||||
|
||||
$mailer->send($email);
|
||||
}
|
||||
|
||||
protected function buildOAuthMailer(): \Symfony\Component\Mailer\Mailer
|
||||
{
|
||||
$token = $this->resolveOAuthToken();
|
||||
$username = $this->smtp_username ?? $this->email_address;
|
||||
$host = $this->smtp_host ?? 'smtp.gmail.com';
|
||||
$port = $this->smtp_port ?? 587;
|
||||
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
|
||||
$transport->setUsername($username);
|
||||
$transport->setPassword($token);
|
||||
$transport->setAuthenticators([
|
||||
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
|
||||
]);
|
||||
|
||||
return new \Symfony\Component\Mailer\Mailer($transport);
|
||||
}
|
||||
|
||||
public function sendReport(string $subject, string $body): bool
|
||||
{
|
||||
if (empty($this->verify_email)) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
+5
-4
@@ -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', [
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GoogleOAuthConnection;
|
||||
use App\Enums\GoogleService;
|
||||
use Google_Client;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GoogleOAuthService
|
||||
{
|
||||
private const TOKEN_CACHE_TTL = 300;
|
||||
private const MAX_RETRIES = 2;
|
||||
|
||||
public function buildClient(): Google_Client
|
||||
{
|
||||
$client = new Google_Client();
|
||||
$client->setClientId(config('services.google.client_id'));
|
||||
$client->setClientSecret(config('services.google.client_secret'));
|
||||
$client->setRedirectUri(route('google-oauth.callback'));
|
||||
$client->setAccessType('offline');
|
||||
$client->setPrompt('consent');
|
||||
$client->setIncludeGrantedScopes(true);
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
public function getAuthUrl(string $service): string
|
||||
{
|
||||
$serviceEnum = GoogleService::from($service);
|
||||
$client = $this->buildClient();
|
||||
$client->setScopes($serviceEnum->scopes());
|
||||
$client->setState($service);
|
||||
|
||||
return $client->createAuthUrl();
|
||||
}
|
||||
|
||||
public function handleCallback(string $authorizationCode, GoogleService $service): GoogleOAuthConnection
|
||||
{
|
||||
$client = $this->buildClient();
|
||||
$client->setScopes($service->scopes());
|
||||
$client->fetchAccessTokenWithAuthCode($authorizationCode);
|
||||
|
||||
$tokenData = $client->getAccessToken();
|
||||
|
||||
if (isset($tokenData['error'])) {
|
||||
throw new \RuntimeException('Google OAuth error: ' . ($tokenData['error_description'] ?? $tokenData['error']));
|
||||
}
|
||||
|
||||
$token = $tokenData['access_token'];
|
||||
$refreshToken = $tokenData['refresh_token'] ?? null;
|
||||
$expiresAt = now()->addSeconds($tokenData['expires_in']);
|
||||
|
||||
$email = $this->getEmailFromToken($token);
|
||||
|
||||
$connection = GoogleOAuthConnection::updateOrCreate([
|
||||
'user_id' => auth()->id(),
|
||||
'service' => $service->value,
|
||||
'email' => $email,
|
||||
], [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_at' => $expiresAt,
|
||||
]);
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
public function getAccessToken(GoogleOAuthConnection $connection): string
|
||||
{
|
||||
$cacheKey = "google_oauth_token_{$connection->id}";
|
||||
|
||||
return Cache::remember($cacheKey, self::TOKEN_CACHE_TTL, function () use ($connection) {
|
||||
if ($connection->isExpired()) {
|
||||
$this->refreshToken($connection);
|
||||
}
|
||||
return $connection->access_token;
|
||||
});
|
||||
}
|
||||
|
||||
public function refreshToken(GoogleOAuthConnection $connection): void
|
||||
{
|
||||
if (!$connection->refresh_token) {
|
||||
throw new \RuntimeException('Impossibile rinnovare: nessun refresh token');
|
||||
}
|
||||
|
||||
$client = $this->buildClient();
|
||||
$client->fetchAccessTokenWithRefreshToken($connection->refresh_token);
|
||||
|
||||
$tokenData = $client->getAccessToken();
|
||||
|
||||
if (isset($tokenData['error'])) {
|
||||
$connection->delete();
|
||||
Cache::forget("google_oauth_token_{$connection->id}");
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Refresh token scaduto o revocato per ' . $connection->email
|
||||
);
|
||||
}
|
||||
|
||||
$connection->access_token = $tokenData['access_token'];
|
||||
$connection->expires_at = now()->addSeconds($tokenData['expires_in']);
|
||||
|
||||
if (isset($tokenData['refresh_token'])) {
|
||||
$connection->refresh_token = $tokenData['refresh_token'];
|
||||
}
|
||||
|
||||
$connection->save();
|
||||
Cache::forget("google_oauth_token_{$connection->id}");
|
||||
}
|
||||
|
||||
public function revoke(GoogleOAuthConnection $connection): void
|
||||
{
|
||||
$client = $this->buildClient();
|
||||
$client->revokeToken($connection->access_token);
|
||||
|
||||
$connection->delete();
|
||||
Cache::forget("google_oauth_token_{$connection->id}");
|
||||
}
|
||||
|
||||
public function createXoAuth2SmtpTransport(
|
||||
string $email,
|
||||
string $accessToken
|
||||
): \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport {
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
|
||||
'smtp.gmail.com',
|
||||
587,
|
||||
true,
|
||||
);
|
||||
|
||||
$transport->setUsername($email);
|
||||
$transport->setPassword($accessToken);
|
||||
$transport->setAuthenticators([
|
||||
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
|
||||
]);
|
||||
|
||||
return $transport;
|
||||
}
|
||||
|
||||
private function getEmailFromToken(string $accessToken): string
|
||||
{
|
||||
$client = $this->buildClient();
|
||||
$payload = $client->verifyIdToken($accessToken);
|
||||
|
||||
if ($payload && isset($payload['email'])) {
|
||||
return $payload['email'];
|
||||
}
|
||||
|
||||
$http = new \GuzzleHttp\Client();
|
||||
$response = $http->get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', [
|
||||
'headers' => ['Authorization' => "Bearer $accessToken"],
|
||||
]);
|
||||
$data = json_decode((string) $response->getBody(), true);
|
||||
|
||||
return $data['email'];
|
||||
}
|
||||
|
||||
public function getConnectionStatus(): array
|
||||
{
|
||||
$connections = GoogleOAuthConnection::where('user_id', auth()->id())->get();
|
||||
$status = [];
|
||||
|
||||
foreach (GoogleService::cases() as $service) {
|
||||
$connection = $connections->firstWhere('service', $service->value);
|
||||
$status[$service->value] = [
|
||||
'connected' => $connection !== null,
|
||||
'connection' => $connection,
|
||||
'email' => $connection?->email,
|
||||
'expired' => $connection?->isExpired() ?? false,
|
||||
];
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -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
-21
@@ -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,27 +289,14 @@ echo "Per estrarre sul server di destinazione:"
|
||||
echo " tar xzf ${ARCHIVE}"
|
||||
echo ""
|
||||
echo "Poi esegui:"
|
||||
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 " # Upgrade di un'installazione esistente:"
|
||||
echo " bash post-deploy.sh upgrade"
|
||||
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 " # Fresh install:"
|
||||
echo " bash post-deploy.sh install"
|
||||
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 " # Solo controllo (nessuna modifica):"
|
||||
echo " bash post-deploy.sh check"
|
||||
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 " # Batch mode (nessuna conferma):"
|
||||
echo " bash post-deploy.sh upgrade --yes"
|
||||
|
||||
@@ -35,4 +35,10 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'google' => [
|
||||
'client_id' => env('GOOGLE_CLIENT_ID'),
|
||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
'redirect' => env('GOOGLE_REDIRECT_URI', config('app.url') . '/auth/google/callback'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -13,10 +13,10 @@ return new class extends Migration
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||
$table->string('nome_file');
|
||||
$table->string('file_path');
|
||||
$table->string('file_path')->nullable();
|
||||
$table->string('tipo')->nullable();
|
||||
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento');
|
||||
$table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo');
|
||||
$table->string('tipologia', 50)->default('documento');
|
||||
$table->string('visibilita', 50)->default('gruppo');
|
||||
$table->unsignedBigInteger('visibilita_target_id')->nullable();
|
||||
$table->string('visibilita_target_type')->nullable();
|
||||
$table->string('mime_type')->nullable();
|
||||
|
||||
@@ -17,18 +17,18 @@ return new class extends Migration
|
||||
$table->boolean('attiva')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
$defaultTipologie = ['avatar', 'galleria', 'documento', 'statuto', 'altro'];
|
||||
foreach ($defaultTipologie as $i => $nome) {
|
||||
DB::table('tipologie_documenti')->insert([
|
||||
'nome' => $nome,
|
||||
'descrizione' => ucfirst($nome),
|
||||
'ordine' => $i,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$defaultTipologie = ['avatar', 'galleria', 'documento', 'statuto', 'altro'];
|
||||
foreach ($defaultTipologie as $i => $nome) {
|
||||
DB::table('tipologie_documenti')->insert([
|
||||
'nome' => $nome,
|
||||
'descrizione' => ucfirst($nome),
|
||||
'ordine' => $i,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<?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('documenti', function (Blueprint $table) {
|
||||
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro', 'email_attachment'])->default('documento')->change();
|
||||
});
|
||||
// Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
|
||||
// Application-level validation handles allowed values.
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento')->change();
|
||||
});
|
||||
// No-op: column is string, not enum.
|
||||
}
|
||||
};
|
||||
@@ -7,14 +7,15 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('tipologie_documenti')->insert([
|
||||
'nome' => 'email_attachment',
|
||||
'descrizione' => 'Allegato email',
|
||||
'ordine' => 100,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
DB::table('tipologie_documenti')->updateOrInsert(
|
||||
['nome' => 'email_attachment'],
|
||||
[
|
||||
'descrizione' => 'Allegato email',
|
||||
'ordine' => 100,
|
||||
'attiva' => true,
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -17,27 +17,27 @@ return new class extends Migration
|
||||
$table->boolean('attiva')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
$defaultRuoli = [
|
||||
['nome' => 'Membro', 'descrizione' => 'Membro del gruppo'],
|
||||
['nome' => 'Catechista', 'descrizione' => 'Catechista del gruppo'],
|
||||
['nome' => 'Animatore', 'descrizione' => 'Animatore del gruppo'],
|
||||
['nome' => 'Responsabile', 'descrizione' => 'Responsabile del gruppo'],
|
||||
['nome' => 'Vice Responsabile', 'descrizione' => 'Vice responsabile del gruppo'],
|
||||
['nome' => 'Segretario', 'descrizione' => 'Segretario del gruppo'],
|
||||
['nome' => 'Tesoriere', 'descrizione' => 'Tesoriere del gruppo'],
|
||||
];
|
||||
$defaultRuoli = [
|
||||
['nome' => 'Membro', 'descrizione' => 'Membro del gruppo'],
|
||||
['nome' => 'Catechista', 'descrizione' => 'Catechista del gruppo'],
|
||||
['nome' => 'Animatore', 'descrizione' => 'Animatore del gruppo'],
|
||||
['nome' => 'Responsabile', 'descrizione' => 'Responsabile del gruppo'],
|
||||
['nome' => 'Vice Responsabile', 'descrizione' => 'Vice responsabile del gruppo'],
|
||||
['nome' => 'Segretario', 'descrizione' => 'Segretario del gruppo'],
|
||||
['nome' => 'Tesoriere', 'descrizione' => 'Tesoriere del gruppo'],
|
||||
];
|
||||
|
||||
foreach ($defaultRuoli as $i => $ruolo) {
|
||||
DB::table('ruoli')->insert([
|
||||
'nome' => $ruolo['nome'],
|
||||
'descrizione' => $ruolo['descrizione'],
|
||||
'ordine' => $i,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
foreach ($defaultRuoli as $i => $ruolo) {
|
||||
DB::table('ruoli')->insert([
|
||||
'nome' => $ruolo['nome'],
|
||||
'descrizione' => $ruolo['descrizione'],
|
||||
'ordine' => $i,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
|
||||
|
||||
@@ -8,10 +8,13 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||
$table->dropForeign(['ruolo_id']);
|
||||
$table->dropColumn('ruolo_id');
|
||||
});
|
||||
if (Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
|
||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||
$table->dropIndex(['ruolo_id']);
|
||||
$table->dropForeign(['ruolo_id']);
|
||||
$table->dropColumn('ruolo_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_ids')) {
|
||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro', 'programma', 'locandina') DEFAULT 'documento'");
|
||||
// Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
|
||||
// Application-level validation handles allowed values.
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro') DEFAULT 'documento'");
|
||||
// No-op: column is string, not enum.
|
||||
}
|
||||
};
|
||||
@@ -19,18 +19,18 @@ return new class extends Migration
|
||||
$table->boolean('attiva')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
DB::table('tipologie_eventi')->insert([
|
||||
['nome' => 'catechesi', 'descrizione' => 'Catechesi', 'ordine' => 0, 'attiva' => true],
|
||||
['nome' => 'liturgia', 'descrizione' => 'Liturgia', 'ordine' => 1, 'attiva' => true],
|
||||
['nome' => 'animazione', 'descrizione' => 'Animazione', 'ordine' => 2, 'attiva' => true],
|
||||
['nome' => 'formazione', 'descrizione' => 'Formazione', 'ordine' => 3, 'attiva' => true],
|
||||
['nome' => 'incontro', 'descrizione' => 'Incontro', 'ordine' => 4, 'attiva' => true],
|
||||
['nome' => 'riunione', 'descrizione' => 'Riunione', 'ordine' => 5, 'attiva' => true],
|
||||
['nome' => 'festa', 'descrizione' => 'Festa / Sagra', 'ordine' => 6, 'attiva' => true],
|
||||
['nome' => 'altro', 'descrizione' => 'Altro', 'ordine' => 7, 'attiva' => true],
|
||||
]);
|
||||
DB::table('tipologie_eventi')->insert([
|
||||
['nome' => 'catechesi', 'descrizione' => 'Catechesi', 'ordine' => 0, 'attiva' => true],
|
||||
['nome' => 'liturgia', 'descrizione' => 'Liturgia', 'ordine' => 1, 'attiva' => true],
|
||||
['nome' => 'animazione', 'descrizione' => 'Animazione', 'ordine' => 2, 'attiva' => true],
|
||||
['nome' => 'formazione', 'descrizione' => 'Formazione', 'ordine' => 3, 'attiva' => true],
|
||||
['nome' => 'incontro', 'descrizione' => 'Incontro', 'ordine' => 4, 'attiva' => true],
|
||||
['nome' => 'riunione', 'descrizione' => 'Riunione', 'ordine' => 5, 'attiva' => true],
|
||||
['nome' => 'festa', 'descrizione' => 'Festa / Sagra', 'ordine' => 6, 'attiva' => true],
|
||||
['nome' => 'altro', 'descrizione' => 'Altro', 'ordine' => 7, 'attiva' => true],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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('google_oauth_connections')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('google_oauth_connections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('service'); // email, drive, calendar
|
||||
$table->string('email');
|
||||
$table->text('access_token');
|
||||
$table->text('refresh_token')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'service', 'email']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('google_oauth_connections');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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::hasColumn('email_settings', 'auth_method')) {
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->string('auth_method', 20)->default('password');
|
||||
$table->foreignId('google_oauth_connection_id')
|
||||
->nullable()
|
||||
->constrained('google_oauth_connections')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('sender_accounts', 'auth_method')) {
|
||||
Schema::table('sender_accounts', function (Blueprint $table) {
|
||||
$table->string('auth_method', 20)->default('password');
|
||||
$table->foreignId('google_oauth_connection_id')
|
||||
->nullable()
|
||||
->constrained('google_oauth_connections')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('email_settings', 'google_oauth_connection_id')) {
|
||||
$table->dropForeign(['google_oauth_connection_id']);
|
||||
}
|
||||
if (Schema::hasColumn('email_settings', 'auth_method')) {
|
||||
$table->dropColumn(['auth_method', 'google_oauth_connection_id']);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('sender_accounts', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('sender_accounts', 'google_oauth_connection_id')) {
|
||||
$table->dropForeign(['google_oauth_connection_id']);
|
||||
}
|
||||
if (Schema::hasColumn('sender_accounts', 'auth_method')) {
|
||||
$table->dropColumn(['auth_method', 'google_oauth_connection_id']);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
+25
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasColumn('gruppi', 'anno_fondazione')) {
|
||||
Schema::table('gruppi', function (Blueprint $table) {
|
||||
$table->year('anno_fondazione')->nullable()->after('nome');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('gruppi', 'anno_fondazione')) {
|
||||
Schema::table('gruppi', function (Blueprint $table) {
|
||||
$table->dropColumn('anno_fondazione');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->text('url')->nullable()->after('note');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->dropColumn('url');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasColumn('documenti', 'file_path') && DB::getDriverName() === 'mysql') {
|
||||
DB::statement('ALTER TABLE documenti MODIFY COLUMN file_path VARCHAR(255) NULL');
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('documenti', 'file_path') && DB::getDriverName() === 'mysql') {
|
||||
DB::statement('ALTER TABLE documenti MODIFY COLUMN file_path VARCHAR(255) NOT NULL');
|
||||
}
|
||||
}
|
||||
};
|
||||
+244
-139
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1162
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
../laravel-vite-plugin/bin/clean.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../concurrently/dist/bin/concurrently.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../concurrently/dist/bin/concurrently.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../jiti/lib/jiti-cli.mjs
|
||||
+1
@@ -0,0 +1 @@
|
||||
../nanoid/bin/nanoid.cjs
|
||||
+1
@@ -0,0 +1 @@
|
||||
../rolldown/bin/cli.mjs
|
||||
+1
@@ -0,0 +1 @@
|
||||
../tree-kill/cli.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../vite/bin/vite.js
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+66
@@ -0,0 +1,66 @@
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Instrument Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-400-normal.woff2) format('woff2'), url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-400-normal.woff) format('woff');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Instrument Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-ext-400-normal.woff2) format('woff2'), url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-ext-400-normal.woff) format('woff');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Instrument Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-stretch: 100%;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-500-normal.woff2) format('woff2'), url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-500-normal.woff) format('woff');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Instrument Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-stretch: 100%;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-ext-500-normal.woff2) format('woff2'), url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-ext-500-normal.woff) format('woff');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Instrument Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-stretch: 100%;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-600-normal.woff) format('woff');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Instrument Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-stretch: 100%;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-ext-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/instrument-sans/files/instrument-sans-latin-ext-600-normal.woff) format('woff');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+929
@@ -0,0 +1,929 @@
|
||||
{
|
||||
"name": "glastree",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.133.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
|
||||
"integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"enhanced-resolve": "5.21.6",
|
||||
"jiti": "^2.7.0",
|
||||
"lightningcss": "1.32.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
|
||||
"integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.3.1",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.3.1",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.3.1",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.3.1",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.3.1",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.3.1",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
|
||||
"integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
|
||||
"integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz",
|
||||
"integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.3.1",
|
||||
"@tailwindcss/oxide": "4.3.1",
|
||||
"tailwindcss": "4.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.21.6",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
|
||||
"integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/laravel-vite-plugin": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.1.0.tgz",
|
||||
"integrity": "sha512-Fzocl+X4eQ9jOi0RwdphYRGkUbPJ3ky1pTAST5Ot18cS2gw6d2vldK2eCrlKDVjtibCjCx5qptYDlA0373n7qg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
"tinyglobby": "^0.2.12",
|
||||
"vite-plugin-full-reload": "^1.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"clean-orphaned-assets": "bin/clean.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"fontaine": "^0.5.0",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"fontaine": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.133.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
|
||||
"integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
|
||||
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.15",
|
||||
"rolldown": "1.0.3",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.18",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-full-reload": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz",
|
||||
"integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
"picomatch": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-full-reload/node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-present Toyobayashi
|
||||
|
||||
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.
|
||||
+1
@@ -0,0 +1 @@
|
||||
See [https://github.com/toyobayashi/emnapi](https://github.com/toyobayashi/emnapi)
|
||||
+7397
File diff suppressed because it is too large
Load Diff
+420
@@ -0,0 +1,420 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import type { Context } from '@emnapi/runtime';
|
||||
import type { ReferenceOwnership } from '@emnapi/runtime';
|
||||
import type { Worker as Worker_2 } from 'worker_threads';
|
||||
|
||||
/** @public */
|
||||
export declare type BaseCreateOptions = {
|
||||
filename?: string
|
||||
nodeBinding?: NodeBinding
|
||||
reuseWorker?: ThreadManagerOptionsMain['reuseWorker']
|
||||
asyncWorkPoolSize?: number
|
||||
waitThreadStart?: MainThreadBaseOptions['waitThreadStart']
|
||||
onCreateWorker?: (info: CreateWorkerInfo) => any
|
||||
print?: (str: string) => void
|
||||
printErr?: (str: string) => void
|
||||
postMessage?: (msg: any) => any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface BaseOptions {
|
||||
wasi: WASIInstance;
|
||||
version?: 'preview1';
|
||||
wasm64?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ChildThreadOptions extends BaseOptions {
|
||||
childThread: true;
|
||||
postMessage?: (data: any) => void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CleanupThreadPayload {
|
||||
tid: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CommandInfo<T extends CommandType> {
|
||||
type: T;
|
||||
payload: CommandPayloadMap[T];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CommandPayloadMap {
|
||||
load: LoadPayload;
|
||||
loaded: LoadedPayload;
|
||||
start: StartPayload;
|
||||
'cleanup-thread': CleanupThreadPayload;
|
||||
'terminate-all-threads': TerminateAllThreadsPayload;
|
||||
'spawn-thread': SpawnThreadPayload;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type CommandType = keyof CommandPayloadMap;
|
||||
|
||||
/** @public */
|
||||
export declare function createInstanceProxy(instance: WebAssembly.Instance, memory?: WebAssembly.Memory | (() => WebAssembly.Memory)): WebAssembly.Instance;
|
||||
|
||||
/** @public */
|
||||
export declare function createNapiModule (
|
||||
options: CreateOptions
|
||||
): NapiModule
|
||||
|
||||
/** @public */
|
||||
export declare type CreateOptions = BaseCreateOptions & ({
|
||||
context: Context
|
||||
childThread?: boolean
|
||||
} | {
|
||||
context?: Context
|
||||
childThread: true
|
||||
})
|
||||
|
||||
/** @public */
|
||||
export declare interface CreateWorkerInfo {
|
||||
type: 'thread' | 'async-work'
|
||||
name: string
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface InitOptions {
|
||||
instance: WebAssembly.Instance
|
||||
module: WebAssembly.Module
|
||||
memory?: WebAssembly.Memory
|
||||
table?: WebAssembly.Table
|
||||
}
|
||||
|
||||
export declare type InputType = string | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
/** @public */
|
||||
export declare interface InstantiatedSource extends LoadedSource {
|
||||
napiModule: NapiModule;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare function instantiateNapiModule(
|
||||
/** Only support `BufferSource` or `WebAssembly.Module` on Node.js */
|
||||
wasmInput: InputType | Promise<InputType>, options: InstantiateOptions): Promise<InstantiatedSource>;
|
||||
|
||||
/** @public */
|
||||
export declare function instantiateNapiModuleSync(wasmInput: BufferSource | WebAssembly.Module, options: InstantiateOptions): InstantiatedSource;
|
||||
|
||||
/** @public */
|
||||
export declare type InstantiateOptions = CreateOptions & LoadOptions;
|
||||
|
||||
/** @public */
|
||||
export declare function isSharedArrayBuffer(value: any): value is SharedArrayBuffer;
|
||||
|
||||
/** @public */
|
||||
export declare function isTrapError(e: Error): e is WebAssembly.RuntimeError;
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadedPayload {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadedSource extends WebAssembly.WebAssemblyInstantiatedSource {
|
||||
usedInstance: WebAssembly.Instance;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare function loadNapiModule(napiModule: NapiModule,
|
||||
/** Only support `BufferSource` or `WebAssembly.Module` on Node.js */
|
||||
wasmInput: InputType | Promise<InputType>, options?: LoadOptions): Promise<LoadedSource>;
|
||||
|
||||
/** @public */
|
||||
export declare function loadNapiModuleSync(napiModule: NapiModule, wasmInput: BufferSource | WebAssembly.Module, options?: LoadOptions): LoadedSource;
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadOptions {
|
||||
wasi?: WASIInstance;
|
||||
overwriteImports?: (importObject: WebAssembly.Imports) => WebAssembly.Imports;
|
||||
beforeInit?: (source: WebAssembly.WebAssemblyInstantiatedSource) => void;
|
||||
getMemory?: (exports: WebAssembly.Exports) => WebAssembly.Memory;
|
||||
getTable?: (exports: WebAssembly.Exports) => WebAssembly.Table;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadPayload {
|
||||
wasmModule: WebAssembly.Module;
|
||||
wasmMemory: WebAssembly.Memory;
|
||||
sab?: Int32Array;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadBaseOptions extends BaseOptions {
|
||||
waitThreadStart?: boolean | number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type MainThreadOptions = MainThreadOptionsWithThreadManager | MainThreadOptionsCreateThreadManager;
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadOptionsCreateThreadManager extends MainThreadBaseOptions, ThreadManagerOptionsMain {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadOptionsWithThreadManager extends MainThreadBaseOptions {
|
||||
threadManager?: ThreadManager | (() => ThreadManager);
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MessageEventData<T extends CommandType> {
|
||||
__emnapi__: CommandInfo<T>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class MessageHandler extends ThreadMessageHandler {
|
||||
napiModule: NapiModule | undefined;
|
||||
constructor(options: MessageHandlerOptions);
|
||||
instantiate(data: LoadPayload): InstantiatedSource | PromiseLike<InstantiatedSource>;
|
||||
handle(e: WorkerMessageEvent): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MessageHandlerOptions extends ThreadMessageHandlerOptions {
|
||||
onLoad: (data: LoadPayload) => InstantiatedSource | PromiseLike<InstantiatedSource>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface NapiModule {
|
||||
imports: {
|
||||
env: any
|
||||
napi: any
|
||||
emnapi: any
|
||||
}
|
||||
exports: any
|
||||
loaded: boolean
|
||||
filename: string
|
||||
childThread: boolean
|
||||
emnapi: {
|
||||
syncMemory<T extends ArrayBuffer | ArrayBufferView> (
|
||||
js_to_wasm: boolean,
|
||||
arrayBufferOrView: T,
|
||||
offset?: number,
|
||||
len?: number
|
||||
): T
|
||||
getMemoryAddress (arrayBufferOrView: ArrayBuffer | ArrayBufferView): PointerInfo
|
||||
addSendListener (worker: any): boolean
|
||||
}
|
||||
|
||||
init (options: InitOptions): any
|
||||
initWorker (arg: number, func: [number, number]): void
|
||||
postMessage?: (msg: any) => any
|
||||
|
||||
waitThreadStart: boolean | number
|
||||
/* Excluded from this release type: PThread */}
|
||||
|
||||
/** @public */
|
||||
export declare interface NodeBinding {
|
||||
node: {
|
||||
emitAsyncInit: Function
|
||||
emitAsyncDestroy: Function
|
||||
makeCallback: Function
|
||||
}
|
||||
napi: {
|
||||
asyncInit: Function
|
||||
asyncDestroy: Function
|
||||
makeCallback: Function
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface PointerInfo {
|
||||
address: number
|
||||
ownership: ReferenceOwnership
|
||||
runtimeAllocated: 0 | 1
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ReuseWorkerOptions {
|
||||
/**
|
||||
* @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size | PTHREAD_POOL_SIZE}
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size-strict | PTHREAD_POOL_SIZE_STRICT}
|
||||
*/
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface SpawnThreadPayload {
|
||||
startArg: number;
|
||||
errorOrTid: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface StartPayload {
|
||||
tid: number;
|
||||
arg: number;
|
||||
sab?: Int32Array;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface StartResult {
|
||||
exitCode: number;
|
||||
instance: WebAssembly.Instance;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface TerminateAllThreadsPayload {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class ThreadManager {
|
||||
unusedWorkers: WorkerLike[];
|
||||
runningWorkers: WorkerLike[];
|
||||
pthreads: Record<number, WorkerLike>;
|
||||
get nextWorkerID(): number;
|
||||
wasmModule: WebAssembly.Module | null;
|
||||
wasmMemory: WebAssembly.Memory | null;
|
||||
private readonly messageEvents;
|
||||
private readonly _childThread;
|
||||
private readonly _onCreateWorker;
|
||||
private readonly _reuseWorker;
|
||||
private readonly _beforeLoad?;
|
||||
/* Excluded from this release type: printErr */
|
||||
threadSpawn?: ((startArg: number, errorOrTid?: number) => number);
|
||||
constructor(options: ThreadManagerOptions);
|
||||
init(): void;
|
||||
initMainThread(): void;
|
||||
private preparePool;
|
||||
shouldPreloadWorkers(): boolean;
|
||||
loadWasmModuleToAllWorkers(): Promise<WorkerLike[]>;
|
||||
preloadWorkers(): Promise<WorkerLike[]>;
|
||||
setup(wasmModule: WebAssembly.Module, wasmMemory: WebAssembly.Memory): void;
|
||||
markId(worker: WorkerLike): number;
|
||||
returnWorkerToPool(worker: WorkerLike): void;
|
||||
loadWasmModuleToWorker(worker: WorkerLike, sab?: Int32Array): Promise<WorkerLike>;
|
||||
allocateUnusedWorker(): WorkerLike;
|
||||
getNewWorker(sab?: Int32Array): WorkerLike | undefined;
|
||||
cleanThread(worker: WorkerLike, tid: number, force?: boolean): void;
|
||||
terminateWorker(worker: WorkerLike): void;
|
||||
terminateAllThreads(): void;
|
||||
addMessageEventListener(worker: WorkerLike, onMessage: (e: WorkerMessageEvent) => void): () => void;
|
||||
fireMessageEvent(worker: WorkerLike, e: WorkerMessageEvent): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type ThreadManagerOptions = ThreadManagerOptionsMain | ThreadManagerOptionsChild;
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsBase {
|
||||
printErr?: (message: string) => void;
|
||||
threadSpawn?: (startArg: number, errorOrTid?: number) => number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsChild extends ThreadManagerOptionsBase {
|
||||
childThread: true;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsMain extends ThreadManagerOptionsBase {
|
||||
beforeLoad?: (worker: WorkerLike) => any;
|
||||
reuseWorker?: boolean | number | ReuseWorkerOptions;
|
||||
onCreateWorker: WorkerFactory;
|
||||
childThread?: false;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class ThreadMessageHandler {
|
||||
protected instance: WebAssembly.Instance | undefined;
|
||||
private messagesBeforeLoad;
|
||||
protected postMessage: (message: any) => void;
|
||||
protected onLoad?: (data: LoadPayload) => WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
protected onError: (error: Error, type: WorkerMessageType) => void;
|
||||
constructor(options?: ThreadMessageHandlerOptions);
|
||||
/** @virtual */
|
||||
instantiate(data: LoadPayload): WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
/** @virtual */
|
||||
handle(e: WorkerMessageEvent<MessageEventData<WorkerMessageType>>): void;
|
||||
private _load;
|
||||
private _start;
|
||||
protected _loaded(err: Error | null, source: WebAssembly.WebAssemblyInstantiatedSource | null, payload: LoadPayload): void;
|
||||
protected handleAfterLoad<E extends WorkerMessageEvent>(e: E, f: (e: E) => void): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadMessageHandlerOptions {
|
||||
onLoad?: (data: LoadPayload) => WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
onError?: (error: Error, type: WorkerMessageType) => void;
|
||||
postMessage?: (message: any) => void;
|
||||
}
|
||||
|
||||
export declare const version: string;
|
||||
|
||||
/** @public */
|
||||
export declare interface WASIInstance {
|
||||
readonly wasiImport?: Record<string, any>;
|
||||
initialize(instance: object): void;
|
||||
start(instance: object): number;
|
||||
getImportObject?(): any;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class WASIThreads {
|
||||
PThread: ThreadManager | undefined;
|
||||
private wasmMemory;
|
||||
private wasmInstance;
|
||||
private readonly threadSpawn;
|
||||
readonly childThread: boolean;
|
||||
private readonly postMessage;
|
||||
readonly wasi: WASIInstance;
|
||||
constructor(options: WASIThreadsOptions);
|
||||
getImportObject(): {
|
||||
wasi: WASIThreadsImports;
|
||||
};
|
||||
setup(wasmInstance: WebAssembly.Instance, wasmModule: WebAssembly.Module, wasmMemory?: WebAssembly.Memory): void;
|
||||
preloadWorkers(): Promise<WorkerLike[]>;
|
||||
/**
|
||||
* It's ok to call this method to a WASI command module.
|
||||
*
|
||||
* in child thread, must call this method instead of {@link WASIThreads.start} even if it's a WASI command module
|
||||
*
|
||||
* @returns A proxied WebAssembly instance if in child thread, other wise the original instance
|
||||
*/
|
||||
initialize(instance: WebAssembly.Instance, module: WebAssembly.Module, memory?: WebAssembly.Memory): WebAssembly.Instance;
|
||||
/**
|
||||
* Equivalent to calling {@link WASIThreads.initialize} and then calling {@link WASIInstance.start}
|
||||
* ```js
|
||||
* this.initialize(instance, module, memory)
|
||||
* this.wasi.start(instance)
|
||||
* ```
|
||||
*/
|
||||
start(instance: WebAssembly.Instance, module: WebAssembly.Module, memory?: WebAssembly.Memory): StartResult;
|
||||
terminateAllThreads(): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface WASIThreadsImports {
|
||||
'thread-spawn': (startArg: number, errorOrTid?: number) => number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type WASIThreadsOptions = MainThreadOptions | ChildThreadOptions;
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerFactory = (ctx: {
|
||||
type: string;
|
||||
name: string;
|
||||
}) => WorkerLike;
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerLike = (Worker | Worker_2) & {
|
||||
whenLoaded?: Promise<WorkerLike>;
|
||||
loaded?: boolean;
|
||||
__emnapi_tid?: number;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export declare interface WorkerMessageEvent<T = any> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerMessageType = 'load' | 'start';
|
||||
|
||||
export { }
|
||||
+1
File diff suppressed because one or more lines are too long
+420
@@ -0,0 +1,420 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import type { Context } from '@emnapi/runtime';
|
||||
import type { ReferenceOwnership } from '@emnapi/runtime';
|
||||
import type { Worker as Worker_2 } from 'worker_threads';
|
||||
|
||||
/** @public */
|
||||
export declare type BaseCreateOptions = {
|
||||
filename?: string
|
||||
nodeBinding?: NodeBinding
|
||||
reuseWorker?: ThreadManagerOptionsMain['reuseWorker']
|
||||
asyncWorkPoolSize?: number
|
||||
waitThreadStart?: MainThreadBaseOptions['waitThreadStart']
|
||||
onCreateWorker?: (info: CreateWorkerInfo) => any
|
||||
print?: (str: string) => void
|
||||
printErr?: (str: string) => void
|
||||
postMessage?: (msg: any) => any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface BaseOptions {
|
||||
wasi: WASIInstance;
|
||||
version?: 'preview1';
|
||||
wasm64?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ChildThreadOptions extends BaseOptions {
|
||||
childThread: true;
|
||||
postMessage?: (data: any) => void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CleanupThreadPayload {
|
||||
tid: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CommandInfo<T extends CommandType> {
|
||||
type: T;
|
||||
payload: CommandPayloadMap[T];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CommandPayloadMap {
|
||||
load: LoadPayload;
|
||||
loaded: LoadedPayload;
|
||||
start: StartPayload;
|
||||
'cleanup-thread': CleanupThreadPayload;
|
||||
'terminate-all-threads': TerminateAllThreadsPayload;
|
||||
'spawn-thread': SpawnThreadPayload;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type CommandType = keyof CommandPayloadMap;
|
||||
|
||||
/** @public */
|
||||
export declare function createInstanceProxy(instance: WebAssembly.Instance, memory?: WebAssembly.Memory | (() => WebAssembly.Memory)): WebAssembly.Instance;
|
||||
|
||||
/** @public */
|
||||
export declare function createNapiModule (
|
||||
options: CreateOptions
|
||||
): NapiModule
|
||||
|
||||
/** @public */
|
||||
export declare type CreateOptions = BaseCreateOptions & ({
|
||||
context: Context
|
||||
childThread?: boolean
|
||||
} | {
|
||||
context?: Context
|
||||
childThread: true
|
||||
})
|
||||
|
||||
/** @public */
|
||||
export declare interface CreateWorkerInfo {
|
||||
type: 'thread' | 'async-work'
|
||||
name: string
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface InitOptions {
|
||||
instance: WebAssembly.Instance
|
||||
module: WebAssembly.Module
|
||||
memory?: WebAssembly.Memory
|
||||
table?: WebAssembly.Table
|
||||
}
|
||||
|
||||
export declare type InputType = string | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
/** @public */
|
||||
export declare interface InstantiatedSource extends LoadedSource {
|
||||
napiModule: NapiModule;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare function instantiateNapiModule(
|
||||
/** Only support `BufferSource` or `WebAssembly.Module` on Node.js */
|
||||
wasmInput: InputType | Promise<InputType>, options: InstantiateOptions): Promise<InstantiatedSource>;
|
||||
|
||||
/** @public */
|
||||
export declare function instantiateNapiModuleSync(wasmInput: BufferSource | WebAssembly.Module, options: InstantiateOptions): InstantiatedSource;
|
||||
|
||||
/** @public */
|
||||
export declare type InstantiateOptions = CreateOptions & LoadOptions;
|
||||
|
||||
/** @public */
|
||||
export declare function isSharedArrayBuffer(value: any): value is SharedArrayBuffer;
|
||||
|
||||
/** @public */
|
||||
export declare function isTrapError(e: Error): e is WebAssembly.RuntimeError;
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadedPayload {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadedSource extends WebAssembly.WebAssemblyInstantiatedSource {
|
||||
usedInstance: WebAssembly.Instance;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare function loadNapiModule(napiModule: NapiModule,
|
||||
/** Only support `BufferSource` or `WebAssembly.Module` on Node.js */
|
||||
wasmInput: InputType | Promise<InputType>, options?: LoadOptions): Promise<LoadedSource>;
|
||||
|
||||
/** @public */
|
||||
export declare function loadNapiModuleSync(napiModule: NapiModule, wasmInput: BufferSource | WebAssembly.Module, options?: LoadOptions): LoadedSource;
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadOptions {
|
||||
wasi?: WASIInstance;
|
||||
overwriteImports?: (importObject: WebAssembly.Imports) => WebAssembly.Imports;
|
||||
beforeInit?: (source: WebAssembly.WebAssemblyInstantiatedSource) => void;
|
||||
getMemory?: (exports: WebAssembly.Exports) => WebAssembly.Memory;
|
||||
getTable?: (exports: WebAssembly.Exports) => WebAssembly.Table;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadPayload {
|
||||
wasmModule: WebAssembly.Module;
|
||||
wasmMemory: WebAssembly.Memory;
|
||||
sab?: Int32Array;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadBaseOptions extends BaseOptions {
|
||||
waitThreadStart?: boolean | number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type MainThreadOptions = MainThreadOptionsWithThreadManager | MainThreadOptionsCreateThreadManager;
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadOptionsCreateThreadManager extends MainThreadBaseOptions, ThreadManagerOptionsMain {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadOptionsWithThreadManager extends MainThreadBaseOptions {
|
||||
threadManager?: ThreadManager | (() => ThreadManager);
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MessageEventData<T extends CommandType> {
|
||||
__emnapi__: CommandInfo<T>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class MessageHandler extends ThreadMessageHandler {
|
||||
napiModule: NapiModule | undefined;
|
||||
constructor(options: MessageHandlerOptions);
|
||||
instantiate(data: LoadPayload): InstantiatedSource | PromiseLike<InstantiatedSource>;
|
||||
handle(e: WorkerMessageEvent): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MessageHandlerOptions extends ThreadMessageHandlerOptions {
|
||||
onLoad: (data: LoadPayload) => InstantiatedSource | PromiseLike<InstantiatedSource>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface NapiModule {
|
||||
imports: {
|
||||
env: any
|
||||
napi: any
|
||||
emnapi: any
|
||||
}
|
||||
exports: any
|
||||
loaded: boolean
|
||||
filename: string
|
||||
childThread: boolean
|
||||
emnapi: {
|
||||
syncMemory<T extends ArrayBuffer | ArrayBufferView> (
|
||||
js_to_wasm: boolean,
|
||||
arrayBufferOrView: T,
|
||||
offset?: number,
|
||||
len?: number
|
||||
): T
|
||||
getMemoryAddress (arrayBufferOrView: ArrayBuffer | ArrayBufferView): PointerInfo
|
||||
addSendListener (worker: any): boolean
|
||||
}
|
||||
|
||||
init (options: InitOptions): any
|
||||
initWorker (arg: number, func: [number, number]): void
|
||||
postMessage?: (msg: any) => any
|
||||
|
||||
waitThreadStart: boolean | number
|
||||
/* Excluded from this release type: PThread */}
|
||||
|
||||
/** @public */
|
||||
export declare interface NodeBinding {
|
||||
node: {
|
||||
emitAsyncInit: Function
|
||||
emitAsyncDestroy: Function
|
||||
makeCallback: Function
|
||||
}
|
||||
napi: {
|
||||
asyncInit: Function
|
||||
asyncDestroy: Function
|
||||
makeCallback: Function
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface PointerInfo {
|
||||
address: number
|
||||
ownership: ReferenceOwnership
|
||||
runtimeAllocated: 0 | 1
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ReuseWorkerOptions {
|
||||
/**
|
||||
* @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size | PTHREAD_POOL_SIZE}
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size-strict | PTHREAD_POOL_SIZE_STRICT}
|
||||
*/
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface SpawnThreadPayload {
|
||||
startArg: number;
|
||||
errorOrTid: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface StartPayload {
|
||||
tid: number;
|
||||
arg: number;
|
||||
sab?: Int32Array;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface StartResult {
|
||||
exitCode: number;
|
||||
instance: WebAssembly.Instance;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface TerminateAllThreadsPayload {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class ThreadManager {
|
||||
unusedWorkers: WorkerLike[];
|
||||
runningWorkers: WorkerLike[];
|
||||
pthreads: Record<number, WorkerLike>;
|
||||
get nextWorkerID(): number;
|
||||
wasmModule: WebAssembly.Module | null;
|
||||
wasmMemory: WebAssembly.Memory | null;
|
||||
private readonly messageEvents;
|
||||
private readonly _childThread;
|
||||
private readonly _onCreateWorker;
|
||||
private readonly _reuseWorker;
|
||||
private readonly _beforeLoad?;
|
||||
/* Excluded from this release type: printErr */
|
||||
threadSpawn?: ((startArg: number, errorOrTid?: number) => number);
|
||||
constructor(options: ThreadManagerOptions);
|
||||
init(): void;
|
||||
initMainThread(): void;
|
||||
private preparePool;
|
||||
shouldPreloadWorkers(): boolean;
|
||||
loadWasmModuleToAllWorkers(): Promise<WorkerLike[]>;
|
||||
preloadWorkers(): Promise<WorkerLike[]>;
|
||||
setup(wasmModule: WebAssembly.Module, wasmMemory: WebAssembly.Memory): void;
|
||||
markId(worker: WorkerLike): number;
|
||||
returnWorkerToPool(worker: WorkerLike): void;
|
||||
loadWasmModuleToWorker(worker: WorkerLike, sab?: Int32Array): Promise<WorkerLike>;
|
||||
allocateUnusedWorker(): WorkerLike;
|
||||
getNewWorker(sab?: Int32Array): WorkerLike | undefined;
|
||||
cleanThread(worker: WorkerLike, tid: number, force?: boolean): void;
|
||||
terminateWorker(worker: WorkerLike): void;
|
||||
terminateAllThreads(): void;
|
||||
addMessageEventListener(worker: WorkerLike, onMessage: (e: WorkerMessageEvent) => void): () => void;
|
||||
fireMessageEvent(worker: WorkerLike, e: WorkerMessageEvent): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type ThreadManagerOptions = ThreadManagerOptionsMain | ThreadManagerOptionsChild;
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsBase {
|
||||
printErr?: (message: string) => void;
|
||||
threadSpawn?: (startArg: number, errorOrTid?: number) => number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsChild extends ThreadManagerOptionsBase {
|
||||
childThread: true;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsMain extends ThreadManagerOptionsBase {
|
||||
beforeLoad?: (worker: WorkerLike) => any;
|
||||
reuseWorker?: boolean | number | ReuseWorkerOptions;
|
||||
onCreateWorker: WorkerFactory;
|
||||
childThread?: false;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class ThreadMessageHandler {
|
||||
protected instance: WebAssembly.Instance | undefined;
|
||||
private messagesBeforeLoad;
|
||||
protected postMessage: (message: any) => void;
|
||||
protected onLoad?: (data: LoadPayload) => WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
protected onError: (error: Error, type: WorkerMessageType) => void;
|
||||
constructor(options?: ThreadMessageHandlerOptions);
|
||||
/** @virtual */
|
||||
instantiate(data: LoadPayload): WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
/** @virtual */
|
||||
handle(e: WorkerMessageEvent<MessageEventData<WorkerMessageType>>): void;
|
||||
private _load;
|
||||
private _start;
|
||||
protected _loaded(err: Error | null, source: WebAssembly.WebAssemblyInstantiatedSource | null, payload: LoadPayload): void;
|
||||
protected handleAfterLoad<E extends WorkerMessageEvent>(e: E, f: (e: E) => void): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadMessageHandlerOptions {
|
||||
onLoad?: (data: LoadPayload) => WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
onError?: (error: Error, type: WorkerMessageType) => void;
|
||||
postMessage?: (message: any) => void;
|
||||
}
|
||||
|
||||
export declare const version: string;
|
||||
|
||||
/** @public */
|
||||
export declare interface WASIInstance {
|
||||
readonly wasiImport?: Record<string, any>;
|
||||
initialize(instance: object): void;
|
||||
start(instance: object): number;
|
||||
getImportObject?(): any;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class WASIThreads {
|
||||
PThread: ThreadManager | undefined;
|
||||
private wasmMemory;
|
||||
private wasmInstance;
|
||||
private readonly threadSpawn;
|
||||
readonly childThread: boolean;
|
||||
private readonly postMessage;
|
||||
readonly wasi: WASIInstance;
|
||||
constructor(options: WASIThreadsOptions);
|
||||
getImportObject(): {
|
||||
wasi: WASIThreadsImports;
|
||||
};
|
||||
setup(wasmInstance: WebAssembly.Instance, wasmModule: WebAssembly.Module, wasmMemory?: WebAssembly.Memory): void;
|
||||
preloadWorkers(): Promise<WorkerLike[]>;
|
||||
/**
|
||||
* It's ok to call this method to a WASI command module.
|
||||
*
|
||||
* in child thread, must call this method instead of {@link WASIThreads.start} even if it's a WASI command module
|
||||
*
|
||||
* @returns A proxied WebAssembly instance if in child thread, other wise the original instance
|
||||
*/
|
||||
initialize(instance: WebAssembly.Instance, module: WebAssembly.Module, memory?: WebAssembly.Memory): WebAssembly.Instance;
|
||||
/**
|
||||
* Equivalent to calling {@link WASIThreads.initialize} and then calling {@link WASIInstance.start}
|
||||
* ```js
|
||||
* this.initialize(instance, module, memory)
|
||||
* this.wasi.start(instance)
|
||||
* ```
|
||||
*/
|
||||
start(instance: WebAssembly.Instance, module: WebAssembly.Module, memory?: WebAssembly.Memory): StartResult;
|
||||
terminateAllThreads(): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface WASIThreadsImports {
|
||||
'thread-spawn': (startArg: number, errorOrTid?: number) => number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type WASIThreadsOptions = MainThreadOptions | ChildThreadOptions;
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerFactory = (ctx: {
|
||||
type: string;
|
||||
name: string;
|
||||
}) => WorkerLike;
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerLike = (Worker | Worker_2) & {
|
||||
whenLoaded?: Promise<WorkerLike>;
|
||||
loaded?: boolean;
|
||||
__emnapi_tid?: number;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export declare interface WorkerMessageEvent<T = any> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerMessageType = 'load' | 'start';
|
||||
|
||||
export { }
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import type { Context } from '@emnapi/runtime';
|
||||
import type { ReferenceOwnership } from '@emnapi/runtime';
|
||||
import type { Worker as Worker_2 } from 'worker_threads';
|
||||
|
||||
/** @public */
|
||||
export declare type BaseCreateOptions = {
|
||||
filename?: string
|
||||
nodeBinding?: NodeBinding
|
||||
reuseWorker?: ThreadManagerOptionsMain['reuseWorker']
|
||||
asyncWorkPoolSize?: number
|
||||
waitThreadStart?: MainThreadBaseOptions['waitThreadStart']
|
||||
onCreateWorker?: (info: CreateWorkerInfo) => any
|
||||
print?: (str: string) => void
|
||||
printErr?: (str: string) => void
|
||||
postMessage?: (msg: any) => any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface BaseOptions {
|
||||
wasi: WASIInstance;
|
||||
version?: 'preview1';
|
||||
wasm64?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ChildThreadOptions extends BaseOptions {
|
||||
childThread: true;
|
||||
postMessage?: (data: any) => void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CleanupThreadPayload {
|
||||
tid: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CommandInfo<T extends CommandType> {
|
||||
type: T;
|
||||
payload: CommandPayloadMap[T];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CommandPayloadMap {
|
||||
load: LoadPayload;
|
||||
loaded: LoadedPayload;
|
||||
start: StartPayload;
|
||||
'cleanup-thread': CleanupThreadPayload;
|
||||
'terminate-all-threads': TerminateAllThreadsPayload;
|
||||
'spawn-thread': SpawnThreadPayload;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type CommandType = keyof CommandPayloadMap;
|
||||
|
||||
/** @public */
|
||||
export declare function createInstanceProxy(instance: WebAssembly.Instance, memory?: WebAssembly.Memory | (() => WebAssembly.Memory)): WebAssembly.Instance;
|
||||
|
||||
/** @public */
|
||||
export declare function createNapiModule (
|
||||
options: CreateOptions
|
||||
): NapiModule
|
||||
|
||||
/** @public */
|
||||
export declare type CreateOptions = BaseCreateOptions & ({
|
||||
context: Context
|
||||
childThread?: boolean
|
||||
} | {
|
||||
context?: Context
|
||||
childThread: true
|
||||
})
|
||||
|
||||
/** @public */
|
||||
export declare interface CreateWorkerInfo {
|
||||
type: 'thread' | 'async-work'
|
||||
name: string
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface InitOptions {
|
||||
instance: WebAssembly.Instance
|
||||
module: WebAssembly.Module
|
||||
memory?: WebAssembly.Memory
|
||||
table?: WebAssembly.Table
|
||||
}
|
||||
|
||||
export declare type InputType = string | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
/** @public */
|
||||
export declare interface InstantiatedSource extends LoadedSource {
|
||||
napiModule: NapiModule;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare function instantiateNapiModule(
|
||||
/** Only support `BufferSource` or `WebAssembly.Module` on Node.js */
|
||||
wasmInput: InputType | Promise<InputType>, options: InstantiateOptions): Promise<InstantiatedSource>;
|
||||
|
||||
/** @public */
|
||||
export declare function instantiateNapiModuleSync(wasmInput: BufferSource | WebAssembly.Module, options: InstantiateOptions): InstantiatedSource;
|
||||
|
||||
/** @public */
|
||||
export declare type InstantiateOptions = CreateOptions & LoadOptions;
|
||||
|
||||
/** @public */
|
||||
export declare function isSharedArrayBuffer(value: any): value is SharedArrayBuffer;
|
||||
|
||||
/** @public */
|
||||
export declare function isTrapError(e: Error): e is WebAssembly.RuntimeError;
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadedPayload {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadedSource extends WebAssembly.WebAssemblyInstantiatedSource {
|
||||
usedInstance: WebAssembly.Instance;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare function loadNapiModule(napiModule: NapiModule,
|
||||
/** Only support `BufferSource` or `WebAssembly.Module` on Node.js */
|
||||
wasmInput: InputType | Promise<InputType>, options?: LoadOptions): Promise<LoadedSource>;
|
||||
|
||||
/** @public */
|
||||
export declare function loadNapiModuleSync(napiModule: NapiModule, wasmInput: BufferSource | WebAssembly.Module, options?: LoadOptions): LoadedSource;
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadOptions {
|
||||
wasi?: WASIInstance;
|
||||
overwriteImports?: (importObject: WebAssembly.Imports) => WebAssembly.Imports;
|
||||
beforeInit?: (source: WebAssembly.WebAssemblyInstantiatedSource) => void;
|
||||
getMemory?: (exports: WebAssembly.Exports) => WebAssembly.Memory;
|
||||
getTable?: (exports: WebAssembly.Exports) => WebAssembly.Table;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadPayload {
|
||||
wasmModule: WebAssembly.Module;
|
||||
wasmMemory: WebAssembly.Memory;
|
||||
sab?: Int32Array;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadBaseOptions extends BaseOptions {
|
||||
waitThreadStart?: boolean | number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type MainThreadOptions = MainThreadOptionsWithThreadManager | MainThreadOptionsCreateThreadManager;
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadOptionsCreateThreadManager extends MainThreadBaseOptions, ThreadManagerOptionsMain {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadOptionsWithThreadManager extends MainThreadBaseOptions {
|
||||
threadManager?: ThreadManager | (() => ThreadManager);
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MessageEventData<T extends CommandType> {
|
||||
__emnapi__: CommandInfo<T>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class MessageHandler extends ThreadMessageHandler {
|
||||
napiModule: NapiModule | undefined;
|
||||
constructor(options: MessageHandlerOptions);
|
||||
instantiate(data: LoadPayload): InstantiatedSource | PromiseLike<InstantiatedSource>;
|
||||
handle(e: WorkerMessageEvent): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MessageHandlerOptions extends ThreadMessageHandlerOptions {
|
||||
onLoad: (data: LoadPayload) => InstantiatedSource | PromiseLike<InstantiatedSource>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface NapiModule {
|
||||
imports: {
|
||||
env: any
|
||||
napi: any
|
||||
emnapi: any
|
||||
}
|
||||
exports: any
|
||||
loaded: boolean
|
||||
filename: string
|
||||
childThread: boolean
|
||||
emnapi: {
|
||||
syncMemory<T extends ArrayBuffer | ArrayBufferView> (
|
||||
js_to_wasm: boolean,
|
||||
arrayBufferOrView: T,
|
||||
offset?: number,
|
||||
len?: number
|
||||
): T
|
||||
getMemoryAddress (arrayBufferOrView: ArrayBuffer | ArrayBufferView): PointerInfo
|
||||
addSendListener (worker: any): boolean
|
||||
}
|
||||
|
||||
init (options: InitOptions): any
|
||||
initWorker (arg: number, func: [number, number]): void
|
||||
postMessage?: (msg: any) => any
|
||||
|
||||
waitThreadStart: boolean | number
|
||||
/* Excluded from this release type: PThread */}
|
||||
|
||||
/** @public */
|
||||
export declare interface NodeBinding {
|
||||
node: {
|
||||
emitAsyncInit: Function
|
||||
emitAsyncDestroy: Function
|
||||
makeCallback: Function
|
||||
}
|
||||
napi: {
|
||||
asyncInit: Function
|
||||
asyncDestroy: Function
|
||||
makeCallback: Function
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface PointerInfo {
|
||||
address: number
|
||||
ownership: ReferenceOwnership
|
||||
runtimeAllocated: 0 | 1
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ReuseWorkerOptions {
|
||||
/**
|
||||
* @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size | PTHREAD_POOL_SIZE}
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size-strict | PTHREAD_POOL_SIZE_STRICT}
|
||||
*/
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface SpawnThreadPayload {
|
||||
startArg: number;
|
||||
errorOrTid: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface StartPayload {
|
||||
tid: number;
|
||||
arg: number;
|
||||
sab?: Int32Array;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface StartResult {
|
||||
exitCode: number;
|
||||
instance: WebAssembly.Instance;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface TerminateAllThreadsPayload {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class ThreadManager {
|
||||
unusedWorkers: WorkerLike[];
|
||||
runningWorkers: WorkerLike[];
|
||||
pthreads: Record<number, WorkerLike>;
|
||||
get nextWorkerID(): number;
|
||||
wasmModule: WebAssembly.Module | null;
|
||||
wasmMemory: WebAssembly.Memory | null;
|
||||
private readonly messageEvents;
|
||||
private readonly _childThread;
|
||||
private readonly _onCreateWorker;
|
||||
private readonly _reuseWorker;
|
||||
private readonly _beforeLoad?;
|
||||
/* Excluded from this release type: printErr */
|
||||
threadSpawn?: ((startArg: number, errorOrTid?: number) => number);
|
||||
constructor(options: ThreadManagerOptions);
|
||||
init(): void;
|
||||
initMainThread(): void;
|
||||
private preparePool;
|
||||
shouldPreloadWorkers(): boolean;
|
||||
loadWasmModuleToAllWorkers(): Promise<WorkerLike[]>;
|
||||
preloadWorkers(): Promise<WorkerLike[]>;
|
||||
setup(wasmModule: WebAssembly.Module, wasmMemory: WebAssembly.Memory): void;
|
||||
markId(worker: WorkerLike): number;
|
||||
returnWorkerToPool(worker: WorkerLike): void;
|
||||
loadWasmModuleToWorker(worker: WorkerLike, sab?: Int32Array): Promise<WorkerLike>;
|
||||
allocateUnusedWorker(): WorkerLike;
|
||||
getNewWorker(sab?: Int32Array): WorkerLike | undefined;
|
||||
cleanThread(worker: WorkerLike, tid: number, force?: boolean): void;
|
||||
terminateWorker(worker: WorkerLike): void;
|
||||
terminateAllThreads(): void;
|
||||
addMessageEventListener(worker: WorkerLike, onMessage: (e: WorkerMessageEvent) => void): () => void;
|
||||
fireMessageEvent(worker: WorkerLike, e: WorkerMessageEvent): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type ThreadManagerOptions = ThreadManagerOptionsMain | ThreadManagerOptionsChild;
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsBase {
|
||||
printErr?: (message: string) => void;
|
||||
threadSpawn?: (startArg: number, errorOrTid?: number) => number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsChild extends ThreadManagerOptionsBase {
|
||||
childThread: true;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsMain extends ThreadManagerOptionsBase {
|
||||
beforeLoad?: (worker: WorkerLike) => any;
|
||||
reuseWorker?: boolean | number | ReuseWorkerOptions;
|
||||
onCreateWorker: WorkerFactory;
|
||||
childThread?: false;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class ThreadMessageHandler {
|
||||
protected instance: WebAssembly.Instance | undefined;
|
||||
private messagesBeforeLoad;
|
||||
protected postMessage: (message: any) => void;
|
||||
protected onLoad?: (data: LoadPayload) => WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
protected onError: (error: Error, type: WorkerMessageType) => void;
|
||||
constructor(options?: ThreadMessageHandlerOptions);
|
||||
/** @virtual */
|
||||
instantiate(data: LoadPayload): WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
/** @virtual */
|
||||
handle(e: WorkerMessageEvent<MessageEventData<WorkerMessageType>>): void;
|
||||
private _load;
|
||||
private _start;
|
||||
protected _loaded(err: Error | null, source: WebAssembly.WebAssemblyInstantiatedSource | null, payload: LoadPayload): void;
|
||||
protected handleAfterLoad<E extends WorkerMessageEvent>(e: E, f: (e: E) => void): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadMessageHandlerOptions {
|
||||
onLoad?: (data: LoadPayload) => WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
onError?: (error: Error, type: WorkerMessageType) => void;
|
||||
postMessage?: (message: any) => void;
|
||||
}
|
||||
|
||||
export declare const version: string;
|
||||
|
||||
/** @public */
|
||||
export declare interface WASIInstance {
|
||||
readonly wasiImport?: Record<string, any>;
|
||||
initialize(instance: object): void;
|
||||
start(instance: object): number;
|
||||
getImportObject?(): any;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class WASIThreads {
|
||||
PThread: ThreadManager | undefined;
|
||||
private wasmMemory;
|
||||
private wasmInstance;
|
||||
private readonly threadSpawn;
|
||||
readonly childThread: boolean;
|
||||
private readonly postMessage;
|
||||
readonly wasi: WASIInstance;
|
||||
constructor(options: WASIThreadsOptions);
|
||||
getImportObject(): {
|
||||
wasi: WASIThreadsImports;
|
||||
};
|
||||
setup(wasmInstance: WebAssembly.Instance, wasmModule: WebAssembly.Module, wasmMemory?: WebAssembly.Memory): void;
|
||||
preloadWorkers(): Promise<WorkerLike[]>;
|
||||
/**
|
||||
* It's ok to call this method to a WASI command module.
|
||||
*
|
||||
* in child thread, must call this method instead of {@link WASIThreads.start} even if it's a WASI command module
|
||||
*
|
||||
* @returns A proxied WebAssembly instance if in child thread, other wise the original instance
|
||||
*/
|
||||
initialize(instance: WebAssembly.Instance, module: WebAssembly.Module, memory?: WebAssembly.Memory): WebAssembly.Instance;
|
||||
/**
|
||||
* Equivalent to calling {@link WASIThreads.initialize} and then calling {@link WASIInstance.start}
|
||||
* ```js
|
||||
* this.initialize(instance, module, memory)
|
||||
* this.wasi.start(instance)
|
||||
* ```
|
||||
*/
|
||||
start(instance: WebAssembly.Instance, module: WebAssembly.Module, memory?: WebAssembly.Memory): StartResult;
|
||||
terminateAllThreads(): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface WASIThreadsImports {
|
||||
'thread-spawn': (startArg: number, errorOrTid?: number) => number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type WASIThreadsOptions = MainThreadOptions | ChildThreadOptions;
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerFactory = (ctx: {
|
||||
type: string;
|
||||
name: string;
|
||||
}) => WorkerLike;
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerLike = (Worker | Worker_2) & {
|
||||
whenLoaded?: Promise<WorkerLike>;
|
||||
loaded?: boolean;
|
||||
__emnapi_tid?: number;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export declare interface WorkerMessageEvent<T = any> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerMessageType = 'load' | 'start';
|
||||
|
||||
export { }
|
||||
|
||||
export as namespace emnapiCore;
|
||||
+8228
File diff suppressed because it is too large
Load Diff
+9231
File diff suppressed because it is too large
Load Diff
+420
@@ -0,0 +1,420 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import type { Context } from '@emnapi/runtime';
|
||||
import type { ReferenceOwnership } from '@emnapi/runtime';
|
||||
import type { Worker as Worker_2 } from 'worker_threads';
|
||||
|
||||
/** @public */
|
||||
export declare type BaseCreateOptions = {
|
||||
filename?: string
|
||||
nodeBinding?: NodeBinding
|
||||
reuseWorker?: ThreadManagerOptionsMain['reuseWorker']
|
||||
asyncWorkPoolSize?: number
|
||||
waitThreadStart?: MainThreadBaseOptions['waitThreadStart']
|
||||
onCreateWorker?: (info: CreateWorkerInfo) => any
|
||||
print?: (str: string) => void
|
||||
printErr?: (str: string) => void
|
||||
postMessage?: (msg: any) => any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface BaseOptions {
|
||||
wasi: WASIInstance;
|
||||
version?: 'preview1';
|
||||
wasm64?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ChildThreadOptions extends BaseOptions {
|
||||
childThread: true;
|
||||
postMessage?: (data: any) => void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CleanupThreadPayload {
|
||||
tid: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CommandInfo<T extends CommandType> {
|
||||
type: T;
|
||||
payload: CommandPayloadMap[T];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface CommandPayloadMap {
|
||||
load: LoadPayload;
|
||||
loaded: LoadedPayload;
|
||||
start: StartPayload;
|
||||
'cleanup-thread': CleanupThreadPayload;
|
||||
'terminate-all-threads': TerminateAllThreadsPayload;
|
||||
'spawn-thread': SpawnThreadPayload;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type CommandType = keyof CommandPayloadMap;
|
||||
|
||||
/** @public */
|
||||
export declare function createInstanceProxy(instance: WebAssembly.Instance, memory?: WebAssembly.Memory | (() => WebAssembly.Memory)): WebAssembly.Instance;
|
||||
|
||||
/** @public */
|
||||
export declare function createNapiModule (
|
||||
options: CreateOptions
|
||||
): NapiModule
|
||||
|
||||
/** @public */
|
||||
export declare type CreateOptions = BaseCreateOptions & ({
|
||||
context: Context
|
||||
childThread?: boolean
|
||||
} | {
|
||||
context?: Context
|
||||
childThread: true
|
||||
})
|
||||
|
||||
/** @public */
|
||||
export declare interface CreateWorkerInfo {
|
||||
type: 'thread' | 'async-work'
|
||||
name: string
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface InitOptions {
|
||||
instance: WebAssembly.Instance
|
||||
module: WebAssembly.Module
|
||||
memory?: WebAssembly.Memory
|
||||
table?: WebAssembly.Table
|
||||
}
|
||||
|
||||
export declare type InputType = string | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
/** @public */
|
||||
export declare interface InstantiatedSource extends LoadedSource {
|
||||
napiModule: NapiModule;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare function instantiateNapiModule(
|
||||
/** Only support `BufferSource` or `WebAssembly.Module` on Node.js */
|
||||
wasmInput: InputType | Promise<InputType>, options: InstantiateOptions): Promise<InstantiatedSource>;
|
||||
|
||||
/** @public */
|
||||
export declare function instantiateNapiModuleSync(wasmInput: BufferSource | WebAssembly.Module, options: InstantiateOptions): InstantiatedSource;
|
||||
|
||||
/** @public */
|
||||
export declare type InstantiateOptions = CreateOptions & LoadOptions;
|
||||
|
||||
/** @public */
|
||||
export declare function isSharedArrayBuffer(value: any): value is SharedArrayBuffer;
|
||||
|
||||
/** @public */
|
||||
export declare function isTrapError(e: Error): e is WebAssembly.RuntimeError;
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadedPayload {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadedSource extends WebAssembly.WebAssemblyInstantiatedSource {
|
||||
usedInstance: WebAssembly.Instance;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare function loadNapiModule(napiModule: NapiModule,
|
||||
/** Only support `BufferSource` or `WebAssembly.Module` on Node.js */
|
||||
wasmInput: InputType | Promise<InputType>, options?: LoadOptions): Promise<LoadedSource>;
|
||||
|
||||
/** @public */
|
||||
export declare function loadNapiModuleSync(napiModule: NapiModule, wasmInput: BufferSource | WebAssembly.Module, options?: LoadOptions): LoadedSource;
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadOptions {
|
||||
wasi?: WASIInstance;
|
||||
overwriteImports?: (importObject: WebAssembly.Imports) => WebAssembly.Imports;
|
||||
beforeInit?: (source: WebAssembly.WebAssemblyInstantiatedSource) => void;
|
||||
getMemory?: (exports: WebAssembly.Exports) => WebAssembly.Memory;
|
||||
getTable?: (exports: WebAssembly.Exports) => WebAssembly.Table;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface LoadPayload {
|
||||
wasmModule: WebAssembly.Module;
|
||||
wasmMemory: WebAssembly.Memory;
|
||||
sab?: Int32Array;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadBaseOptions extends BaseOptions {
|
||||
waitThreadStart?: boolean | number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type MainThreadOptions = MainThreadOptionsWithThreadManager | MainThreadOptionsCreateThreadManager;
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadOptionsCreateThreadManager extends MainThreadBaseOptions, ThreadManagerOptionsMain {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MainThreadOptionsWithThreadManager extends MainThreadBaseOptions {
|
||||
threadManager?: ThreadManager | (() => ThreadManager);
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MessageEventData<T extends CommandType> {
|
||||
__emnapi__: CommandInfo<T>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class MessageHandler extends ThreadMessageHandler {
|
||||
napiModule: NapiModule | undefined;
|
||||
constructor(options: MessageHandlerOptions);
|
||||
instantiate(data: LoadPayload): InstantiatedSource | PromiseLike<InstantiatedSource>;
|
||||
handle(e: WorkerMessageEvent): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface MessageHandlerOptions extends ThreadMessageHandlerOptions {
|
||||
onLoad: (data: LoadPayload) => InstantiatedSource | PromiseLike<InstantiatedSource>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface NapiModule {
|
||||
imports: {
|
||||
env: any
|
||||
napi: any
|
||||
emnapi: any
|
||||
}
|
||||
exports: any
|
||||
loaded: boolean
|
||||
filename: string
|
||||
childThread: boolean
|
||||
emnapi: {
|
||||
syncMemory<T extends ArrayBuffer | ArrayBufferView> (
|
||||
js_to_wasm: boolean,
|
||||
arrayBufferOrView: T,
|
||||
offset?: number,
|
||||
len?: number
|
||||
): T
|
||||
getMemoryAddress (arrayBufferOrView: ArrayBuffer | ArrayBufferView): PointerInfo
|
||||
addSendListener (worker: any): boolean
|
||||
}
|
||||
|
||||
init (options: InitOptions): any
|
||||
initWorker (arg: number, func: [number, number]): void
|
||||
postMessage?: (msg: any) => any
|
||||
|
||||
waitThreadStart: boolean | number
|
||||
/* Excluded from this release type: PThread */}
|
||||
|
||||
/** @public */
|
||||
export declare interface NodeBinding {
|
||||
node: {
|
||||
emitAsyncInit: Function
|
||||
emitAsyncDestroy: Function
|
||||
makeCallback: Function
|
||||
}
|
||||
napi: {
|
||||
asyncInit: Function
|
||||
asyncDestroy: Function
|
||||
makeCallback: Function
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface PointerInfo {
|
||||
address: number
|
||||
ownership: ReferenceOwnership
|
||||
runtimeAllocated: 0 | 1
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ReuseWorkerOptions {
|
||||
/**
|
||||
* @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size | PTHREAD_POOL_SIZE}
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size-strict | PTHREAD_POOL_SIZE_STRICT}
|
||||
*/
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface SpawnThreadPayload {
|
||||
startArg: number;
|
||||
errorOrTid: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface StartPayload {
|
||||
tid: number;
|
||||
arg: number;
|
||||
sab?: Int32Array;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface StartResult {
|
||||
exitCode: number;
|
||||
instance: WebAssembly.Instance;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface TerminateAllThreadsPayload {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class ThreadManager {
|
||||
unusedWorkers: WorkerLike[];
|
||||
runningWorkers: WorkerLike[];
|
||||
pthreads: Record<number, WorkerLike>;
|
||||
get nextWorkerID(): number;
|
||||
wasmModule: WebAssembly.Module | null;
|
||||
wasmMemory: WebAssembly.Memory | null;
|
||||
private readonly messageEvents;
|
||||
private readonly _childThread;
|
||||
private readonly _onCreateWorker;
|
||||
private readonly _reuseWorker;
|
||||
private readonly _beforeLoad?;
|
||||
/* Excluded from this release type: printErr */
|
||||
threadSpawn?: ((startArg: number, errorOrTid?: number) => number);
|
||||
constructor(options: ThreadManagerOptions);
|
||||
init(): void;
|
||||
initMainThread(): void;
|
||||
private preparePool;
|
||||
shouldPreloadWorkers(): boolean;
|
||||
loadWasmModuleToAllWorkers(): Promise<WorkerLike[]>;
|
||||
preloadWorkers(): Promise<WorkerLike[]>;
|
||||
setup(wasmModule: WebAssembly.Module, wasmMemory: WebAssembly.Memory): void;
|
||||
markId(worker: WorkerLike): number;
|
||||
returnWorkerToPool(worker: WorkerLike): void;
|
||||
loadWasmModuleToWorker(worker: WorkerLike, sab?: Int32Array): Promise<WorkerLike>;
|
||||
allocateUnusedWorker(): WorkerLike;
|
||||
getNewWorker(sab?: Int32Array): WorkerLike | undefined;
|
||||
cleanThread(worker: WorkerLike, tid: number, force?: boolean): void;
|
||||
terminateWorker(worker: WorkerLike): void;
|
||||
terminateAllThreads(): void;
|
||||
addMessageEventListener(worker: WorkerLike, onMessage: (e: WorkerMessageEvent) => void): () => void;
|
||||
fireMessageEvent(worker: WorkerLike, e: WorkerMessageEvent): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type ThreadManagerOptions = ThreadManagerOptionsMain | ThreadManagerOptionsChild;
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsBase {
|
||||
printErr?: (message: string) => void;
|
||||
threadSpawn?: (startArg: number, errorOrTid?: number) => number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsChild extends ThreadManagerOptionsBase {
|
||||
childThread: true;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadManagerOptionsMain extends ThreadManagerOptionsBase {
|
||||
beforeLoad?: (worker: WorkerLike) => any;
|
||||
reuseWorker?: boolean | number | ReuseWorkerOptions;
|
||||
onCreateWorker: WorkerFactory;
|
||||
childThread?: false;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class ThreadMessageHandler {
|
||||
protected instance: WebAssembly.Instance | undefined;
|
||||
private messagesBeforeLoad;
|
||||
protected postMessage: (message: any) => void;
|
||||
protected onLoad?: (data: LoadPayload) => WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
protected onError: (error: Error, type: WorkerMessageType) => void;
|
||||
constructor(options?: ThreadMessageHandlerOptions);
|
||||
/** @virtual */
|
||||
instantiate(data: LoadPayload): WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
/** @virtual */
|
||||
handle(e: WorkerMessageEvent<MessageEventData<WorkerMessageType>>): void;
|
||||
private _load;
|
||||
private _start;
|
||||
protected _loaded(err: Error | null, source: WebAssembly.WebAssemblyInstantiatedSource | null, payload: LoadPayload): void;
|
||||
protected handleAfterLoad<E extends WorkerMessageEvent>(e: E, f: (e: E) => void): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface ThreadMessageHandlerOptions {
|
||||
onLoad?: (data: LoadPayload) => WebAssembly.WebAssemblyInstantiatedSource | PromiseLike<WebAssembly.WebAssemblyInstantiatedSource>;
|
||||
onError?: (error: Error, type: WorkerMessageType) => void;
|
||||
postMessage?: (message: any) => void;
|
||||
}
|
||||
|
||||
export declare const version: string;
|
||||
|
||||
/** @public */
|
||||
export declare interface WASIInstance {
|
||||
readonly wasiImport?: Record<string, any>;
|
||||
initialize(instance: object): void;
|
||||
start(instance: object): number;
|
||||
getImportObject?(): any;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare class WASIThreads {
|
||||
PThread: ThreadManager | undefined;
|
||||
private wasmMemory;
|
||||
private wasmInstance;
|
||||
private readonly threadSpawn;
|
||||
readonly childThread: boolean;
|
||||
private readonly postMessage;
|
||||
readonly wasi: WASIInstance;
|
||||
constructor(options: WASIThreadsOptions);
|
||||
getImportObject(): {
|
||||
wasi: WASIThreadsImports;
|
||||
};
|
||||
setup(wasmInstance: WebAssembly.Instance, wasmModule: WebAssembly.Module, wasmMemory?: WebAssembly.Memory): void;
|
||||
preloadWorkers(): Promise<WorkerLike[]>;
|
||||
/**
|
||||
* It's ok to call this method to a WASI command module.
|
||||
*
|
||||
* in child thread, must call this method instead of {@link WASIThreads.start} even if it's a WASI command module
|
||||
*
|
||||
* @returns A proxied WebAssembly instance if in child thread, other wise the original instance
|
||||
*/
|
||||
initialize(instance: WebAssembly.Instance, module: WebAssembly.Module, memory?: WebAssembly.Memory): WebAssembly.Instance;
|
||||
/**
|
||||
* Equivalent to calling {@link WASIThreads.initialize} and then calling {@link WASIInstance.start}
|
||||
* ```js
|
||||
* this.initialize(instance, module, memory)
|
||||
* this.wasi.start(instance)
|
||||
* ```
|
||||
*/
|
||||
start(instance: WebAssembly.Instance, module: WebAssembly.Module, memory?: WebAssembly.Memory): StartResult;
|
||||
terminateAllThreads(): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare interface WASIThreadsImports {
|
||||
'thread-spawn': (startArg: number, errorOrTid?: number) => number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type WASIThreadsOptions = MainThreadOptions | ChildThreadOptions;
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerFactory = (ctx: {
|
||||
type: string;
|
||||
name: string;
|
||||
}) => WorkerLike;
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerLike = (Worker | Worker_2) & {
|
||||
whenLoaded?: Promise<WorkerLike>;
|
||||
loaded?: boolean;
|
||||
__emnapi_tid?: number;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export declare interface WorkerMessageEvent<T = any> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export declare type WorkerMessageType = 'load' | 'start';
|
||||
|
||||
export { }
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+7386
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user