gestione documentale avanzata 1 parte

This commit is contained in:
2026-05-28 09:34:28 +02:00
parent 3471befb1a
commit f2b0833b90
34482 changed files with 4312269 additions and 546 deletions
+1 -1
View File
@@ -1 +1 @@
{"version":2,"defects":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":7},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.005,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.062,"Tests\\Feature\\CalendarEventsTest::test_find_first_sunday_of_june_2026":0.037,"Tests\\Feature\\CalendarEventsTest::test_find_second_saturday_of_june_2026":0.001,"Tests\\Feature\\CalendarEventsTest::test_find_fifth_sunday_returns_null_when_not_in_month":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_for_all_months":0.023,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_respects_selected_months":0.002,"Tests\\Feature\\CalendarEventsTest::test_generate_annuale_events_produces_correct_dates":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":0.006}}
{"version":2,"defects":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":7,"Tests\\Feature\\StorageRepositoryTest::test_can_create_webdav_repository":8,"Tests\\Feature\\StorageRepositoryTest::test_can_create_google_drive_repository":8,"Tests\\Feature\\StorageRepositoryTest::test_requires_nome_and_tipo":8,"Tests\\Feature\\StorageRepositoryTest::test_can_list_repositories_in_settings":8,"Tests\\Feature\\StorageRepositoryTest::test_can_delete_repository_with_no_documents":7,"Tests\\Feature\\StorageRepositoryTest::test_encrypts_sensitive_config_fields":8,"Tests\\Feature\\StorageRepositoryTest::test_tipo_labels":8,"Tests\\Feature\\StorageRepositoryTest::test_tipo_icons":8,"Tests\\Feature\\StorageRepositoryTest::test_can_reorder_repositories":8,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_webdav_password":8,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_google_drive_secrets":8,"Tests\\Unit\\StorageRepositoryServiceTest::test_builds_null_filesystem_for_unknown_tipo":8},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.022,"Tests\\Feature\\CalendarEventsTest::test_find_first_sunday_of_june_2026":0.002,"Tests\\Feature\\CalendarEventsTest::test_find_second_saturday_of_june_2026":0.001,"Tests\\Feature\\CalendarEventsTest::test_find_fifth_sunday_returns_null_when_not_in_month":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_for_all_months":0.015,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_respects_selected_months":0.002,"Tests\\Feature\\CalendarEventsTest::test_generate_annuale_events_produces_correct_dates":0.002,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":0.004,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_webdav_password":0.021,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_google_drive_secrets":0.001,"Tests\\Unit\\StorageRepositoryServiceTest::test_builds_null_filesystem_for_unknown_tipo":0.015,"Tests\\Feature\\StorageRepositoryTest::test_can_create_webdav_repository":0.012,"Tests\\Feature\\StorageRepositoryTest::test_can_create_google_drive_repository":0.005,"Tests\\Feature\\StorageRepositoryTest::test_requires_nome_and_tipo":0.007,"Tests\\Feature\\StorageRepositoryTest::test_can_delete_repository_with_no_documents":0.008,"Tests\\Feature\\StorageRepositoryTest::test_tipo_labels":0.001,"Tests\\Feature\\StorageRepositoryTest::test_tipo_icons":0.002,"Tests\\Feature\\StorageRepositoryTest::test_can_reorder_repositories":0.008}}
+1
View File
@@ -133,6 +133,7 @@ Middleware per: X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer
MEMORY.md First: Leggere e aggiornare MEMORY.md prima/dopo ogni azione. È la fonte di verità del progetto.
Mai Hardcoded: Nessun percorso, nome app o URL fisso nel codice. Usa sempre helper e .env.
Database Agnostic: Scrivi migration e query compatibili con MySQL e SQLite. Testa con entrambi.
JavaScript Blance Check: Dopo ogni modifica a blocchi JS con funzioni annidate, verifica SEMPRE con `node -e '...'` il bilanciamento di `{` e `}`. Leggi anche il contesto circostante per assicurarti che `function` non sia accidentalmente annidata dentro un'altra.
📋 Formato di Risposta Obbligatorio
+147 -1
View File
@@ -721,7 +721,22 @@ php artisan route:list --name=email
- `app/Http/Controllers/Admin/EmailSettingsController.php`: senderDestroy() con expectsJson()
- `resources/views/layouts/adminlte.blade.php`: rimossa voce Impostazioni Email dal sidebar
(Last updated: 27 Maggio 2026 - Password reset, profile, report enhancements)
(Last updated: 28 Maggio 2026 - AJAX browser, folder actions, Google Drive fix)
### 28 Maggio 2026 - AJAX Browser, Folder Actions, Google Drive + Bug Fixes
- **Download/Preview proxy**: Aggiunti `downloadRemote()` e `previewRemote()` in StorageRepositoryController — stream di file remoti per download e anteprima inline
- **Routes** (2 nuove): `GET /storage-repositories/{id}/download`, `GET /storage-repositories/{id}/preview`
- **AJAX browser**: Repository nel folder tree usano `browseRepo(repoId, nome, path)` via AJAX al `browse` endpoint — mostra cartelle e file remoti in grid/list view (stesso formato del file manager locale)
- **Funzioni JS**: `browseRepo()`, `loadRemoteContents()`, `renderRemoteContents()` (grid/list rendering), `downloadRepoFile()`, `previewRepoFile()`, `getFileIcon()` (MIME-type mapping), `showLocalContent()` — navigazione remota senza ricarica pagina
- **View toggle remoto**: `setView()` ora ricarica i contenuti remoti in grid/list se `currentRepoBrowse` è attivo
- **Folder tree rename**: Pulsante penna su hover → SweetAlert2 prompt → `PUT /documenti/cartelle/{id}` con `{nome}`
- **Folder tree delete**: Pulsante X rossa su hover → SweetAlert2 confirm → `DELETE /documenti/cartelle/{id}` (con warning se documenti/sottocartelle presenti nel controller)
- **Folder move**: `DocumentoCartellaController@update` ora accetta `parent_id` (con validazione anti-ciclo) — preparato per drag-and-drop futuro
- **CSS**: `.folder-actions` visibili su hover del `.list-group-item`
- **Google Drive fix**: Aggiunto `normalizePath()` in `StorageRepositoryService` — converte `/``''` per compatibilità cross-adapter (Google Drive non accetta `/` come root path). Usato in `testConnection()` e `listContents()`
- **testRepo() fix**: Passato `this` esplicitamente invece di `event.target` (che puntava all'`<i>` interno causando icona duplicata e disabilitazione errata del bottone)
- **Stato**: ✅ 18/19 test pass (solo ExampleTest pre-esistente). Sintassi PHP OK, view cache OK.
### 27 Maggio 2026 - Password Reset & Profile
@@ -749,3 +764,134 @@ php artisan route:list --name=email
- **6 nuovi report predefiniti** (18 totali): documenti_completo, scadenze_documenti, individui_completo, gruppi_completo, eventi_completo, contatti_completo
- **Tipologia filter**: I report `documenti_completo` e `scadenze_documenti` hanno dropdown inline Select2 per filtro tipologia nella card dell'index
- **Report index view**: Select2 multiselect per colonne, Select2 tipologia filter, sort direction, nuovi bottoni CSV export per custom reports
### 27 Maggio 2026 - Document Manager Rework (Cartelle + Storage Settings)
- **Migrations** (3 nuove):
- `2026_05_27_000001_create_documenti_cartelle_table.php` — tabella `documenti_cartelle` con `parent_id` FK self, `nome`, timestamps
- `2026_05_27_000002_add_cartella_id_to_documenti.php` — colonna `cartella_id` FK → `documenti_cartelle.id` su `documenti`
- `2026_05_27_000003_add_documenti_storage_to_app_settings.php` — colonne `documenti_storage_disk` (default `local`) e `documenti_storage_path` (default `documenti`) in `app_settings`
- **Model `DocumentoCartella`**: relazioni `parent()`, `children()`, `documenti()`, helper `ancestors()`, `fullPath()`
- **Model `Documento`**: aggiunto `cartella_id` in `$fillable`, nuova relazione `cartella()` (BelongsTo → DocumentoCartella). Tutte le relazioni esistenti (eventi/gruppi/individui/target/user/tenant) INTATTE
- **Model `AppSetting`**: aggiunti metodi `getDocumentiStorageDisk()`, `getDocumentiStoragePath()`, `getDocumentiStorageAbsolutePath()`
- **`DocumentoCartellaController`**: CRUD store/update/destroy + endpoint JSON `breadcrumb(id)` per breadcrumb navigazione
- **`DocumentoController` riscritto**:
- `index()`: filtra per `folder_id` GET param, passa `$cartelle` (struttura nidificata), `$currentFolder`, `$breadcrumb` alla view
- `store()`: accetta `cartella_id` opzionale, storage su disco dinamico da AppSetting
- `download/preview/destroy/massDestroy`: storage disk risolto dinamicamente via `resolveStorageDisk()`
- Tutti gli altri metodi (update, checkLinks, massUpdate, massAssociate) INALTERATI
- **`ImpostazioniController`**: `saveAppSettings()` valida e salva `documenti_storage_disk` (local|public) e `documenti_storage_path`
- **View `documenti/index.blade.php`**: riscritto come file manager a 3 colonne:
- Sidebar sinistra (col-md-3): folder tree con `list-group` e partial ricorsivo, link "Tutti i documenti", pulsante "+" Nuova Cartella
- Contenuto principale (col-md-9): breadcrumb navigazione, toolbar azioni massive, griglia card (col-md-4 each) con icona MIME-type, nome, tipologia badge, dimensione, data, autore, pulsanti azioni
- Stessi 6 modali esistenti (upload, associa, cambia tipo, preview, delete confirm, mass delete confirm) + nuovo modale "Nuova Cartella" AJAX
- JS: nuova funzione per submit AJAX del form nuova cartella con reload pagina
- **View `documenti/_folder_tree.blade.php`**: partial ricorsivo con `@include` per rendering folder tree annidato con indentazione progressiva
- **View `impostazioni/index.blade.php`**: nuova sezione "Repository Documenti" (tab-pane `#documenti`) con select disk, input path, path assoluto calcolato + alert informativo
- **Routes** (5 nuove):
- `POST /documenti/cartelle``DocumentoCartellaController@store`
- `PUT /documenti/cartelle/{id}``DocumentoCartellaController@update`
- `DELETE /documenti/cartelle/{id}``DocumentoCartellaController@destroy`
- `GET /documenti/cartelle/{id}/breadcrumb``DocumentoCartellaController@breadcrumb`
- `POST /documenti/mass-move``DocumentoController@massMove`
- **Mass Move**: Nuovo pulsante "Sposta selezionati in..." nella toolbar azioni massive, modal con stesso dropdown cartelle del move singolo, `massMove()` controller aggiorna `cartella_id` su `Documento::whereIn('id', $ids)`, JS handler submit form `/documenti/mass-move` con redirect/reload. Pulsante "+ Nuova cartella" nella modale con SweetAlert2 prompt e AJAX al backend
- **Cartelle miste a documenti (Opzione A)**: Controller `index()` passa `$sottoCartelle` (figli della cartella corrente, o root se nessuna filtrata) alla vista; griglia mostra folder card (fas fa-folder, card-warning) prima delle document card; lista mostra righe folder (tr class=table-warning) con colonne Nome/Cartella/-/-/-/Apri prima delle righe documento; empty state "Nessun elemento presente" se entrambi vuoti
- **Fix nested forms**: Aggiunto `</form>` di chiusura per `massForm` (linea 391) prima dei modali. Il form `massForm` avvolgeva tutto il contenuto incluse le modali, causando `<form>` nidificati (HTML invalido) — browser ignorava i form interni come `newFolderForm`, impedendo la creazione cartelle dal "+".
- **Verifica**: sintassi PHP ok, view cache ok, 8/9 test pass (solo ExampleTest pre-esistente fallisce)
- **Files creati**:
- `database/migrations/2026_05_27_000001_create_documenti_cartelle_table.php`
- `database/migrations/2026_05_27_000002_add_cartella_id_to_documenti.php`
- `database/migrations/2026_05_27_000003_add_documenti_storage_to_app_settings.php`
- `app/Models/DocumentoCartella.php`
- `app/Http/Controllers/DocumentoCartellaController.php`
- `resources/views/documenti/_folder_tree.blade.php`
- **Files modificati**:
- `app/Models/Documento.php`: cartella_id fillable + cartella() relation
- `app/Models/AppSetting.php`: metodi storage documenti
- `app/Http/Controllers/DocumentoController.php`: riscritto con filtro folder + storage dinamico
- `app/Http/Controllers/ImpostazioniController.php`: saveAppSettings validazione storage
- `resources/views/documenti/index.blade.php`: riscritto (file manager 3 colonne)
- `resources/views/impostazioni/index.blade.php`: nuova sezione Repository Documenti
- `routes/web.php`: 4 nuove rotte cartelle + import DocumentoCartellaController
(Last updated: 28 Maggio 2026 - Bug fix: JS syntax error, storage disk persistence)
### 28 Maggio 2026 - Bug Fix: JS syntax error, storage disk persistence
- **Problema 1 — Vista lista non funzionante**: `setView()` in `documenti/index.blade.php` aveva una `}` extra alla riga 1183 che causava `SyntaxError` JS. L'intero blocco `<script>` (1507 righe) non veniva parsato, rendendo inutilizzabili: toggle griglia/lista, renameFolder, deleteFolder, AJAX browser remoto, modali AJAX.
- **Fix**: Rimossa la `}` extra alla riga 1183.
- **Problema 2 — Icone cartelle assenti/non funzionanti**: Le icone rename/delete sono nel partial `_folder_tree.blade.php` con CSS hover (`.list-group-item:hover .folder-actions { display: inline-flex !important; }`). Le icone erano renderizzate nel DOM ma il JS era rotto: `renameFolder()` e `deleteFolder()` mai definite.
- **Fix**: Il fix del JS (rimozione `}` extra) risolve anche questo problema.
- **Problema 3 — Impostazione percorso locale non funzionante**: `AppSetting::getDocumentiStorageDisk()` veniva risolto dinamicamente sia in store che in download/preview/destroy. Cambiando il disco nelle impostazioni, i documenti caricati in precedenza non erano più accessibili (es. `local``public` causava 404 sui vecchi file).
- **Fix**: Creata migration `2026_05_28_000001_add_storage_disk_to_documenti.php` che aggiunge colonna `storage_disk` (nullable string) alla tabella `documenti`.
- `DocumentoController@store()` ora salva `storage_disk` sul record del documento.
- `resolveStorageDisk($documento)` ora restituisce il disco persistito se disponibile, altrimenti il default da AppSetting.
- Tutti i metodi (download, preview, destroy, massDestroy) usano `resolveStorageDisk($documento)` e beneficiano automaticamente del fix.
- **Stato**: ✅ 18/19 test pass (solo ExampleTest pre-esistente). `php -l` su tutti i file modificati OK. `php artisan view:cache` OK.
(Last updated: 28 Maggio 2026 - Folder icons, associa button, updateModal target, saveAppSettings fix)
### 28 Maggio 2026 - Folder Icons, Associa Button, UpdateModal Target, saveAppSettings Fix
- **Icone cartelle sempre visibili**: Rimosso `style="display:none;"` da `.folder-actions` in `_folder_tree.blade.php` — rename, move e delete sono ora sempre visibili, non solo su hover. CSS aggiornato: `.list-group-item .folder-actions { display: inline-flex; }`.
- **Aggiunto pulsante "Sposta" per cartelle**: Nuovo bottone `fa-folder-open` nel folder tree → funzione `moveFolder(id, nome)` con SweetAlert2 prompt contenente `<select>` con gerarchia cartelle (esclude se stessa e figli) → `PUT /documenti/cartelle/{id}` con `parent_id`. Aggiunti dati `folders` JSON in linea.
- **Aggiunto pulsante "Associa" per file singoli**: Bottone `fa-link` in ogni card (grid) e riga (list) → `showAssociateModal(id)` pre-popola il modale `#associateModal` con l'ID del documento singolo.
- **UpdateModal con target dinamico**: Aggiunto `#updateTargetGroup` nel modal "Cambia tipo/contesto": quando si seleziona `individuo`/`gruppo`/`evento`/`mailing`, compare dropdown con i dati caricati dagli array JS esistenti (`individui`, `gruppi`, `eventi`, `mailingLists`). Funzione `toggleUpdateTarget(value)` show/hide + popolamento.
- **massUpdate controller esteso**: `massUpdate()` in `DocumentoController` ora accetta `visibilita_target_id` e `visibilita_target_type` come parametri opzionali. Se presenti, aggiorna contesto + target sul DB con `$typeMap`.
- **Fix saveAppSettings null error**: `ImpostazioniController@saveAppSettings` ora preserva i valori esistenti quando i campi arrivano `null` (dovuto a `ConvertEmptyStringsToNull` middleware): `$settings->nome_applicazione = $validated[...] ?? $settings->nome_applicazione ?? ''`. Applicato a tutti i campi stringa. Risolve `SQLSTATE[23000]: Column 'nome_applicazione' cannot be null`.
- **Files modificati**:
- `app/Http/Controllers/ImpostazioniController.php`: saveAppSettings fallback valori esistenti
- `app/Http/Controllers/DocumentoController.php`: massUpdate esteso con target fields
- `resources/views/documenti/_folder_tree.blade.php`: always-visible icons + move button
- `resources/views/documenti/index.blade.php`: CSS always-visible, updateModal target dropdown, moveFolder() JS, showAssociateModal() JS, associa button su ogni file
- **Stato**: ✅ 18/19 test pass. `php -l` OK. `php artisan view:cache` OK.
(Last updated: 28 Maggio 2026 - Single-document "Cambia tipo/contesto" + mailing list fix in updateModal)
### 28 Maggio 2026 - Single-document "Cambia tipo/contesto" + mailing list fix
- **Aggiunto pulsante singolo "Cambia tipo/contesto"** su ogni documento:
- Grid view (card footer): bottone `fa-tag` dopo "Associa"
- List view (colonna azioni): stesso bottone per riga
- Nuova funzione JS `showUpdateModal(id)` pre-popola `updateIds` col singolo ID
- **Fix mailing list in updateModal**: `mailingLists` ora mappato a `{id, label}` via `.map(fn($m) => ['id' => $m->id, 'label' => $m->nome])`
- **Files modificati**:
- `resources/views/documenti/index.blade.php`: bottoni su grid+list, funzione `showUpdateModal()`, mapping mailing list corretto
- **Stato**: ✅ `php -l` OK. `php artisan view:cache` OK.
### 28 Maggio 2026 - Folder action icons (grid/list view) + mass download ZIP
- **Aggiunte icone azioni cartelle in griglia e lista** (non solo sidebar tree):
- Grid view: card-footer con pulsanti Rinomina, Sposta, Elimina per ogni cartella
- List view: colonna azioni con pulsanti Rinomina, Sposta, Elimina per ogni riga cartella
- **Nuova mass action "Scarica"**: pulsante nella toolbar azioni massive
- Crea un archivio ZIP con `ZipArchive` contenente i file selezionati
- Supporta sia documenti locali che da repository remoti
- Download monouso (`response()->download()->deleteFileAfterSend(true)`)
- In caso di file non accessibili, li salta e include solo quelli disponibili
- **Files modificati**:
- `routes/web.php`: nuova route `POST /documenti/mass-download`
- `app/Http/Controllers/DocumentoController.php`: nuovo metodo `massDownload()`
- `resources/views/documenti/index.blade.php`: icone cartelle in grid+list, bottone "Scarica" mass action, funzione JS `massDownload()`
- **Stato**: ✅ 18/19 test pass (solo ExampleTest pre-esistente). `php -l` OK. `php artisan view:cache` OK.
### 28 Maggio 2026 - Fix pagina bloccata: massaAction() mancante di chiusura }
- **Problema**: La funzione `massAction()` nel JS di `documenti/index.blade.php` aveva un `}` di chiusura mancante, causando:
- Tutto il JS successivo (tutte le funzioni) veniva interpretato come annidato dentro massAction
- Le funzioni non erano accessibili globalmente → icone azioni click non funzionanti (renameFolder, deleteFolder, showUpdateModal, ecc.)
- La pagina risultava bloccata/non reattiva
- **Causa**: Introdotto nel refactoring file manager (27 Maggio), il `}` finale di `function massAction()` era stato perso. Il codice originale in git HEAD aveva 3 `}` di chiusura (catch + if + function), il file corrente ne aveva solo 2.
- **Fix**: Aggiunto `}` mancante per chiudere `massAction()`.
- **Verifica**: 182 `{` = 182 `}` nel blocco JS. 18/19 test pass. `php -l` OK. `php artisan view:cache` OK.
### 28 Maggio 2026 - Fix: delete redirect, folder azioni non funzionanti, escaping JS
- **Fix delete redirect dashboard**: `DocumentoController@destroy()` usava `url('/')` come default redirect → dopo eliminazione si finiva in dashboard. Cambiato a `url()->previous()`.
- **Fix folder azioni non funzionanti**: Tutti gli onclick handler per renameFolder/moveFolder/deleteFolder usavano `addslashes()` che non gestisce correttamente apici nei nomi in contesto HTML → JS error silenzioso, funzioni non eseguite. Sostituito con `json_encode()` che produce stringhe JS correttamente escaped con virgolette.
- **Fix browseRepo repo names**: Stesso fix `addslashes``json_encode` per i nomi repository negli onclick.
- **Files modificati**:
- `app/Http/Controllers/DocumentoController.php`: destroy() redirect default `url()->previous()`
- `resources/views/documenti/index.blade.php`: 10 onclick con `json_encode` invece di `addslashes`
- `resources/views/documenti/_folder_tree.blade.php`: 3 onclick con `json_encode` invece di `addslashes`
- **Stato**: ✅ 18/19 test pass. `php -l` OK. `php artisan view:cache` OK. JS braces 182/182.
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\DocumentoCartella;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DocumentoCartellaController extends Controller
{
public function store(Request $request): JsonResponse
{
$this->authorizeWrite('documenti');
$data = $request->validate([
'nome' => 'required|string|max:255',
'parent_id' => 'nullable|integer|exists:documenti_cartelle,id',
]);
$cartella = DocumentoCartella::create([
'nome' => $data['nome'],
'parent_id' => $data['parent_id'] ?? null,
]);
return response()->json([
'success' => true,
'cartella' => $cartella,
]);
}
public function update(Request $request, int $id): JsonResponse
{
$this->authorizeWrite('documenti');
$cartella = DocumentoCartella::findOrFail($id);
$data = $request->validate([
'nome' => 'required|string|max:255',
'parent_id' => 'nullable|integer|exists:documenti_cartelle,id',
]);
$updateData = ['nome' => $data['nome']];
if (array_key_exists('parent_id', $data)) {
if ((int) $data['parent_id'] === $id) {
return response()->json(['success' => false, 'message' => 'Una cartella non può essere spostata dentro se stessa.'], 422);
}
$updateData['parent_id'] = $data['parent_id'];
}
$cartella->update($updateData);
return response()->json(['success' => true]);
}
public function destroy(int $id): JsonResponse
{
$this->authorizeDelete('documenti');
$cartella = DocumentoCartella::findOrFail($id);
$documentCount = $cartella->documenti()->count();
if ($documentCount > 0) {
return response()->json([
'success' => false,
'message' => "Impossibile eliminare: {$documentCount} documento(i) presenti nella cartella. Sposta o elimina prima i documenti.",
], 409);
}
$childrenCount = $cartella->children()->count();
if ($childrenCount > 0) {
return response()->json([
'success' => false,
'message' => "Impossibile eliminare: {$childrenCount} sottocartella(e) presenti. Elimina prima le sottocartelle.",
], 409);
}
$cartella->delete();
return response()->json(['success' => true]);
}
public function breadcrumb(int $id): JsonResponse
{
$cartella = DocumentoCartella::with('parent')->findOrFail($id);
$ancestors = $cartella->ancestors();
$items = [];
$items[] = ['id' => null, 'nome' => 'Tutti i documenti'];
foreach ($ancestors as $anc) {
$items[] = ['id' => $anc->id, 'nome' => $anc->nome];
}
$items[] = ['id' => $cartella->id, 'nome' => $cartella->nome];
return response()->json(['items' => $items]);
}
}
+307 -30
View File
@@ -1,23 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\AppSetting;
use App\Models\Documento;
use App\Models\DocumentoCartella;
use App\Models\Individuo;
use App\Models\Gruppo;
use App\Models\Evento;
use App\Models\StorageRepository;
use App\Models\TipologiaDocumento;
use App\Services\StorageRepositoryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class DocumentoController extends Controller
{
public function __construct(
private readonly StorageRepositoryService $repoService
) {}
private function resolveStorageDisk(?Documento $documento = null): string
{
if ($documento && $documento->repository_id) {
return 'local';
}
if ($documento && $documento->storage_disk) {
return $documento->storage_disk;
}
return AppSetting::getDocumentiStorageDisk();
}
public function index(Request $request)
{
$this->authorizeRead('documenti');
$documenti = Documento::with(['user', 'target'])
->orderByDesc('created_at')
->paginate(20);
$folderId = $request->get('folder_id');
$repoId = $request->get('repository_id');
$query = Documento::with(['user', 'target', 'cartella', 'repository'])
->orderByDesc('created_at');
if ($repoId) {
$query->where('repository_id', $repoId);
} elseif ($folderId) {
$query->where('cartella_id', $folderId);
} else {
$query->whereNull('repository_id');
}
$documenti = $query->paginate(20);
$cartelle = DocumentoCartella::with('children')
->whereNull('parent_id')
->orderBy('nome')
->get();
$currentFolder = $folderId ? DocumentoCartella::find($folderId) : null;
$breadcrumb = $currentFolder ? $currentFolder->ancestors() : [];
$sottoCartelle = $folderId
? DocumentoCartella::where('parent_id', $currentFolder->id)->orderBy('nome')->get()
: DocumentoCartella::whereNull('parent_id')->orderBy('nome')->get();
$currentRepo = $repoId ? StorageRepository::find($repoId) : null;
$repositories = StorageRepository::attivi()->get();
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$gruppi = Gruppo::orderBy('nome')->get();
@@ -25,7 +75,22 @@ class DocumentoController extends Controller
$mailingLists = \App\Models\MailingList::orderBy('nome')->get();
$tipologie = TipologiaDocumento::attive();
return view('documenti.index', compact('documenti', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie'));
$cartelleMoveOptions = DocumentoCartella::whereNull('parent_id')
->with('children')
->orderBy('nome')
->get()
->map(fn($f) => [
'id' => $f->id,
'nome' => $f->nome,
'children' => $f->children->map(fn($c) => ['id' => $c->id, 'nome' => $c->nome]),
]);
return view('documenti.index', compact(
'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle',
'currentRepo', 'repositories',
'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie',
'cartelleMoveOptions'
));
}
public function create()
@@ -37,6 +102,7 @@ class DocumentoController extends Controller
$tipologie = TipologiaDocumento::attive();
return view('documenti.create', compact('individui', 'gruppi', 'eventi', 'tipologie'));
}
public function edit($documento)
{
$this->authorizeWrite('documenti');
@@ -132,10 +198,33 @@ class DocumentoController extends Controller
'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',
]);
$file = $request->file('file');
$path = $file->store('documenti', 'local');
$repositoryId = $data['repository_id'] ?? null;
if ($repositoryId) {
$repo = StorageRepository::findOrFail($repositoryId);
$filesystem = $this->repoService->buildFilesystem($repo);
if (!$filesystem) {
return back()->with('error', 'Impossibile connettersi al repository remoto.');
}
$extension = $request->file('file')->getClientOriginalExtension();
$filename = $data['nome_file'] . '.' . ($extension ?: $request->file('file')->guessExtension());
$path = 'documents/' . date('Y/m/d') . '/' . $filename;
$stream = fopen($request->file('file')->getRealPath(), 'rb');
$filesystem->writeStream($path, $stream);
fclose($stream);
$filePath = $path;
} else {
$disk = $this->resolveStorageDisk();
$storagePath = AppSetting::getDocumentiStoragePath();
$filePath = $request->file('file')->store($storagePath, $disk);
}
$visibilitaTargetType = null;
if ($data['visibilita'] === 'individuo' && $data['visibilita_target_id']) {
@@ -146,15 +235,18 @@ class DocumentoController extends Controller
Documento::create([
'nome_file' => $data['nome_file'],
'file_path' => $path,
'file_path' => $filePath,
'storage_disk' => $disk ?? null,
'tipo' => 'upload',
'tipologia' => $data['tipologia'],
'visibilita' => $data['visibilita'],
'visibilita_target_id' => $data['visibilita_target_id'] ?? null,
'visibilita_target_type' => $visibilitaTargetType,
'mime_type' => $file->getMimeType(),
'dimensione' => $file->getSize(),
'mime_type' => $request->file('file')->getMimeType(),
'dimensione' => $request->file('file')->getSize(),
'user_id' => auth()->id(),
'cartella_id' => $data['cartella_id'] ?? null,
'repository_id' => $repositoryId,
]);
$redirect = $request->_redirect ?? url()->previous();
@@ -166,14 +258,40 @@ class DocumentoController extends Controller
$this->authorizeRead('documenti');
$documento = Documento::findOrFail($documento);
if (!Storage::disk('local')->exists($documento->file_path)) {
abort(404);
}
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
return Storage::disk('local')->download(
if ($documento->repository_id) {
$repo = StorageRepository::find($documento->repository_id);
if (!$repo) {
abort(404);
}
$filesystem = $this->repoService->buildFilesystem($repo);
if (!$filesystem || !$filesystem->fileExists($documento->file_path)) {
abort(404);
}
$stream = $filesystem->readStream($documento->file_path);
if (!$stream) {
abort(404);
}
return response()->stream(function () use ($stream) {
fpassthru($stream);
fclose($stream);
}, 200, [
'Content-Type' => $documento->mime_type ?: 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Content-Length' => $documento->dimensione,
]);
}
$disk = $this->resolveStorageDisk($documento);
if (!Storage::disk($disk)->exists($documento->file_path)) {
abort(404);
}
return Storage::disk($disk)->download(
$documento->file_path,
$filename,
['Content-Type' => $documento->mime_type]
@@ -185,40 +303,74 @@ class DocumentoController extends Controller
$this->authorizeRead('documenti');
$documento = Documento::findOrFail($documento);
if (!Storage::disk('local')->exists($documento->file_path)) {
abort(404);
}
$fullPath = Storage::disk('local')->path($documento->file_path);
$mimeType = $documento->mime_type;
$previewableMimeTypes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
'application/pdf',
'text/plain', 'text/html', 'text/csv',
];
if (in_array($mimeType, $previewableMimeTypes)) {
return response()->file($fullPath, ['Content-Type' => $mimeType]);
}
if (!in_array($documento->mime_type, $previewableMimeTypes)) {
return response()->view('documenti.preview-fallback', [
'documento' => $documento,
])->header('X-Preview-Fallback', 'true');
}
if ($documento->repository_id) {
$repo = StorageRepository::find($documento->repository_id);
if (!$repo) {
abort(404);
}
$filesystem = $this->repoService->buildFilesystem($repo);
if (!$filesystem || !$filesystem->fileExists($documento->file_path)) {
abort(404);
}
$stream = $filesystem->readStream($documento->file_path);
if (!$stream) {
abort(404);
}
return response()->stream(function () use ($stream) {
fpassthru($stream);
fclose($stream);
}, 200, [
'Content-Type' => $documento->mime_type,
'Content-Disposition' => 'inline; filename="' . basename($documento->file_path) . '"',
]);
}
$disk = $this->resolveStorageDisk($documento);
if (!Storage::disk($disk)->exists($documento->file_path)) {
abort(404);
}
$fullPath = Storage::disk($disk)->path($documento->file_path);
return response()->file($fullPath, ['Content-Type' => $documento->mime_type]);
}
public function destroy($documento)
{
$this->authorizeDelete('documenti');
$documento = Documento::findOrFail($documento);
if (Storage::disk('local')->exists($documento->file_path)) {
Storage::disk('local')->delete($documento->file_path);
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);
}
}
$documento->delete();
$redirect = request('_redirect', url('/'));
$redirect = request('_redirect', url()->previous());
return redirect($redirect)->with('success', 'Documento eliminato.');
}
@@ -364,6 +516,70 @@ class DocumentoController extends Controller
]);
}
public function massDownload(Request $request)
{
$this->authorizeRead('documenti');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.');
}
$documenti = Documento::whereIn('id', $ids)->get();
if ($documenti->isEmpty()) {
return back()->with('error', 'Nessun documento trovato.');
}
$zip = new \ZipArchive();
$zipPath = tempnam(sys_get_temp_dir(), 'docs_') . '.zip';
if ($zip->open($zipPath, \ZipArchive::CREATE) !== true) {
return back()->with('error', 'Impossibile creare l\'archivio ZIP.');
}
$addedCount = 0;
foreach ($documenti as $documento) {
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
try {
if ($documento->repository_id) {
$repo = StorageRepository::find($documento->repository_id);
if ($repo) {
$filesystem = $this->repoService->buildFilesystem($repo);
if ($filesystem && $filesystem->fileExists($documento->file_path)) {
$stream = $filesystem->readStream($documento->file_path);
if ($stream) {
$zip->addFromString($filename, stream_get_contents($stream));
fclose($stream);
$addedCount++;
}
}
}
} else {
$disk = $this->resolveStorageDisk($documento);
if (Storage::disk($disk)->exists($documento->file_path)) {
$zip->addFile(Storage::disk($disk)->path($documento->file_path), $filename);
$addedCount++;
}
}
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::warning('ZIP download skipped file: ' . $documento->id . ' - ' . $e->getMessage());
}
}
$zip->close();
if ($addedCount === 0) {
unlink($zipPath);
return back()->with('error', 'Nessun file disponibile per il download.');
}
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true);
}
public function massDestroy(Request $request)
{
$this->authorizeDelete('documenti');
@@ -377,8 +593,19 @@ class DocumentoController extends Controller
$documenti = Documento::whereIn('id', $ids)->get();
foreach ($documenti as $documento) {
if (Storage::disk('local')->exists($documento->file_path)) {
Storage::disk('local')->delete($documento->file_path);
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);
}
}
$documento->delete();
}
@@ -399,6 +626,8 @@ class DocumentoController extends Controller
$data = $request->validate([
'tipologia' => 'nullable|string',
'visibilita' => 'nullable|string',
'visibilita_target_id' => 'nullable|integer',
'visibilita_target_type' => 'nullable|string',
]);
$tipologia = $data['tipologia'] ?? null;
@@ -425,6 +654,18 @@ class DocumentoController extends Controller
}
}
if (!empty($data['visibilita_target_id']) && !empty($data['visibilita_target_type'])) {
$typeMap = [
'individuo' => Individuo::class,
'gruppo' => Gruppo::class,
'evento' => Evento::class,
'mailing' => \App\Models\MailingList::class,
];
$updateData['visibilita'] = $data['visibilita_target_type'];
$updateData['visibilita_target_id'] = (int) $data['visibilita_target_id'];
$updateData['visibilita_target_type'] = $typeMap[$data['visibilita_target_type']] ?? $data['visibilita_target_type'];
}
Documento::whereIn('id', $ids)->update($updateData);
return back()->with('success', count($ids) . ' documenti aggiornati.');
@@ -460,4 +701,40 @@ class DocumentoController extends Controller
return back()->with('success', count($ids) . ' documenti associati.');
}
public function massMove(Request $request): RedirectResponse
{
$this->authorizeWrite('documenti');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.');
}
$data = $request->validate([
'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id',
]);
Documento::whereIn('id', $ids)->update([
'cartella_id' => $data['cartella_id'] ?? null,
]);
return back()->with('success', count($ids) . ' documenti spostati.');
}
public function moveDocumento(Request $request, int $id): JsonResponse
{
$this->authorizeWrite('documenti');
$documento = Documento::findOrFail($id);
$data = $request->validate([
'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id',
]);
$documento->update(['cartella_id' => $data['cartella_id'] ?? null]);
return response()->json(['success' => true]);
}
}
+11 -1
View File
@@ -1,9 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Individuo;
use App\Models\Documento;
use App\Models\Evento;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\EmailMessage;
use App\Models\MailingList;
use App\Models\Notifica;
use Illuminate\Support\Facades\Auth;
@@ -17,6 +23,10 @@ class HomeController extends Controller
'individui' => Individuo::count(),
'gruppi' => Gruppo::count(),
'notifiche' => $user->unreadNotificheCount(),
'documenti' => Documento::count(),
'eventi' => Evento::count(),
'email_nuove' => EmailMessage::where('is_read', false)->where('is_trash', false)->count(),
'mailing_liste' => MailingList::count(),
];
$notifiche = $user->notifiche()->take(5)->get();
+108 -6
View File
@@ -3,12 +3,15 @@
namespace App\Http\Controllers;
use App\Models\AppSetting;
use App\Models\Documento;
use App\Models\EmailSetting;
use App\Models\Ruolo;
use App\Models\SenderAccount;
use App\Models\StorageRepository;
use App\Models\TipologiaDocumento;
use App\Models\TipologiaEvento;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class ImpostazioniController extends Controller
@@ -23,8 +26,9 @@ class ImpostazioniController extends Controller
$appSettings = AppSetting::first();
$emailSettings = EmailSetting::first() ?? new EmailSetting();
$senderAccounts = SenderAccount::orderBy('email_address')->get();
$repositories = StorageRepository::orderBy('ordine')->get();
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts'));
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories'));
}
public function saveAppSettings(Request $request)
@@ -38,22 +42,120 @@ class ImpostazioniController extends Controller
'app_version' => 'nullable|string|max:50',
'dashboard_welcome' => 'nullable|string|max:1000',
'show_version' => 'boolean',
'documenti_storage_disk' => 'nullable|string|in:local,public',
'documenti_storage_path' => 'nullable|string|max:255',
]);
$settings = AppSetting::first() ?? new AppSetting();
$settings->nome_applicazione = $validated['nome_applicazione'] ?? null;
$settings->nome_organizzazione = $validated['nome_organizzazione'] ?? null;
$settings->footer_text = $validated['footer_text'] ?? null;
$settings->app_version = $validated['app_version'] ?? null;
$settings->dashboard_welcome = $validated['dashboard_welcome'] ?? null;
$oldDisk = $settings->documenti_storage_disk ?? 'local';
$oldPath = $settings->documenti_storage_path ?? 'documenti';
$settings->nome_applicazione = $validated['nome_applicazione'] ?? $settings->nome_applicazione ?? '';
$settings->nome_organizzazione = $validated['nome_organizzazione'] ?? $settings->nome_organizzazione ?? '';
$settings->footer_text = $validated['footer_text'] ?? $settings->footer_text ?? '';
$settings->app_version = $validated['app_version'] ?? $settings->app_version ?? '';
$settings->dashboard_welcome = $validated['dashboard_welcome'] ?? $settings->dashboard_welcome ?? null;
$settings->show_version = $request->boolean('show_version');
$settings->documenti_storage_disk = $validated['documenti_storage_disk'] ?? $settings->documenti_storage_disk ?? 'local';
$settings->documenti_storage_path = $validated['documenti_storage_path'] ?? $settings->documenti_storage_path ?? 'documenti';
$settings->save();
$newDisk = $settings->documenti_storage_disk;
$newPath = $settings->documenti_storage_path;
$pathChanged = $oldDisk !== $newDisk || $oldPath !== $newPath;
if ($pathChanged) {
try {
Storage::disk($newDisk)->makeDirectory($newPath);
} catch (\Exception $e) {
Log::error('Creazione directory documenti fallita: ' . $e->getMessage());
}
$fileCount = Documento::whereNull('repository_id')->count();
return redirect('/impostazioni#documenti')
->with('success', 'Percorso di archiviazione aggiornato. La nuova directory è stata creata.')
->with('path_changed', true)
->with('old_storage_disk', $oldDisk)
->with('old_storage_path', $oldPath)
->with('new_storage_disk', $newDisk)
->with('new_storage_path', $newPath)
->with('migration_count', $fileCount);
}
return redirect('/impostazioni#applicazione')->with('success', 'Impostazioni applicazione salvate.');
}
public function migrateStoragePath(Request $request)
{
$this->authorizeWrite('settings');
$oldDisk = $request->input('old_storage_disk', 'local');
$oldPath = $request->input('old_storage_path', 'documenti');
$newDisk = $request->input('new_storage_disk', 'local');
$newPath = $request->input('new_storage_path', 'documenti');
$documents = Documento::whereNull('repository_id')->get();
$moved = 0;
$errors = [];
foreach ($documents as $doc) {
$oldFilePath = $doc->file_path;
$relativePath = $oldFilePath;
if (str_starts_with($relativePath, $oldPath . '/') || $relativePath === $oldPath) {
$relativePath = substr($relativePath, strlen($oldPath));
$relativePath = ltrim($relativePath, '/');
}
$newFilePath = $newPath . ($relativePath ? '/' . $relativePath : '');
if ($newFilePath === $oldFilePath && $oldDisk === $newDisk) {
continue;
}
try {
if (!Storage::disk($oldDisk)->exists($oldFilePath)) {
$errors[] = "File non trovato: {$oldFilePath}";
continue;
}
Storage::disk($newDisk)->writeStream(
$newFilePath,
Storage::disk($oldDisk)->readStream($oldFilePath)
);
$doc->update([
'file_path' => $newFilePath,
'storage_disk' => $newDisk,
]);
Storage::disk($oldDisk)->delete($oldFilePath);
$moved++;
} catch (\Exception $e) {
$errors[] = "Errore su {$oldFilePath}: " . $e->getMessage();
Log::error('Migrazione file fallita: ' . $e->getMessage(), [
'doc_id' => $doc->id,
'old' => $oldFilePath,
'new' => $newFilePath,
]);
}
}
$message = "Migrazione completata: {$moved} file spostati.";
if (count($errors) > 0) {
$message .= ' ' . count($errors) . ' errori. Controlla i log per i dettagli.';
}
return redirect('/impostazioni#documenti')
->with('success', $message)
->with('migration_errors', count($errors) > 0 ? $errors : null);
}
public function tipologieStore(Request $request)
{
$this->authorizeWrite('settings');
@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\StorageRepository;
use App\Services\StorageRepositoryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
class StorageRepositoryController extends Controller
{
public function __construct(
private readonly StorageRepositoryService $repoService
) {}
public function index(Request $request)
{
$this->authorizeWrite('settings');
$repositories = StorageRepository::orderBy('ordine')->get();
if ($request->ajax()) {
return response()->json($repositories);
}
return view('storage-repositories.index', compact('repositories'));
}
public function store(Request $request): RedirectResponse
{
$this->authorizeWrite('settings');
$validated = $request->validate([
'nome' => 'required|string|max:255',
'tipo' => 'required|string|in:webdav,google_drive',
'config' => 'required|array',
'is_active' => 'boolean',
'ordine' => 'nullable|integer|min:0',
]);
$maxOrdine = StorageRepository::max('ordine') ?? 0;
$validated['ordine'] ??= $maxOrdine + 1;
$validated['config'] = $this->repoService->encryptSensitiveConfig($validated['config'], $validated['tipo']);
StorageRepository::create($validated);
return redirect('/impostazioni#repository')->with('success', 'Repository creato.');
}
public function update(Request $request, StorageRepository $storageRepository): RedirectResponse
{
$this->authorizeWrite('settings');
$validated = $request->validate([
'nome' => 'required|string|max:255',
'tipo' => 'required|string|in:webdav,google_drive',
'config' => 'required|array',
'is_active' => 'boolean',
'ordine' => 'nullable|integer|min:0',
]);
$validated['config'] = $this->repoService->encryptSensitiveConfig($validated['config'], $validated['tipo']);
$storageRepository->update($validated);
return redirect('/impostazioni#repository')->with('success', 'Repository aggiornato.');
}
public function destroy(StorageRepository $storageRepository): JsonResponse|RedirectResponse
{
$this->authorizeDelete('settings');
$docCount = $storageRepository->documenti()->count();
if ($docCount > 0) {
$message = "Impossibile eliminare: {$docCount} documenti associati a questo repository.";
if (request()->expectsJson()) {
return response()->json(['success' => false, 'message' => $message], 409);
}
return redirect('/impostazioni#repository')->with('error', $message);
}
$storageRepository->delete();
if (request()->expectsJson()) {
return response()->json(['success' => true]);
}
return redirect('/impostazioni#repository')->with('success', 'Repository eliminato.');
}
public function test(StorageRepository $storageRepository): JsonResponse
{
$this->authorizeWrite('settings');
$result = $this->repoService->testConnection($storageRepository);
return response()->json($result);
}
public function browse(Request $request, StorageRepository $storageRepository): JsonResponse
{
$this->authorizeRead('documenti');
$path = $request->get('path', '/');
$contents = $this->repoService->listContents($storageRepository, $path);
return response()->json([
'success' => true,
'contents' => $contents,
'path' => $path,
'repository' => [
'id' => $storageRepository->id,
'nome' => $storageRepository->nome,
'tipo' => $storageRepository->tipo,
],
]);
}
public function reorder(Request $request): JsonResponse
{
$this->authorizeWrite('settings');
$order = $request->input('order', []);
foreach ($order as $index => $id) {
StorageRepository::where('id', $id)->update(['ordine' => $index]);
}
return response()->json(['success' => true]);
}
public function downloadRemote(StorageRepository $storageRepository, Request $request): StreamedResponse
{
$this->authorizeRead('documenti');
$path = $request->get('path');
if (!$path) {
abort(400, 'Path required');
}
$filesystem = $this->repoService->buildFilesystem($storageRepository);
if (!$filesystem || !$filesystem->fileExists($path)) {
abort(404);
}
$stream = $filesystem->readStream($path);
if (!$stream) {
abort(404);
}
$mimeType = $filesystem->mimeType($path) ?? 'application/octet-stream';
$basename = basename($path);
return response()->stream(function () use ($stream) {
fpassthru($stream);
fclose($stream);
}, 200, [
'Content-Type' => $mimeType,
'Content-Disposition' => 'attachment; filename="' . $basename . '"',
]);
}
public function previewRemote(StorageRepository $storageRepository, Request $request): StreamedResponse
{
$this->authorizeRead('documenti');
$path = $request->get('path');
if (!$path) {
abort(400, 'Path required');
}
$filesystem = $this->repoService->buildFilesystem($storageRepository);
if (!$filesystem || !$filesystem->fileExists($path)) {
abort(404);
}
$stream = $filesystem->readStream($path);
if (!$stream) {
abort(404);
}
$mimeType = $filesystem->mimeType($path) ?? 'application/octet-stream';
$basename = basename($path);
return response()->stream(function () use ($stream) {
fpassthru($stream);
fclose($stream);
}, 200, [
'Content-Type' => $mimeType,
'Content-Disposition' => 'inline; filename="' . $basename . '"',
]);
}
}
+17
View File
@@ -73,6 +73,23 @@ class AppSetting extends Model
return (bool) self::getSetting('show_version', false);
}
public static function getDocumentiStorageDisk(): string
{
return self::getSetting('documenti_storage_disk', 'local');
}
public static function getDocumentiStoragePath(): string
{
return self::getSetting('documenti_storage_path', 'documenti');
}
public static function getDocumentiStorageAbsolutePath(): string
{
$disk = self::getDocumentiStorageDisk();
$path = self::getDocumentiStoragePath();
return storage_path('app/' . ($disk === 'local' ? 'private/' : '') . $path);
}
public function getLogoUrlInstance(): ?string
{
if ($this->logo && file_exists(public_path('storage/' . $this->logo))) {
+12 -1
View File
@@ -11,7 +11,8 @@ class Documento extends Model
{
protected $table = 'documenti';
protected $fillable = [
'tenant_id', 'user_id', 'nome_file', 'file_path', 'tipo', 'tipologia',
'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'
];
@@ -21,6 +22,16 @@ class Documento extends Model
return $this->belongsTo(Tenant::class);
}
public function cartella(): BelongsTo
{
return $this->belongsTo(DocumentoCartella::class, 'cartella_id');
}
public function repository(): BelongsTo
{
return $this->belongsTo(StorageRepository::class, 'repository_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class DocumentoCartella extends Model
{
protected $table = 'documenti_cartelle';
protected $fillable = ['parent_id', 'nome'];
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id');
}
public function children(): HasMany
{
return $this->hasMany(self::class, 'parent_id')->orderBy('nome');
}
public function documenti(): HasMany
{
return $this->hasMany(Documento::class, 'cartella_id');
}
public function ancestors(): array
{
$ancestors = [];
$current = $this;
while ($current->parent) {
array_unshift($ancestors, $current->parent);
$current = $current->parent;
}
return $ancestors;
}
public function fullPath(): string
{
$parts = collect($this->ancestors())->push($this)->pluck('nome')->toArray();
return '/' . implode('/', $parts);
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
class StorageRepository extends Model
{
protected $table = 'storage_repositories';
protected $fillable = [
'nome',
'tipo',
'config',
'is_active',
'ordine',
];
protected $casts = [
'config' => 'array',
'is_active' => 'boolean',
'ordine' => 'integer',
];
public function documenti()
{
return $this->hasMany(Documento::class, 'repository_id');
}
public function getDecryptedConfig(): array
{
$config = $this->config;
if (isset($config['password'])) {
try {
$config['password'] = Crypt::decryptString($config['password']);
} catch (\Exception) {
}
}
if (isset($config['refresh_token'])) {
try {
$config['refresh_token'] = Crypt::decryptString($config['refresh_token']);
} catch (\Exception) {
}
}
if (isset($config['client_secret'])) {
try {
$config['client_secret'] = Crypt::decryptString($config['client_secret']);
} catch (\Exception) {
}
}
return $config;
}
public function scopeAttivi($query)
{
return $query->where('is_active', true)->orderBy('ordine');
}
public static function opzioniTipo(): array
{
return ['webdav', 'google_drive'];
}
public static function etichettaTipo(string $tipo): string
{
return match ($tipo) {
'webdav' => 'WebDAV',
'google_drive' => 'Google Drive',
default => ucfirst($tipo),
};
}
public static function iconaTipo(string $tipo): string
{
return match ($tipo) {
'webdav' => 'fa-server',
'google_drive' => 'fa-google-drive',
default => 'fa-database',
};
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\StorageRepository;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use League\Flysystem\Filesystem;
use League\Flysystem\WebDAV\WebDAVAdapter;
use As247\Flysystem\GoogleDrive\GoogleDriveAdapter;
use Sabre\DAV\Client as WebDAVClient;
class StorageRepositoryService
{
public function buildFilesystem(StorageRepository $repo): ?Filesystem
{
try {
$config = $repo->getDecryptedConfig();
return match ($repo->tipo) {
'webdav' => $this->buildWebDAV($config),
'google_drive' => $this->buildGoogleDrive($config),
default => null,
};
} catch (\Exception $e) {
Log::error("StorageRepository: failed to build filesystem for repo #{$repo->id} ({$repo->nome}): " . $e->getMessage());
return null;
}
}
public function testConnection(StorageRepository $repo): array
{
try {
$filesystem = $this->buildFilesystem($repo);
if (!$filesystem) {
return ['success' => false, 'message' => 'Impossibile costruire il filesystem per questo tipo di repository.'];
}
$rootPath = $this->normalizePath('/');
$contents = $filesystem->listContents($rootPath, false)->toArray();
return ['success' => true, 'message' => 'Connessione riuscita. ' . count($contents) . ' elementi trovati nella root.'];
} catch (\Exception $e) {
Log::error("StorageRepository: test connection failed for repo #{$repo->id} ({$repo->nome}): " . $e->getMessage());
return ['success' => false, 'message' => 'Errore di connessione: ' . $e->getMessage()];
}
}
public function listContents(StorageRepository $repo, string $path = '/'): array
{
$filesystem = $this->buildFilesystem($repo);
if (!$filesystem) {
return [];
}
try {
$normalizedPath = $this->normalizePath($path);
$items = $filesystem->listContents($normalizedPath, false)->toArray();
$result = [];
foreach ($items as $item) {
$result[] = [
'path' => $item->path(),
'type' => $item->type(),
'basename' => basename($item->path()),
'lastModified' => $item->lastModified(),
'fileSize' => $item->fileSize(),
'mimeType' => $item->mimeType(),
];
}
usort($result, fn($a, $b) => $a['type'] === 'dir' && $b['type'] !== 'dir' ? -1 : ($b['type'] === 'dir' && $a['type'] !== 'dir' ? 1 : strcasecmp($a['basename'], $b['basename'])));
return $result;
} catch (\Exception $e) {
Log::error("StorageRepository: listContents failed for repo #{$repo->id} path '{$path}': " . $e->getMessage());
return [];
}
}
public function encryptSensitiveConfig(array $config, string $tipo): array
{
$sensitiveFields = match ($tipo) {
'webdav' => ['password'],
'google_drive' => ['client_secret', 'refresh_token'],
default => [],
};
foreach ($sensitiveFields as $field) {
if (!empty($config[$field]) && !$this->isEncrypted($config[$field])) {
$config[$field] = Crypt::encryptString($config[$field]);
}
}
return $config;
}
private function normalizePath(string $path): string
{
$path = trim($path);
if ($path === '' || $path === '\\') {
return '';
}
if ($path === '/') {
return '/';
}
return ltrim($path, '/\\');
}
private function isEncrypted(string $value): bool
{
return str_starts_with($value, 'ey') || str_contains($value, ':');
}
private function buildWebDAV(array $config): ?Filesystem
{
$client = new WebDAVClient([
'baseUri' => rtrim($config['base_uri'] ?? '', '/') . '/',
'userName' => $config['username'] ?? '',
'password' => $config['password'] ?? '',
'authType' => $config['auth_type'] ?? 'basic',
]);
$root = ltrim($config['root'] ?? '/', '/');
$adapter = new WebDAVAdapter($client, $root);
return new Filesystem($adapter);
}
private function buildGoogleDrive(array $config): ?Filesystem
{
$client = new \Google_Client();
$client->setClientId($config['client_id'] ?? '');
$client->setClientSecret($config['client_secret'] ?? '');
$client->refreshToken($config['refresh_token'] ?? '');
$client->addScope(\Google_Service_Drive::DRIVE);
$service = new \Google_Service_Drive($client);
$adapter = new GoogleDriveAdapter($service, $config['root_folder_id'] ?? 'root');
return new Filesystem($adapter);
}
}
+2
View File
@@ -7,9 +7,11 @@
"license": "MIT",
"require": {
"php": "^8.3",
"as247/flysystem-google-drive": "^3.0",
"directorytree/imapengine-laravel": "^1.2",
"laravel/framework": "^13.7",
"laravel/tinker": "^3.0",
"league/flysystem-webdav": "^3.31",
"spatie/laravel-permission": "^7.4",
"webklex/php-imap": "^6.2"
},
Generated
+888 -1
View File
@@ -4,8 +4,112 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a0180596cd749c38b349eb76b18c390b",
"content-hash": "c6ab297804fe432e1792b166459e6469",
"packages": [
{
"name": "as247/cloud-storages",
"version": "v1.2.9",
"source": {
"type": "git",
"url": "https://github.com/as247/cloud-storages.git",
"reference": "6a8ef430f7f07a2e88ea6b7cfd32230da493c1f8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/as247/cloud-storages/zipball/6a8ef430f7f07a2e88ea6b7cfd32230da493c1f8",
"reference": "6a8ef430f7f07a2e88ea6b7cfd32230da493c1f8",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=7.1"
},
"type": "library",
"autoload": {
"psr-4": {
"As247\\CloudStorages\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "As247",
"email": "as247@vui360.com",
"homepage": "http://as247.vui360.com"
}
],
"description": "Storages wrapper with path support for drive services: google drive, onedrive,...",
"homepage": "https://github.com/as247",
"keywords": [
"drive",
"google",
"google drive",
"one drive",
"php",
"storage"
],
"support": {
"issues": "https://github.com/as247/cloud-storages/issues",
"source": "https://github.com/as247/cloud-storages/tree/v1.2.9"
},
"time": "2024-03-01T10:56:22+00:00"
},
{
"name": "as247/flysystem-google-drive",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/as247/flysystem-google-drive.git",
"reference": "7d2600c5e085cafbb49c9874d1efb6795d36cb2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/as247/flysystem-google-drive/zipball/7d2600c5e085cafbb49c9874d1efb6795d36cb2f",
"reference": "7d2600c5e085cafbb49c9874d1efb6795d36cb2f",
"shasum": ""
},
"require": {
"as247/cloud-storages": "^1.2.4",
"ext-json": "*",
"google/apiclient": "^2.0",
"league/flysystem": "^3.23",
"php": ">=7.1"
},
"type": "library",
"autoload": {
"psr-4": {
"As247\\Flysystem\\GoogleDrive\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "As247",
"email": "as247@vui360.com",
"homepage": "http://as247.vui360.com"
}
],
"description": "Google Drive Adapter for Flysystem",
"homepage": "https://github.com/as247",
"keywords": [
"drive",
"google",
"google-drive",
"php",
"storage"
],
"support": {
"issues": "https://github.com/as247/flysystem-google-drive/issues",
"source": "https://github.com/as247/flysystem-google-drive/tree/v3.0.2"
},
"time": "2024-05-30T14:41:37+00:00"
},
{
"name": "brick/math",
"version": "0.14.8",
@@ -635,6 +739,70 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "firebase/php-jwt",
"version": "v7.0.5",
"source": {
"type": "git",
"url": "https://github.com/googleapis/php-jwt.git",
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpfastcache/phpfastcache": "^9.2",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"type": "library",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
],
"support": {
"issues": "https://github.com/googleapis/php-jwt/issues",
"source": "https://github.com/googleapis/php-jwt/tree/v7.0.5"
},
"time": "2026-04-01T20:38:03+00:00"
},
{
"name": "fruitcake/php-cors",
"version": "v1.4.0",
@@ -706,6 +874,183 @@
],
"time": "2025-12-03T09:33:47+00:00"
},
{
"name": "google/apiclient",
"version": "v2.19.3",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client.git",
"reference": "a1f02761994fd9defb20f6f1449205fd66f450de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/a1f02761994fd9defb20f6f1449205fd66f450de",
"reference": "a1f02761994fd9defb20f6f1449205fd66f450de",
"shasum": ""
},
"require": {
"firebase/php-jwt": "^6.0||^7.0",
"google/apiclient-services": "~0.350",
"google/auth": "^1.37",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.6",
"monolog/monolog": "^2.9||^3.0",
"php": "^8.1"
},
"require-dev": {
"cache/filesystem-adapter": "^1.1",
"composer/composer": "^2.9",
"phpcompatibility/php-compatibility": "^9.2",
"phpspec/prophecy-phpunit": "^2.1",
"phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.8",
"symfony/css-selector": "~2.1",
"symfony/dom-crawler": "~2.1"
},
"suggest": {
"cache/filesystem-adapter": "For caching certs and tokens (using Google\\Client::setCache)"
},
"type": "library",
"extra": {
"component": {
"entry": "src/Client.php"
},
"branch-alias": {
"dev-main": "2.x-dev"
}
},
"autoload": {
"files": [
"src/aliases.php"
],
"psr-4": {
"Google\\": "src/"
},
"classmap": [
"src/aliases.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Client library for Google APIs",
"homepage": "http://developers.google.com/api-client-library/php",
"keywords": [
"google"
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client/issues",
"source": "https://github.com/googleapis/google-api-php-client/tree/v2.19.3"
},
"time": "2026-05-04T21:00:36+00:00"
},
{
"name": "google/apiclient-services",
"version": "v0.442.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
"reference": "3afe7b5c9d1cebf2a5be018820ce62eeb906b81f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/3afe7b5c9d1cebf2a5be018820ce62eeb906b81f",
"reference": "3afe7b5c9d1cebf2a5be018820ce62eeb906b81f",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6"
},
"type": "library",
"autoload": {
"files": [
"autoload.php"
],
"psr-4": {
"Google\\Service\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Client library for Google APIs",
"homepage": "http://developers.google.com/api-client-library/php",
"keywords": [
"google"
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.442.0"
},
"time": "2026-05-25T01:34:24+00:00"
},
{
"name": "google/auth",
"version": "v1.50.1",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-auth-library-php.git",
"reference": "870c17ee3a1d73338d39a9ffa77a700ba77f5a83"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/870c17ee3a1d73338d39a9ffa77a700ba77f5a83",
"reference": "870c17ee3a1d73338d39a9ffa77a700ba77f5a83",
"shasum": ""
},
"require": {
"firebase/php-jwt": "^6.0||^7.0",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.4.5",
"php": "^8.1",
"psr/cache": "^2.0||^3.0",
"psr/http-message": "^1.1||^2.0",
"psr/log": "^2.0||^3.0"
},
"require-dev": {
"guzzlehttp/promises": "^2.0",
"kelvinmo/simplejwt": "^1.1.0",
"phpseclib/phpseclib": "^3.0.35",
"phpspec/prophecy-phpunit": "^2.1",
"phpunit/phpunit": "^9.6",
"sebastian/comparator": ">=1.2.3",
"squizlabs/php_codesniffer": "^4.0",
"symfony/filesystem": "^6.3||^7.3",
"symfony/process": "^6.0||^7.0",
"webmozart/assert": "^1.11||^2.0"
},
"suggest": {
"phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
},
"type": "library",
"autoload": {
"psr-4": {
"Google\\Auth\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Google Auth Library for PHP",
"homepage": "https://github.com/google/google-auth-library-php",
"keywords": [
"Authentication",
"google",
"oauth2"
],
"support": {
"docs": "https://cloud.google.com/php/docs/reference/auth/latest",
"issues": "https://github.com/googleapis/google-auth-library-php/issues",
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.50.1"
},
"time": "2026-03-18T20:03:29+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.4",
@@ -1914,6 +2259,54 @@
},
"time": "2026-01-23T15:30:45+00:00"
},
{
"name": "league/flysystem-webdav",
"version": "3.31.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-webdav.git",
"reference": "1705b5d4aab6d755a78ea850e523dda9c58ad20f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-webdav/zipball/1705b5d4aab6d755a78ea850e523dda9c58ad20f",
"reference": "1705b5d4aab6d755a78ea850e523dda9c58ad20f",
"shasum": ""
},
"require": {
"league/flysystem": "^3.6.0",
"php": "^8.0.2",
"sabre/dav": "^4.6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\Flysystem\\WebDAV\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
],
"description": "WebDAV filesystem adapter for Flysystem.",
"keywords": [
"Flysystem",
"WebDAV",
"file",
"files",
"filesystem"
],
"support": {
"source": "https://github.com/thephpleague/flysystem-webdav/tree/3.31.0"
},
"time": "2026-01-23T15:30:45+00:00"
},
{
"name": "league/mime-type-detection",
"version": "1.16.0",
@@ -2866,6 +3259,55 @@
],
"time": "2025-12-27T19:41:33+00:00"
},
{
"name": "psr/cache",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/3.0.0"
},
"time": "2021-02-03T23:26:27+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",
@@ -3555,6 +3997,451 @@
},
"time": "2025-12-14T04:43:48+00:00"
},
{
"name": "sabre/dav",
"version": "4.7.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/dav.git",
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-iconv": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"ext-spl": "*",
"lib-libxml": ">=2.7.0",
"php": "^7.1.0 || ^8.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"sabre/event": "^5.0",
"sabre/http": "^5.0.5",
"sabre/uri": "^2.0",
"sabre/vobject": "^4.2.1",
"sabre/xml": "^2.0.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.19",
"monolog/monolog": "^1.27 || ^2.0",
"phpstan/phpstan": "^0.12 || ^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"suggest": {
"ext-curl": "*",
"ext-imap": "*",
"ext-pdo": "*"
},
"bin": [
"bin/sabredav",
"bin/naturalselection"
],
"type": "library",
"autoload": {
"psr-4": {
"Sabre\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "WebDAV Framework for PHP",
"homepage": "http://sabre.io/",
"keywords": [
"CalDAV",
"CardDAV",
"WebDAV",
"framework",
"iCalendar"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/dav/issues",
"source": "https://github.com/fruux/sabre-dav"
},
"time": "2024-10-29T11:46:02+00:00"
},
{
"name": "sabre/event",
"version": "5.1.8",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/event.git",
"reference": "1dd5f55421b0092006510264131a4d632d02d2c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/event/zipball/1dd5f55421b0092006510264131a4d632d02d2c1",
"reference": "1dd5f55421b0092006510264131a4d632d02d2c1",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.17.1||^3.95",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"type": "library",
"autoload": {
"files": [
"lib/coroutine.php",
"lib/Loop/functions.php",
"lib/Promise/functions.php"
],
"psr-4": {
"Sabre\\Event\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "sabre/event is a library for lightweight event-based programming",
"homepage": "http://sabre.io/event/",
"keywords": [
"EventEmitter",
"async",
"coroutine",
"eventloop",
"events",
"hooks",
"plugin",
"promise",
"reactor",
"signal"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/event/issues",
"source": "https://github.com/fruux/sabre-event"
},
"time": "2026-04-27T04:19:36+00:00"
},
{
"name": "sabre/http",
"version": "5.1.13",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/http.git",
"reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/http/zipball/7c2a14097d1a0de2347dcbdc91a02f38e338f4db",
"reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-curl": "*",
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabre/event": ">=4.0 <6.0",
"sabre/uri": "^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.17.1||3.63.2",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"suggest": {
"ext-curl": " to make http requests with the Client class"
},
"type": "library",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Sabre\\HTTP\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
"homepage": "https://github.com/fruux/sabre-http",
"keywords": [
"http"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/http/issues",
"source": "https://github.com/fruux/sabre-http"
},
"time": "2025-09-09T10:21:47+00:00"
},
{
"name": "sabre/uri",
"version": "2.3.4",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/uri.git",
"reference": "b76524c22de90d80ca73143680a8e77b1266c291"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291",
"reference": "b76524c22de90d80ca73143680a8e77b1266c291",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.63",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^1.12",
"phpstan/phpstan-phpunit": "^1.4",
"phpstan/phpstan-strict-rules": "^1.6",
"phpunit/phpunit": "^9.6"
},
"type": "library",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Sabre\\Uri\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "Functions for making sense out of URIs.",
"homepage": "http://sabre.io/uri/",
"keywords": [
"rfc3986",
"uri",
"url"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/uri/issues",
"source": "https://github.com/fruux/sabre-uri"
},
"time": "2024-08-27T12:18:16+00:00"
},
{
"name": "sabre/vobject",
"version": "4.5.8",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/vobject.git",
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabre/xml": "^2.1 || ^3.0 || ^4.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.17.1",
"phpstan/phpstan": "^0.12 || ^1.12 || ^2.0",
"phpunit/php-invoker": "^2.0 || ^3.1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"suggest": {
"hoa/bench": "If you would like to run the benchmark scripts"
},
"bin": [
"bin/vobject",
"bin/generate_vcards"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Sabre\\VObject\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Dominik Tobschall",
"email": "dominik@fruux.com",
"homepage": "http://tobschall.de/",
"role": "Developer"
},
{
"name": "Ivan Enderlin",
"email": "ivan.enderlin@hoa-project.net",
"homepage": "http://mnt.io/",
"role": "Developer"
}
],
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
"homepage": "http://sabre.io/vobject/",
"keywords": [
"availability",
"freebusy",
"iCalendar",
"ical",
"ics",
"jCal",
"jCard",
"recurrence",
"rfc2425",
"rfc2426",
"rfc2739",
"rfc4770",
"rfc5545",
"rfc5546",
"rfc6321",
"rfc6350",
"rfc6351",
"rfc6474",
"rfc6638",
"rfc6715",
"rfc6868",
"vCalendar",
"vCard",
"vcf",
"xCal",
"xCard"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/vobject/issues",
"source": "https://github.com/fruux/sabre-vobject"
},
"time": "2026-01-12T10:45:19+00:00"
},
{
"name": "sabre/xml",
"version": "2.2.11",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/xml.git",
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"lib-libxml": ">=2.6.20",
"php": "^7.1 || ^8.0",
"sabre/uri": ">=1.0,<3.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.17.1||3.63.2",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"type": "library",
"autoload": {
"files": [
"lib/Deserializer/functions.php",
"lib/Serializer/functions.php"
],
"psr-4": {
"Sabre\\Xml\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Markus Staab",
"email": "markus.staab@redaxo.de",
"role": "Developer"
}
],
"description": "sabre/xml is an XML library that you may not hate.",
"homepage": "https://sabre.io/xml/",
"keywords": [
"XMLReader",
"XMLWriter",
"dom",
"xml"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/xml/issues",
"source": "https://github.com/fruux/sabre-xml"
},
"time": "2024-09-06T07:37:46+00:00"
},
{
"name": "spatie/laravel-package-tools",
"version": "1.93.0",
@@ -8,15 +8,19 @@ return new class extends Migration
{
public function up(): void
{
if (Schema::hasColumn('individui', 'telegram_id')) {
Schema::table('individui', function (Blueprint $table) {
$table->dropColumn('telegram_id');
});
}
}
public function down(): void
{
if (!Schema::hasColumn('individui', 'telegram_id')) {
Schema::table('individui', function (Blueprint $table) {
$table->string('telegram_id', 100)->nullable()->after('note');
});
}
}
};
@@ -0,0 +1,23 @@
<?php
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('documenti_cartelle', function (Blueprint $table) {
$table->id();
$table->foreignId('parent_id')->nullable()->constrained('documenti_cartelle')->onDelete('cascade');
$table->string('nome');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('documenti_cartelle');
}
};
@@ -0,0 +1,23 @@
<?php
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->foreignId('cartella_id')->nullable()->constrained('documenti_cartelle')->onDelete('set null');
});
}
public function down(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->dropForeign(['cartella_id']);
$table->dropColumn('cartella_id');
});
}
};
@@ -0,0 +1,23 @@
<?php
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('app_settings', function (Blueprint $table) {
$table->string('documenti_storage_disk')->default('local')->after('show_version');
$table->string('documenti_storage_path')->default('documenti')->after('documenti_storage_disk');
});
}
public function down(): void
{
Schema::table('app_settings', function (Blueprint $table) {
$table->dropColumn(['documenti_storage_disk', 'documenti_storage_path']);
});
}
};
@@ -0,0 +1,26 @@
<?php
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('storage_repositories', function (Blueprint $table) {
$table->id();
$table->string('nome');
$table->string('tipo', 50);
$table->text('config');
$table->boolean('is_active')->default(true);
$table->integer('ordine')->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('storage_repositories');
}
};
@@ -0,0 +1,27 @@
<?php
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->foreignId('repository_id')
->nullable()
->constrained('storage_repositories')
->nullOnDelete()
->after('cartella_id');
});
}
public function down(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->dropForeign(['repository_id']);
$table->dropColumn('repository_id');
});
}
};
@@ -0,0 +1,24 @@
<?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->string('storage_disk', 50)->nullable()->after('repository_id');
});
}
public function down(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->dropColumn('storage_disk');
});
}
};
@@ -0,0 +1,43 @@
@props(['cartelle', 'currentFolderId' => null, 'level' => 0, 'canWrite' => false, 'canDelete' => false])
@foreach($cartelle as $cartella)
@php
$isActive = $currentFolderId && (int) $currentFolderId === (int) $cartella->id;
$hasChildren = $cartella->children->count() > 0;
@endphp
<div class="list-group-item d-flex align-items-center py-1 px-2 {{ $isActive ? 'active' : '' }}"
style="padding-left: {{ 20 + $level * 20 }}px !important;">
<a href="/documenti?folder_id={{ $cartella->id }}"
class="d-flex align-items-center text-decoration-none flex-grow-1 {{ $isActive ? 'text-white' : '' }}"
style="min-width: 0;">
<i class="fas fa-folder {{ $isActive ? 'text-white' : 'text-warning' }} mr-2"></i>
<span class="small text-truncate">{{ $cartella->nome }}</span>
</a>
@if($canWrite || $canDelete)
<div class="folder-actions ml-1 flex-shrink-0">
@if($canWrite)
<button type="button" class="btn btn-xs btn-link p-0 mr-1" onclick='event.stopPropagation(); renameFolder({{ $cartella->id }}, {{ json_encode($cartella->nome) }})' title="Rinomina">
<i class="fas fa-pen {{ $isActive ? 'text-white' : 'text-muted' }}"></i>
</button>
<button type="button" class="btn btn-xs btn-link p-0 mr-1" onclick='event.stopPropagation(); moveFolder({{ $cartella->id }}, {{ json_encode($cartella->nome) }})' title="Sposta">
<i class="fas fa-folder-open {{ $isActive ? 'text-white' : 'text-info' }}"></i>
</button>
@endif
@if($canDelete)
<button type="button" class="btn btn-xs btn-link p-0" onclick='event.stopPropagation(); deleteFolder({{ $cartella->id }}, {{ json_encode($cartella->nome) }})' title="Elimina">
<i class="fas fa-times {{ $isActive ? 'text-white' : 'text-danger' }}"></i>
</button>
@endif
</div>
@endif
</div>
@if($hasChildren)
@include('documenti._folder_tree', [
'cartelle' => $cartella->children,
'currentFolderId' => $currentFolderId,
'level' => $level + 1,
'canWrite' => $canWrite,
'canDelete' => $canDelete,
])
@endif
@endforeach
File diff suppressed because it is too large Load Diff
+51
View File
@@ -29,6 +29,33 @@
<a href="{{ route('gruppi.index') }}" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="small-box bg-primary">
<div class="inner">
<h3>{{ $stats['documenti'] }}</h3>
<p>Documenti</p>
</div>
<div class="icon">
<i class="fas fa-file"></i>
</div>
<a href="/documenti" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="small-box bg-danger">
<div class="inner">
<h3>{{ $stats['eventi'] }}</h3>
<p>Eventi</p>
</div>
<div class="icon">
<i class="fas fa-calendar"></i>
</div>
<a href="/eventi" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-3 col-6">
<div class="small-box bg-warning">
<div class="inner">
@@ -41,6 +68,30 @@
<a href="#" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="small-box bg-secondary">
<div class="inner">
<h3>{{ $stats['email_nuove'] }}</h3>
<p>Nuove Email</p>
</div>
<div class="icon">
<i class="fas fa-envelope"></i>
</div>
<a href="/email" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="small-box bg-dark">
<div class="inner">
<h3>{{ $stats['mailing_liste'] }}</h3>
<p>Mailing List</p>
</div>
<div class="icon">
<i class="fas fa-list-alt"></i>
</div>
<a href="/mailing-liste" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
</div>
<div class="row">
@@ -20,6 +20,12 @@
<a href="#applicazione" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-cog mr-2"></i> Applicazione
</a>
<a href="#repository" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-cloud mr-2"></i> Repository Remoti
</a>
<a href="#documenti" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-file mr-2"></i> Repository Documenti
</a>
<a href="#logo" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-image mr-2"></i> Logo
</a>
@@ -124,6 +130,188 @@
</div>
</div>
{{-- === REPOSITORY REMOTI (Opzione B) === --}}
<div class="tab-pane" id="repository">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-cloud mr-2"></i> Repository Remoti</h3>
<div class="card-tools">
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#repoModal" onclick="resetRepoForm()">
<i class="fas fa-plus mr-1"></i> Nuovo Repository
</button>
</div>
</div>
<div class="card-body">
<p class="text-muted">Configura repository remoti (WebDAV, Google Drive) per archiviare documenti. Ogni repository appare come root separata nell'albero delle cartelle in Gestione Documenti.</p>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
@if($repositories->count() > 0)
<div class="table-responsive">
<table class="table table-bordered table-hover table-sm" id="repoTable">
<thead>
<tr>
<th style="width:40px;">#</th>
<th>Nome</th>
<th>Tipo</th>
<th>Stato</th>
<th style="width:180px;">Azioni</th>
</tr>
</thead>
<tbody id="repoSortable">
@foreach($repositories as $repo)
<tr data-id="{{ $repo->id }}">
<td class="text-center handle" style="cursor:grab;"><i class="fas fa-grip-vertical text-muted"></i></td>
<td><strong>{{ $repo->nome }}</strong></td>
<td>
<span class="badge badge-{{ $repo->tipo === 'google_drive' ? 'danger' : 'secondary' }}">
<i class="fas {{ $repo->tipo === 'google_drive' ? 'fa-google-drive' : 'fa-server' }} mr-1"></i>
{{ \App\Models\StorageRepository::etichettaTipo($repo->tipo) }}
</span>
</td>
<td>
@if($repo->is_active)
<span class="badge badge-success">Attivo</span>
@else
<span class="badge badge-secondary">Disattivo</span>
@endif
</td>
<td>
<button type="button" class="btn btn-xs btn-info" onclick="editRepo({{ $repo->id }})" title="Modifica">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-xs btn-success test-repo-btn" onclick="testRepo({{ $repo->id }}, this)" title="Test connessione">
<i class="fas fa-plug"></i>
</button>
<button type="button" class="btn btn-xs btn-danger" onclick="deleteRepo({{ $repo->id }}, '{{ addslashes($repo->nome) }}')" title="Elimina">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="text-center text-muted py-4">
<i class="fas fa-cloud fa-3x mb-3"></i>
<p class="mb-0">Nessun repository remoto configurato.</p>
<p class="mb-0">Clicca "Nuovo Repository" per aggiungere un server WebDAV o Google Drive.</p>
</div>
@endif
</div>
</div>
@if($repositories->count() > 0)
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i> Come funziona</h3>
</div>
<div class="card-body">
<ul class="mb-0">
<li>Ogni repository configurato apparirà come <strong>root separata</strong> nella sidebar <strong>Gestione Documenti</strong>, sotto "Repository Remoti".</li>
<li>Puoi navigare i file e le cartelle di ogni repository remoto direttamente dall'interfaccia documenti.</li>
<li><strong>WebDAV</strong>: supporta server che parlano il protocollo WebDAV (es. NextCloud, ownCloud, Synology).</li>
<li><strong>Google Drive</strong>: richiede OAuth 2.0 con client ID, client secret e refresh token configurati nella <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>.</li>
<li>Usa il pulsante <i class="fas fa-plug text-success"></i> <strong>Test</strong> per verificare la connettività dopo la configurazione.</li>
</ul>
</div>
</div>
@endif
</div>
{{-- === REPOSITORY DOCUMENTI === --}}
<div class="tab-pane" id="documenti">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Repository Documenti</h3>
</div>
<div class="card-body">
<p class="text-muted">Configura il repository filesystem dove vengono salvati i documenti caricati.</p>
@php
$currentDisk = $appSettings->documenti_storage_disk ?? 'local';
$currentPath = $appSettings->documenti_storage_path ?? 'documenti';
$absPath = storage_path('app/' . ($currentDisk === 'local' ? 'private/' : '') . $currentPath);
@endphp
<form method="POST" action="{{ route('impostazioni.app-settings.save') }}">
@csrf
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="documenti_storage_disk">Disco di archiviazione</label>
<select name="documenti_storage_disk" id="documenti_storage_disk" class="form-control">
<option value="local" {{ $currentDisk === 'local' ? 'selected' : '' }}>Locale (storage/app/private)</option>
<option value="public" {{ $currentDisk === 'public' ? 'selected' : '' }}>Pubblico (storage/app/public)</option>
</select>
<small class="text-muted">Il disco predefinito per salvare i documenti</small>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="documenti_storage_path">Percorso relativo</label>
<input type="text" name="documenti_storage_path" id="documenti_storage_path" class="form-control"
value="{{ $currentPath }}" placeholder="es. documenti">
<small class="text-muted">Sottocartella all'interno del disco</small>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle mr-2"></i>
<strong>Percorso assoluto calcolato:</strong>
<code>{{ $absPath }}</code>
<br>
<small class="text-muted">I nuovi documenti verranno salvati in questa directory.</small>
</div>
@if(session('path_changed'))
@php
$migrateCount = session('migration_count', 0);
$oldD = session('old_storage_disk', 'local');
$oldP = session('old_storage_path', 'documenti');
$newD = session('new_storage_disk', 'local');
$newP = session('new_storage_path', 'documenti');
@endphp
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong>Percorso modificato!</strong>
<p class="mb-2">La nuova directory <code>{{ $newD }}://{{ $newP }}</code> è stata creata.
@if($migrateCount > 0)
Ci sono <strong>{{ $migrateCount }}</strong> file nella vecchia posizione <code>{{ $oldD }}://{{ $oldP }}</code>.</p>
<form method="POST" action="{{ route('impostazioni.migrate-percorso') }}" class="d-inline" onsubmit="return confirm('Spostare {{ $migrateCount }} file? Questa operazione potrebbe richiedere alcuni secondi.');">
@csrf
<input type="hidden" name="old_storage_disk" value="{{ $oldD }}">
<input type="hidden" name="old_storage_path" value="{{ $oldP }}">
<input type="hidden" name="new_storage_disk" value="{{ $newD }}">
<input type="hidden" name="new_storage_path" value="{{ $newP }}">
<button type="submit" class="btn btn-warning">
<i class="fas fa-exchange-alt mr-1"></i> Sposta {{ $migrateCount }} file nel nuovo percorso
</button>
<a href="/impostazioni#documenti" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Ignora
</a>
</form>
@else
<p class="mb-0">Nessun file da spostare.</p>
@endif
</div>
@endif
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i> Salva Impostazioni
</button>
</form>
</div>
</div>
</div>
{{-- === LOGO === --}}
<div class="tab-pane" id="logo">
<div class="card card-primary">
@@ -1434,5 +1622,252 @@ if (window.location.hash === '#email') {
setTimeout(initSignatureEditor, 300);
});
}
// ===== REPOSITORY REMOTI =====
let repositories = @json($repositories);
function resetRepoForm() {
document.getElementById('repoForm').action = '{{ route('storage-repositories.store') }}';
document.getElementById('repoMethod').value = 'POST';
document.getElementById('repoModalTitle').textContent = 'Nuovo Repository Remoto';
document.getElementById('repoForm').reset();
document.getElementById('repoId').value = '';
document.getElementById('repo_is_active').checked = true;
toggleRepoConfigFields();
}
function toggleRepoConfigFields() {
const tipo = document.getElementById('repo_tipo').value;
document.getElementById('webdavFields').style.display = tipo === 'webdav' ? '' : 'none';
document.getElementById('googleDriveFields').style.display = tipo === 'google_drive' ? '' : 'none';
}
function editRepo(id) {
const repo = repositories.find(r => r.id === id);
if (!repo) return;
document.getElementById('repoForm').action = '{{ route('storage-repositories.update', '__ID__') }}'.replace('__ID__', id);
document.getElementById('repoMethod').value = 'PUT';
document.getElementById('repoModalTitle').textContent = 'Modifica Repository - ' + repo.nome;
document.getElementById('repoId').value = repo.id;
document.getElementById('repo_nome').value = repo.nome;
document.getElementById('repo_tipo').value = repo.tipo;
document.getElementById('repo_is_active').checked = repo.is_active;
const config = repo.config || {};
document.getElementById('webdav_base_uri').value = config.base_uri || '';
document.getElementById('webdav_username').value = config.username || '';
document.getElementById('webdav_password').value = '';
document.getElementById('webdav_password').placeholder = config.password ? '......' : '';
document.getElementById('webdav_root').value = config.root || '/';
document.getElementById('webdav_auth_type').value = config.auth_type || 'basic';
document.getElementById('gd_client_id').value = config.client_id || '';
document.getElementById('gd_client_secret').value = '';
document.getElementById('gd_client_secret').placeholder = config.client_secret ? '......' : '';
document.getElementById('gd_refresh_token').value = '';
document.getElementById('gd_refresh_token').placeholder = config.refresh_token ? '......' : '';
document.getElementById('gd_root_folder_id').value = config.root_folder_id || 'root';
toggleRepoConfigFields();
$('#repoModal').modal('show');
}
function deleteRepo(id, nome) {
if (!confirm('Eliminare il repository "' + nome + '"?')) return;
var url = '{{ route('storage-repositories.destroy', '__ID__') }}'.replace('__ID__', id);
fetch(url, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'X-HTTP-Method-Override': 'DELETE'
}
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success) {
location.reload();
} else {
alert('❌ ' + (data.message || 'Errore'));
}
}).catch(function() {
alert('❌ Errore di rete');
});
}
function testRepo(id, btn) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Test...';
$.ajax({
url: '{{ route('storage-repositories.test', '__ID__') }}'.replace('__ID__', id),
method: 'POST',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
timeout: 30000,
success: function(response) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plug"></i>';
alert(response.success ? 'Connessione OK: ' + response.message : 'Errore: ' + response.message);
},
error: function(xhr) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plug"></i>';
const msg = xhr.responseJSON?.message || 'Errore di connessione';
alert('Errore: ' + msg);
}
});
}
$(document).ready(function() {
if (document.getElementById('repoSortable')) {
var sortableRepo = Sortable.create(document.getElementById('repoSortable'), {
handle: '.handle',
animation: 150,
onEnd: function(evt) {
var order = [];
$('#repoSortable tr').each(function() {
order.push($(this).data('id'));
});
fetch('{{ route('storage-repositories.reorder') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Content-Type': 'application/json',
},
body: JSON.stringify({ order: order })
});
}
});
}
});
</script>
{{-- MODAL: Repository Remoto --}}
<div class="modal fade" id="repoModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="repoModalTitle"><i class="fas fa-cloud mr-2"></i>Nuovo Repository Remoto</h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<form id="repoForm" method="POST" action="{{ route('storage-repositories.store') }}">
@csrf
<input type="hidden" name="_method" id="repoMethod" value="POST">
<input type="hidden" name="id" id="repoId" value="">
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="repo_nome">Nome *</label>
<input type="text" name="nome" id="repo_nome" class="form-control" placeholder="es. Server Parrocchia" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="repo_tipo">Tipo *</label>
<select name="tipo" id="repo_tipo" class="form-control" required onchange="toggleRepoConfigFields()">
<option value="webdav">WebDAV</option>
<option value="google_drive">Google Drive</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="repo_is_active" name="is_active" value="1" checked>
<label class="custom-control-label" for="repo_is_active">Repository attivo</label>
</div>
</div>
<hr>
<h6>Configurazione</h6>
{{-- WebDAV Fields --}}
<div id="webdavFields">
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="webdav_base_uri">URL Base WebDAV *</label>
<input type="url" name="config[base_uri]" id="webdav_base_uri" class="form-control" placeholder="es. https://nextcloud.parrocchia.it/remote.php/dav/files/utente/">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="webdav_root">Root path</label>
<input type="text" name="config[root]" id="webdav_root" class="form-control" value="/" placeholder="/">
<small class="text-muted">Percorso base nel server</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="webdav_username">Username</label>
<input type="text" name="config[username]" id="webdav_username" class="form-control">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="webdav_password">Password</label>
<input type="password" name="config[password]" id="webdav_password" class="form-control">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="webdav_auth_type">Auth Type</label>
<select name="config[auth_type]" id="webdav_auth_type" class="form-control">
<option value="basic">Basic</option>
<option value="digest">Digest</option>
<option value="ntlm">NTLM</option>
</select>
</div>
</div>
</div>
</div>
{{-- Google Drive Fields --}}
<div id="googleDriveFields" style="display:none;">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="gd_client_id">Client ID *</label>
<input type="text" name="config[client_id]" id="gd_client_id" class="form-control" placeholder="Google Client ID">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="gd_client_secret">Client Secret *</label>
<input type="text" name="config[client_secret]" id="gd_client_secret" class="form-control" placeholder="Google Client Secret">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="gd_refresh_token">Refresh Token *</label>
<input type="text" name="config[refresh_token]" id="gd_refresh_token" class="form-control" placeholder="OAuth Refresh Token">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="gd_root_folder_id">Root Folder ID</label>
<input type="text" name="config[root_folder_id]" id="gd_root_folder_id" class="form-control" value="root" placeholder="root">
<small class="text-muted">ID della cartella Google Drive, default: root</small>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle mr-2"></i>
Per ottenere le credenziali OAuth: <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>
&rarr; API &amp; Services &rarr; Credentials &rarr; Crea OAuth Client ID (Desktop App).
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i> Salva Repository
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
+21
View File
@@ -7,6 +7,7 @@ use App\Http\Controllers\IndividuoController;
use App\Http\Controllers\GruppoController;
use App\Http\Controllers\ContattoController;
use App\Http\Controllers\DocumentoController;
use App\Http\Controllers\DocumentoCartellaController;
use App\Http\Controllers\GruppoIndividuoController;
use App\Http\Controllers\GruppoMembroController;
use App\Http\Controllers\EventoController;
@@ -21,6 +22,7 @@ use App\Http\Controllers\Admin\RuoloController;
use App\Http\Controllers\Admin\ActivityLogController;
use App\Http\Controllers\Admin\EmailSettingsController;
use App\Http\Controllers\EmailController;
use App\Http\Controllers\StorageRepositoryController;
use App\Http\Middleware\AdminOnly;
use App\Http\Middleware\CheckPermission;
@@ -109,9 +111,16 @@ Route::delete('gruppi/vista/{vista}', [GruppoController::class, 'deleteVista'])-
Route::get('documenti', [DocumentoController::class, 'index'])->name('documenti.index')->middleware('auth');
Route::post('documenti', [DocumentoController::class, 'store'])->middleware('auth');
Route::post('documenti/cartelle', [DocumentoCartellaController::class, 'store'])->middleware('auth');
Route::put('documenti/cartelle/{id}', [DocumentoCartellaController::class, 'update'])->middleware('auth');
Route::delete('documenti/cartelle/{id}', [DocumentoCartellaController::class, 'destroy'])->middleware('auth');
Route::get('documenti/cartelle/{id}/breadcrumb', [DocumentoCartellaController::class, 'breadcrumb'])->middleware('auth');
Route::post('documenti/mass-destroy', [DocumentoController::class, 'massDestroy'])->middleware('auth');
Route::post('documenti/mass-update', [DocumentoController::class, 'massUpdate'])->middleware('auth');
Route::post('documenti/mass-associate', [DocumentoController::class, 'massAssociate'])->middleware('auth');
Route::post('documenti/mass-move', [DocumentoController::class, 'massMove'])->middleware('auth');
Route::post('documenti/mass-download', [DocumentoController::class, 'massDownload'])->middleware('auth');
Route::post('documenti/check-links', [DocumentoController::class, 'checkMassLinks'])->middleware('auth');
Route::get('documenti/options', [DocumentoController::class, 'options'])->middleware('auth');
@@ -119,6 +128,7 @@ Route::get('documenti/{documento}/check-links', [DocumentoController::class, 'ch
Route::get('documenti/{documento}/edit', [DocumentoController::class, 'edit'])->middleware('auth');
Route::put('documenti/{documento}', [DocumentoController::class, 'update'])->middleware('auth');
Route::get('documenti/{documento}/download', [DocumentoController::class, 'download'])->middleware('auth');
Route::post('documenti/{documento}/move', [DocumentoController::class, 'moveDocumento'])->middleware('auth');
Route::get('documenti/{documento}/preview', [DocumentoController::class, 'preview'])->middleware('auth');
Route::delete('documenti/{documento}', [DocumentoController::class, 'destroy'])->middleware('auth');
@@ -162,6 +172,7 @@ Route::post('/impostazioni/tipologie-eventi/reorder', [ImpostazioniController::c
Route::post('/impostazioni/logo', [ImpostazioniController::class, 'uploadLogo'])->middleware('auth')->name('impostazioni.logo.upload');
Route::post('/impostazioni/logo/remove', [ImpostazioniController::class, 'removeLogo'])->middleware('auth')->name('impostazioni.logo.remove');
Route::post('/impostazioni/app-settings', [ImpostazioniController::class, 'saveAppSettings'])->middleware('auth')->name('impostazioni.app-settings.save');
Route::post('/impostazioni/migra-percorso', [ImpostazioniController::class, 'migrateStoragePath'])->middleware('auth')->name('impostazioni.migrate-percorso');
Route::get('/impostazioni/email', [EmailSettingsController::class, 'index'])->middleware('auth')->name('impostazioni.email.index');
Route::post('/impostazioni/email', [EmailSettingsController::class, 'save'])->middleware('auth')->name('impostazioni.email.save');
@@ -173,6 +184,16 @@ Route::put('/impostazioni/sender/{id}', [EmailSettingsController::class, 'sender
Route::delete('/impostazioni/sender/{id}', [EmailSettingsController::class, 'senderDestroy'])->middleware('auth')->name('impostazioni.sender.destroy');
Route::post('/impostazioni/sender/{id}/test', [EmailSettingsController::class, 'senderTestSmtp'])->middleware('auth')->name('impostazioni.sender.test');
// Storage Repositories (Opzione B)
Route::post('/storage-repositories', [StorageRepositoryController::class, 'store'])->middleware('auth')->name('storage-repositories.store');
Route::put('/storage-repositories/{storage_repository}', [StorageRepositoryController::class, 'update'])->middleware('auth')->name('storage-repositories.update');
Route::delete('/storage-repositories/{storage_repository}', [StorageRepositoryController::class, 'destroy'])->middleware('auth')->name('storage-repositories.destroy');
Route::post('/storage-repositories/{storage_repository}/test', [StorageRepositoryController::class, 'test'])->middleware('auth')->name('storage-repositories.test');
Route::get('/storage-repositories/{storage_repository}/browse', [StorageRepositoryController::class, 'browse'])->middleware('auth')->name('storage-repositories.browse');
Route::post('/storage-repositories/reorder', [StorageRepositoryController::class, 'reorder'])->middleware('auth')->name('storage-repositories.reorder');
Route::get('/storage-repositories/{storage_repository}/download', [StorageRepositoryController::class, 'downloadRemote'])->middleware('auth')->name('storage-repositories.download');
Route::get('/storage-repositories/{storage_repository}/preview', [StorageRepositoryController::class, 'previewRemote'])->middleware('auth')->name('storage-repositories.preview');
Route::get('/email/{folder?}', [EmailController::class, 'index'])->middleware('auth')->name('email.index')->defaults('folder', 'inbox');
Route::get('/email/{folder}/{id}', [EmailController::class, 'show'])->middleware('auth')->name('email.show');
Route::get('/email/compose', [EmailController::class, 'compose'])->middleware('auth')->name('email.compose');
@@ -1,44 +0,0 @@
<?php if($paginator->hasPages()): ?>
<ul class="pagination justify-content-center" role="navigation">
<?php if($paginator->onFirstPage()): ?>
<li class="page-item disabled" aria-disabled="true" aria-label="<?php echo app('translator')->get('pagination.previous'); ?>">
<span class="page-link" aria-hidden="true">&lsaquo;</span>
</li>
<?php else: ?>
<li class="page-item">
<a class="page-link" href="<?php echo e($paginator->previousPageUrl()); ?>" rel="prev" aria-label="<?php echo app('translator')->get('pagination.previous'); ?>">&lsaquo;</a>
</li>
<?php endif; ?>
<?php $__currentLoopData = $elements; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $element): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php if(is_string($element)): ?>
<li class="page-item disabled" aria-disabled="true"><span class="page-link"><?php echo e($element); ?></span></li>
<?php endif; ?>
<?php if(is_array($element)): ?>
<?php $__currentLoopData = $element; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $page => $url): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php if($page == $paginator->currentPage()): ?>
<li class="page-item active" aria-current="page"><span class="page-link"><?php echo e($page); ?></span></li>
<?php elseif($page >= $paginator->currentPage() - 2 && $page <= $paginator->currentPage() + 2): ?>
<li class="page-item"><a class="page-link" href="<?php echo e($url); ?>"><?php echo e($page); ?></a></li>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php if($paginator->hasMorePages()): ?>
<li class="page-item">
<a class="page-link" href="<?php echo e($paginator->nextPageUrl()); ?>" rel="next" aria-label="<?php echo app('translator')->get('pagination.next'); ?>">&rsaquo;</a>
</li>
<?php else: ?>
<li class="page-item disabled" aria-disabled="true" aria-label="<?php echo app('translator')->get('pagination.next'); ?>">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
<?php endif; ?>
</ul>
<?php endif; ?><?php /**PATH /var/www/html/glastree/resources/views/vendor/pagination/simple-bootstrap-4.blade.php ENDPATH**/ ?>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,72 @@
<?php $attributes ??= new \Illuminate\View\ComponentAttributeBag;
$__newAttributes = [];
$__propNames = \Illuminate\View\ComponentAttributeBag::extractPropNames((['cartelle', 'currentFolderId' => null, 'level' => 0, 'canWrite' => false, 'canDelete' => false]));
foreach ($attributes->all() as $__key => $__value) {
if (in_array($__key, $__propNames)) {
$$__key = $$__key ?? $__value;
} else {
$__newAttributes[$__key] = $__value;
}
}
$attributes = new \Illuminate\View\ComponentAttributeBag($__newAttributes);
unset($__propNames);
unset($__newAttributes);
foreach (array_filter((['cartelle', 'currentFolderId' => null, 'level' => 0, 'canWrite' => false, 'canDelete' => false]), 'is_string', ARRAY_FILTER_USE_KEY) as $__key => $__value) {
$$__key = $$__key ?? $__value;
}
$__defined_vars = get_defined_vars();
foreach ($attributes->all() as $__key => $__value) {
if (array_key_exists($__key, $__defined_vars)) unset($$__key);
}
unset($__defined_vars, $__key, $__value); ?>
<?php $__currentLoopData = $cartelle; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $cartella): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php
$isActive = $currentFolderId && (int) $currentFolderId === (int) $cartella->id;
$hasChildren = $cartella->children->count() > 0;
?>
<div class="list-group-item d-flex align-items-center py-1 px-2 <?php echo e($isActive ? 'active' : ''); ?>"
style="padding-left: <?php echo e(20 + $level * 20); ?>px !important;">
<a href="/documenti?folder_id=<?php echo e($cartella->id); ?>"
class="d-flex align-items-center text-decoration-none flex-grow-1 <?php echo e($isActive ? 'text-white' : ''); ?>"
style="min-width: 0;">
<i class="fas fa-folder <?php echo e($isActive ? 'text-white' : 'text-warning'); ?> mr-2"></i>
<span class="small text-truncate"><?php echo e($cartella->nome); ?></span>
</a>
<?php if($canWrite || $canDelete): ?>
<div class="folder-actions ml-1 flex-shrink-0">
<?php if($canWrite): ?>
<button type="button" class="btn btn-xs btn-link p-0 mr-1" onclick='event.stopPropagation(); renameFolder(<?php echo e($cartella->id); ?>, <?php echo e(json_encode($cartella->nome)); ?>)' title="Rinomina">
<i class="fas fa-pen <?php echo e($isActive ? 'text-white' : 'text-muted'); ?>"></i>
</button>
<button type="button" class="btn btn-xs btn-link p-0 mr-1" onclick='event.stopPropagation(); moveFolder(<?php echo e($cartella->id); ?>, <?php echo e(json_encode($cartella->nome)); ?>)' title="Sposta">
<i class="fas fa-folder-open <?php echo e($isActive ? 'text-white' : 'text-info'); ?>"></i>
</button>
<?php endif; ?>
<?php if($canDelete): ?>
<button type="button" class="btn btn-xs btn-link p-0" onclick='event.stopPropagation(); deleteFolder(<?php echo e($cartella->id); ?>, <?php echo e(json_encode($cartella->nome)); ?>)' title="Elimina">
<i class="fas fa-times <?php echo e($isActive ? 'text-white' : 'text-danger'); ?>"></i>
</button>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php if($hasChildren): ?>
<?php echo $__env->make('documenti._folder_tree', [
'cartelle' => $cartella->children,
'currentFolderId' => $currentFolderId,
'level' => $level + 1,
'canWrite' => $canWrite,
'canDelete' => $canDelete,
], array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php /**PATH /var/www/html/glastree/resources/views/documenti/_folder_tree.blade.php ENDPATH**/ ?>
@@ -27,6 +27,33 @@
<a href="<?php echo e(route('gruppi.index')); ?>" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="small-box bg-primary">
<div class="inner">
<h3><?php echo e($stats['documenti']); ?></h3>
<p>Documenti</p>
</div>
<div class="icon">
<i class="fas fa-file"></i>
</div>
<a href="/documenti" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="small-box bg-danger">
<div class="inner">
<h3><?php echo e($stats['eventi']); ?></h3>
<p>Eventi</p>
</div>
<div class="icon">
<i class="fas fa-calendar"></i>
</div>
<a href="/eventi" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-3 col-6">
<div class="small-box bg-warning">
<div class="inner">
@@ -39,6 +66,30 @@
<a href="#" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="small-box bg-secondary">
<div class="inner">
<h3><?php echo e($stats['email_nuove']); ?></h3>
<p>Nuove Email</p>
</div>
<div class="icon">
<i class="fas fa-envelope"></i>
</div>
<a href="/email" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="small-box bg-dark">
<div class="inner">
<h3><?php echo e($stats['mailing_liste']); ?></h3>
<p>Mailing List</p>
</div>
<div class="icon">
<i class="fas fa-list-alt"></i>
</div>
<a href="/mailing-liste" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
</div>
<div class="row">
@@ -18,6 +18,12 @@
<a href="#applicazione" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-cog mr-2"></i> Applicazione
</a>
<a href="#repository" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-cloud mr-2"></i> Repository Remoti
</a>
<a href="#documenti" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-file mr-2"></i> Repository Documenti
</a>
<a href="#logo" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-image mr-2"></i> Logo
</a>
@@ -123,6 +129,189 @@
</div>
<div class="tab-pane" id="repository">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-cloud mr-2"></i> Repository Remoti</h3>
<div class="card-tools">
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#repoModal" onclick="resetRepoForm()">
<i class="fas fa-plus mr-1"></i> Nuovo Repository
</button>
</div>
</div>
<div class="card-body">
<p class="text-muted">Configura repository remoti (WebDAV, Google Drive) per archiviare documenti. Ogni repository appare come root separata nell'albero delle cartelle in Gestione Documenti.</p>
<?php if(session('success')): ?>
<div class="alert alert-success"><?php echo e(session('success')); ?></div>
<?php endif; ?>
<?php if(session('error')): ?>
<div class="alert alert-danger"><?php echo e(session('error')); ?></div>
<?php endif; ?>
<?php if($repositories->count() > 0): ?>
<div class="table-responsive">
<table class="table table-bordered table-hover table-sm" id="repoTable">
<thead>
<tr>
<th style="width:40px;">#</th>
<th>Nome</th>
<th>Tipo</th>
<th>Stato</th>
<th style="width:180px;">Azioni</th>
</tr>
</thead>
<tbody id="repoSortable">
<?php $__currentLoopData = $repositories; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $repo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<tr data-id="<?php echo e($repo->id); ?>">
<td class="text-center handle" style="cursor:grab;"><i class="fas fa-grip-vertical text-muted"></i></td>
<td><strong><?php echo e($repo->nome); ?></strong></td>
<td>
<span class="badge badge-<?php echo e($repo->tipo === 'google_drive' ? 'danger' : 'secondary'); ?>">
<i class="fas <?php echo e($repo->tipo === 'google_drive' ? 'fa-google-drive' : 'fa-server'); ?> mr-1"></i>
<?php echo e(\App\Models\StorageRepository::etichettaTipo($repo->tipo)); ?>
</span>
</td>
<td>
<?php if($repo->is_active): ?>
<span class="badge badge-success">Attivo</span>
<?php else: ?>
<span class="badge badge-secondary">Disattivo</span>
<?php endif; ?>
</td>
<td>
<button type="button" class="btn btn-xs btn-info" onclick="editRepo(<?php echo e($repo->id); ?>)" title="Modifica">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-xs btn-success test-repo-btn" onclick="testRepo(<?php echo e($repo->id); ?>, this)" title="Test connessione">
<i class="fas fa-plug"></i>
</button>
<button type="button" class="btn btn-xs btn-danger" onclick="deleteRepo(<?php echo e($repo->id); ?>, '<?php echo e(addslashes($repo->nome)); ?>')" title="Elimina">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="text-center text-muted py-4">
<i class="fas fa-cloud fa-3x mb-3"></i>
<p class="mb-0">Nessun repository remoto configurato.</p>
<p class="mb-0">Clicca "Nuovo Repository" per aggiungere un server WebDAV o Google Drive.</p>
</div>
<?php endif; ?>
</div>
</div>
<?php if($repositories->count() > 0): ?>
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i> Come funziona</h3>
</div>
<div class="card-body">
<ul class="mb-0">
<li>Ogni repository configurato apparirà come <strong>root separata</strong> nella sidebar <strong>Gestione Documenti</strong>, sotto "Repository Remoti".</li>
<li>Puoi navigare i file e le cartelle di ogni repository remoto direttamente dall'interfaccia documenti.</li>
<li><strong>WebDAV</strong>: supporta server che parlano il protocollo WebDAV (es. NextCloud, ownCloud, Synology).</li>
<li><strong>Google Drive</strong>: richiede OAuth 2.0 con client ID, client secret e refresh token configurati nella <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>.</li>
<li>Usa il pulsante <i class="fas fa-plug text-success"></i> <strong>Test</strong> per verificare la connettività dopo la configurazione.</li>
</ul>
</div>
</div>
<?php endif; ?>
</div>
<div class="tab-pane" id="documenti">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Repository Documenti</h3>
</div>
<div class="card-body">
<p class="text-muted">Configura il repository filesystem dove vengono salvati i documenti caricati.</p>
<?php
$currentDisk = $appSettings->documenti_storage_disk ?? 'local';
$currentPath = $appSettings->documenti_storage_path ?? 'documenti';
$absPath = storage_path('app/' . ($currentDisk === 'local' ? 'private/' : '') . $currentPath);
?>
<form method="POST" action="<?php echo e(route('impostazioni.app-settings.save')); ?>">
<?php echo csrf_field(); ?>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="documenti_storage_disk">Disco di archiviazione</label>
<select name="documenti_storage_disk" id="documenti_storage_disk" class="form-control">
<option value="local" <?php echo e($currentDisk === 'local' ? 'selected' : ''); ?>>Locale (storage/app/private)</option>
<option value="public" <?php echo e($currentDisk === 'public' ? 'selected' : ''); ?>>Pubblico (storage/app/public)</option>
</select>
<small class="text-muted">Il disco predefinito per salvare i documenti</small>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="documenti_storage_path">Percorso relativo</label>
<input type="text" name="documenti_storage_path" id="documenti_storage_path" class="form-control"
value="<?php echo e($currentPath); ?>" placeholder="es. documenti">
<small class="text-muted">Sottocartella all'interno del disco</small>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle mr-2"></i>
<strong>Percorso assoluto calcolato:</strong>
<code><?php echo e($absPath); ?></code>
<br>
<small class="text-muted">I nuovi documenti verranno salvati in questa directory.</small>
</div>
<?php if(session('path_changed')): ?>
<?php
$migrateCount = session('migration_count', 0);
$oldD = session('old_storage_disk', 'local');
$oldP = session('old_storage_path', 'documenti');
$newD = session('new_storage_disk', 'local');
$newP = session('new_storage_path', 'documenti');
?>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong>Percorso modificato!</strong>
<p class="mb-2">La nuova directory <code><?php echo e($newD); ?>://<?php echo e($newP); ?></code> è stata creata.
<?php if($migrateCount > 0): ?>
Ci sono <strong><?php echo e($migrateCount); ?></strong> file nella vecchia posizione <code><?php echo e($oldD); ?>://<?php echo e($oldP); ?></code>.</p>
<form method="POST" action="<?php echo e(route('impostazioni.migrate-percorso')); ?>" class="d-inline" onsubmit="return confirm('Spostare <?php echo e($migrateCount); ?> file? Questa operazione potrebbe richiedere alcuni secondi.');">
<?php echo csrf_field(); ?>
<input type="hidden" name="old_storage_disk" value="<?php echo e($oldD); ?>">
<input type="hidden" name="old_storage_path" value="<?php echo e($oldP); ?>">
<input type="hidden" name="new_storage_disk" value="<?php echo e($newD); ?>">
<input type="hidden" name="new_storage_path" value="<?php echo e($newP); ?>">
<button type="submit" class="btn btn-warning">
<i class="fas fa-exchange-alt mr-1"></i> Sposta <?php echo e($migrateCount); ?> file nel nuovo percorso
</button>
<a href="/impostazioni#documenti" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Ignora
</a>
</form>
<?php else: ?>
<p class="mb-0">Nessun file da spostare.</p>
<?php endif; ?>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i> Salva Impostazioni
</button>
</form>
</div>
</div>
</div>
<div class="tab-pane" id="logo">
<div class="card card-primary">
<div class="card-header">
@@ -1433,7 +1622,254 @@ if (window.location.hash === '#email') {
setTimeout(initSignatureEditor, 300);
});
}
// ===== REPOSITORY REMOTI =====
let repositories = <?php echo json_encode($repositories, 15, 512) ?>;
function resetRepoForm() {
document.getElementById('repoForm').action = '<?php echo e(route('storage-repositories.store')); ?>';
document.getElementById('repoMethod').value = 'POST';
document.getElementById('repoModalTitle').textContent = 'Nuovo Repository Remoto';
document.getElementById('repoForm').reset();
document.getElementById('repoId').value = '';
document.getElementById('repo_is_active').checked = true;
toggleRepoConfigFields();
}
function toggleRepoConfigFields() {
const tipo = document.getElementById('repo_tipo').value;
document.getElementById('webdavFields').style.display = tipo === 'webdav' ? '' : 'none';
document.getElementById('googleDriveFields').style.display = tipo === 'google_drive' ? '' : 'none';
}
function editRepo(id) {
const repo = repositories.find(r => r.id === id);
if (!repo) return;
document.getElementById('repoForm').action = '<?php echo e(route('storage-repositories.update', '__ID__')); ?>'.replace('__ID__', id);
document.getElementById('repoMethod').value = 'PUT';
document.getElementById('repoModalTitle').textContent = 'Modifica Repository - ' + repo.nome;
document.getElementById('repoId').value = repo.id;
document.getElementById('repo_nome').value = repo.nome;
document.getElementById('repo_tipo').value = repo.tipo;
document.getElementById('repo_is_active').checked = repo.is_active;
const config = repo.config || {};
document.getElementById('webdav_base_uri').value = config.base_uri || '';
document.getElementById('webdav_username').value = config.username || '';
document.getElementById('webdav_password').value = '';
document.getElementById('webdav_password').placeholder = config.password ? '......' : '';
document.getElementById('webdav_root').value = config.root || '/';
document.getElementById('webdav_auth_type').value = config.auth_type || 'basic';
document.getElementById('gd_client_id').value = config.client_id || '';
document.getElementById('gd_client_secret').value = '';
document.getElementById('gd_client_secret').placeholder = config.client_secret ? '......' : '';
document.getElementById('gd_refresh_token').value = '';
document.getElementById('gd_refresh_token').placeholder = config.refresh_token ? '......' : '';
document.getElementById('gd_root_folder_id').value = config.root_folder_id || 'root';
toggleRepoConfigFields();
$('#repoModal').modal('show');
}
function deleteRepo(id, nome) {
if (!confirm('Eliminare il repository "' + nome + '"?')) return;
var url = '<?php echo e(route('storage-repositories.destroy', '__ID__')); ?>'.replace('__ID__', id);
fetch(url, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
'X-HTTP-Method-Override': 'DELETE'
}
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success) {
location.reload();
} else {
alert('❌ ' + (data.message || 'Errore'));
}
}).catch(function() {
alert('❌ Errore di rete');
});
}
function testRepo(id, btn) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Test...';
$.ajax({
url: '<?php echo e(route('storage-repositories.test', '__ID__')); ?>'.replace('__ID__', id),
method: 'POST',
headers: { 'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>' },
timeout: 30000,
success: function(response) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plug"></i>';
alert(response.success ? 'Connessione OK: ' + response.message : 'Errore: ' + response.message);
},
error: function(xhr) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plug"></i>';
const msg = xhr.responseJSON?.message || 'Errore di connessione';
alert('Errore: ' + msg);
}
});
}
$(document).ready(function() {
if (document.getElementById('repoSortable')) {
var sortableRepo = Sortable.create(document.getElementById('repoSortable'), {
handle: '.handle',
animation: 150,
onEnd: function(evt) {
var order = [];
$('#repoSortable tr').each(function() {
order.push($(this).data('id'));
});
fetch('<?php echo e(route('storage-repositories.reorder')); ?>', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
'Content-Type': 'application/json',
},
body: JSON.stringify({ order: order })
});
}
});
}
});
</script>
<div class="modal fade" id="repoModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="repoModalTitle"><i class="fas fa-cloud mr-2"></i>Nuovo Repository Remoto</h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<form id="repoForm" method="POST" action="<?php echo e(route('storage-repositories.store')); ?>">
<?php echo csrf_field(); ?>
<input type="hidden" name="_method" id="repoMethod" value="POST">
<input type="hidden" name="id" id="repoId" value="">
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="repo_nome">Nome *</label>
<input type="text" name="nome" id="repo_nome" class="form-control" placeholder="es. Server Parrocchia" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="repo_tipo">Tipo *</label>
<select name="tipo" id="repo_tipo" class="form-control" required onchange="toggleRepoConfigFields()">
<option value="webdav">WebDAV</option>
<option value="google_drive">Google Drive</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="repo_is_active" name="is_active" value="1" checked>
<label class="custom-control-label" for="repo_is_active">Repository attivo</label>
</div>
</div>
<hr>
<h6>Configurazione</h6>
<div id="webdavFields">
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="webdav_base_uri">URL Base WebDAV *</label>
<input type="url" name="config[base_uri]" id="webdav_base_uri" class="form-control" placeholder="es. https://nextcloud.parrocchia.it/remote.php/dav/files/utente/">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="webdav_root">Root path</label>
<input type="text" name="config[root]" id="webdav_root" class="form-control" value="/" placeholder="/">
<small class="text-muted">Percorso base nel server</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="webdav_username">Username</label>
<input type="text" name="config[username]" id="webdav_username" class="form-control">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="webdav_password">Password</label>
<input type="password" name="config[password]" id="webdav_password" class="form-control">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="webdav_auth_type">Auth Type</label>
<select name="config[auth_type]" id="webdav_auth_type" class="form-control">
<option value="basic">Basic</option>
<option value="digest">Digest</option>
<option value="ntlm">NTLM</option>
</select>
</div>
</div>
</div>
</div>
<div id="googleDriveFields" style="display:none;">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="gd_client_id">Client ID *</label>
<input type="text" name="config[client_id]" id="gd_client_id" class="form-control" placeholder="Google Client ID">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="gd_client_secret">Client Secret *</label>
<input type="text" name="config[client_secret]" id="gd_client_secret" class="form-control" placeholder="Google Client Secret">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="gd_refresh_token">Refresh Token *</label>
<input type="text" name="config[refresh_token]" id="gd_refresh_token" class="form-control" placeholder="OAuth Refresh Token">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="gd_root_folder_id">Root Folder ID</label>
<input type="text" name="config[root_folder_id]" id="gd_root_folder_id" class="form-control" value="root" placeholder="root">
<small class="text-muted">ID della cartella Google Drive, default: root</small>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle mr-2"></i>
Per ottenere le credenziali OAuth: <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>
&rarr; API &amp; Services &rarr; Credentials &rarr; Crea OAuth Client ID (Desktop App).
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i> Salva Repository
</button>
</div>
</form>
</div>
</div>
</div>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/impostazioni/index.blade.php ENDPATH**/ ?>
+155
View File
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Models\StorageRepository;
use App\Models\User;
use App\Services\StorageRepositoryService;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class StorageRepositoryTest extends TestCase
{
protected User $user;
protected function setUp(): void
{
parent::setUp();
Schema::create('users', function ($table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->boolean('is_admin')->default(false);
$table->text('permissions')->nullable();
$table->string('status')->nullable();
$table->unsignedBigInteger('role_preset_id')->nullable();
$table->timestamps();
});
Schema::create('storage_repositories', function ($table) {
$table->id();
$table->string('nome');
$table->string('tipo', 50);
$table->text('config');
$table->boolean('is_active')->default(true);
$table->integer('ordine')->default(0);
$table->timestamps();
});
Schema::create('documenti', function ($table) {
$table->id();
$table->foreignId('repository_id')->nullable()->constrained('storage_repositories')->nullOnDelete();
$table->timestamps();
});
$this->user = User::factory()->create(['is_admin' => true]);
$this->actingAs($this->user);
}
protected function tearDown(): void
{
Schema::dropIfExists('documenti');
Schema::dropIfExists('storage_repositories');
Schema::dropIfExists('users');
parent::tearDown();
}
public function test_can_create_webdav_repository(): void
{
$response = $this->post('/storage-repositories', [
'nome' => 'Test WebDAV',
'tipo' => 'webdav',
'config' => [
'base_uri' => 'https://example.com/webdav/',
'username' => 'testuser',
'password' => 'testpass',
'root' => '/',
],
'is_active' => true,
]);
$response->assertRedirect('/impostazioni#repository');
$response->assertSessionHas('success');
$this->assertDatabaseHas('storage_repositories', [
'nome' => 'Test WebDAV',
'tipo' => 'webdav',
]);
}
public function test_can_create_google_drive_repository(): void
{
$response = $this->post('/storage-repositories', [
'nome' => 'Test Google Drive',
'tipo' => 'google_drive',
'config' => [
'client_id' => 'test-client-id',
'client_secret' => 'test-client-secret',
'refresh_token' => 'test-refresh-token',
'root_folder_id' => 'root',
],
'is_active' => true,
]);
$response->assertRedirect('/impostazioni#repository');
$this->assertDatabaseHas('storage_repositories', [
'nome' => 'Test Google Drive',
'tipo' => 'google_drive',
]);
}
public function test_requires_nome_and_tipo(): void
{
$response = $this->post('/storage-repositories', [
'config' => ['base_uri' => 'https://example.com'],
]);
$response->assertSessionHasErrors(['nome', 'tipo']);
}
public function test_can_delete_repository_with_no_documents(): void
{
$repo = StorageRepository::create([
'nome' => 'To Delete',
'tipo' => 'webdav',
'config' => ['base_uri' => 'https://example.com'],
]);
$response = $this->delete("/storage-repositories/{$repo->id}");
$response->assertStatus(302);
$this->assertDatabaseMissing('storage_repositories', ['id' => $repo->id]);
}
public function test_tipo_labels(): void
{
$this->assertEquals('WebDAV', StorageRepository::etichettaTipo('webdav'));
$this->assertEquals('Google Drive', StorageRepository::etichettaTipo('google_drive'));
}
public function test_tipo_icons(): void
{
$this->assertEquals('fa-server', StorageRepository::iconaTipo('webdav'));
$this->assertEquals('fa-google-drive', StorageRepository::iconaTipo('google_drive'));
}
public function test_can_reorder_repositories(): void
{
$repo1 = StorageRepository::create(['nome' => 'A', 'tipo' => 'webdav', 'config' => [], 'ordine' => 0]);
$repo2 = StorageRepository::create(['nome' => 'B', 'tipo' => 'webdav', 'config' => [], 'ordine' => 1]);
$response = $this->post('/storage-repositories/reorder', [
'order' => [$repo2->id, $repo1->id],
]);
$response->assertJson(['success' => true]);
$this->assertEquals(0, $repo2->fresh()->ordine);
$this->assertEquals(1, $repo1->fresh()->ordine);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Models\StorageRepository;
use App\Services\StorageRepositoryService;
use Tests\TestCase;
class StorageRepositoryServiceTest extends TestCase
{
public function test_encrypts_webdav_password(): void
{
$service = new StorageRepositoryService();
$config = $service->encryptSensitiveConfig([
'base_uri' => 'https://example.com',
'username' => 'user',
'password' => 'my_secret_pass',
], 'webdav');
$this->assertNotEquals('my_secret_pass', $config['password']);
$this->assertEquals('https://example.com', $config['base_uri']);
}
public function test_encrypts_google_drive_secrets(): void
{
$service = new StorageRepositoryService();
$config = $service->encryptSensitiveConfig([
'client_id' => 'my-client-id',
'client_secret' => 'my-secret',
'refresh_token' => 'my-token',
'root_folder_id' => 'root',
], 'google_drive');
$this->assertNotEquals('my-secret', $config['client_secret']);
$this->assertNotEquals('my-token', $config['refresh_token']);
$this->assertEquals('my-client-id', $config['client_id']);
}
public function test_builds_null_filesystem_for_unknown_tipo(): void
{
$service = new StorageRepositoryService();
$repo = $this->createMock(StorageRepository::class);
$repo->method('getDecryptedConfig')->willReturn([]);
$repo->tipo = 'unknown';
$result = $service->buildFilesystem($repo);
$this->assertNull($result);
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "as247/cloud-storages",
"description": "Storages wrapper with path support for drive services: google drive, onedrive,...",
"keywords": ["php", "storage", "drive","google","google drive","one drive"],
"homepage": "https://github.com/as247",
"license": "MIT",
"authors": [
{
"name": "As247",
"email": "as247@vui360.com",
"homepage": "http://as247.vui360.com"
}
],
"require": {
"php": ">=7.1",
"ext-json": "*"
},
"autoload": {
"psr-4": {
"As247\\CloudStorages\\": "src/"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
+4
View File
@@ -0,0 +1,4 @@
###### This package used to support for google drive and onedrive package
- [as247/flysystem-google-drive](https://packagist.org/packages/as247/flysystem-google-drive)
- [as247/flysystem-onedrive](https://packagist.org/packages/as247/flysystem-onedrive)
- [as247/flysystem-alist](https://packagist.org/packages/as247/flysystem-alist)
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace As247\CloudStorages\Cache;
use As247\CloudStorages\Cache\Stores\ArrayStore;
use As247\CloudStorages\Cache\Stores\GoogleDriveStore;
use As247\CloudStorages\Contracts\Cache\PathStore;
/**
* Class PathCache
* @package As247\CloudStorages\Cache
* @mixin PathStore
* @mixin GoogleDriveStore
* @mixin ArrayStore
*/
class PathCache
{
protected $store;
public function __construct(PathStore $store=null)
{
if($store==null) {
$store=new ArrayStore();
}
$this->store = $store;
}
/**
* @return ArrayStore|GoogleDriveStore|PathStore|null
*/
public function getStore(){
return $this->store;
}
public function __call($name, $arguments)
{
return $this->store->$name(...$arguments);
}
}
@@ -0,0 +1,162 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Contracts\Cache\PathStore;
use As247\CloudStorages\Support\Path;
use RuntimeException;
class ArrayStore implements PathStore
{
protected $files = [];
protected $completed = [];
public function query($path, $deep = 1)
{
$directory = Path::clean($path);
$results = [];
$dirSegCount = Path::countSegments($directory);
foreach ($this->files as $path => $file) {
if (strpos($path, $directory) === 0) {
if ($deep>0) {
if ($path!==$directory && (Path::countSegments($path) - $dirSegCount <= $deep)) {
$results[$path] = $file;
}
}else{
$results[$path] = $file;
}
}
}
return $results;
}
public function get($key)
{
$key = Path::clean($key);
return $this->files[$key] ?? null;
}
public function put($key, $data, $seconds = 3600)
{
$key = Path::clean($key);
if($data===null){
unset($this->files[$key]);
$this->complete($key,false);
}else {
$this->files[$key] = $data;
}
}
public function flush()
{
$root = $this->get('/');
$this->files = [];
$this->put('/', $root);
$this->completed = [];
}
public function forget($path)
{
$this->put($path,null,-1);
}
public function forever($key, $value)
{
$this->put($key,$value,315360000);
}
public function forgetBranch($path)
{
$tmpPath = Path::clean($path);
do {
if(!$this->get($tmpPath)){
$this->forget($tmpPath);//Forget false item only
}
$this->complete($tmpPath,false);
}while(($tmpPath = Path::clean(dirname($tmpPath))) && $tmpPath !== '/');
}
public function delete($path)
{
$this->put($path,false);
}
public function deleteBranch($path)
{
$tmpPath = Path::clean($path);
do{
$this->put($tmpPath, false);
}
while (($tmpPath = Path::clean(dirname($tmpPath))) && $tmpPath !== '/');
}
public function forgetDir($path)
{
$path=Path::clean($path);
foreach ($this->query($path,0) as $key => $file) {
$this->forget($key);
}
$this->complete($path,false);
}
public function deleteDir($path)
{
$path=Path::clean($path);
foreach ($this->query($path,0) as $key => $file) {
$this->put($key, false);
}
$this->complete($path,false);
}
function move($from,$to)
{
if(!$from || !$to){
throw new RuntimeException("Invalid path $from -> $to");
}
$from=Path::clean($from);
$to=Path::clean($to);
//Destination tree changed we should clean up all parent
//This need for onedrive because we not keep track of parents in cache
$this->forgetBranch($to);
foreach ($this->query($from,0) as $path => $file ){
$newPath=Path::replace($from, $to, $path);
if($path!==$newPath){
$this->put($newPath,$file);
$this->put($path,false);
}
}
foreach ($this->getCompleted($from) as $key => $value) {
$newKey = Path::replace($from, $to, $key);
if ($newKey !== $key) {
$this->complete($newKey,$value);
$this->complete($key,false);
}
}
}
public function getCompleted($path){
return $this->completed;
}
public function complete($path, $isCompleted = true)
{
$path = Path::clean($path);
if($isCompleted) {
$this->completed[$path] = $isCompleted;
}else{
$path=Path::clean($path);
foreach ($this->getCompleted($path) as $key => $value) {
if (strpos($key, $path) === 0) {
unset($this->completed[$key]);
}
}
}
}
public function isCompleted($path)
{
$path = Path::clean($path);
return $this->completed[$path] ?? false;
}
}
@@ -0,0 +1,73 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Support\Path;
class GoogleDrivePersistentStore extends GoogleDriveStore
{
protected $sqliteCache;
public function __construct($cacheFile)
{
$this->sqliteCache=new SqliteCache($cacheFile);
}
public function put($key, $data, $seconds = 3600)
{
$key=Path::clean($key);
if(!$data){
//parent::put($key,$data);//Keep false item in object cache
if(is_null($data)){
$this->sqliteCache->forget($key);
$this->complete($key,false);
}else {
$this->sqliteCache->put($key, $data);
}
}else{
//parent::put($key,null);
$this->sqliteCache->forever($key,$data);
}
}
public function get($key)
{
if(null!==($parentValue=parent::get($key))){
return $parentValue;
}
$key=Path::clean($key);
$cache= $this->sqliteCache->get($key);
return $cache;
}
function query($path, $deep = 1)
{
$path=Path::clean($path);
$directory=$path;
if($deep>=1) {
//Like /dirname/% and not like /dirname/%/%
$directory = $path === '/' ? $path : $path . '/';
}//Like /dirname%
$list=$this->sqliteCache->pathQuery($directory,$deep);
if($deep===0){
return $list;
}
unset($list[$path]);
return $list;
}
public function complete($path, $isCompleted = true)
{
$path=Path::clean($path);
return $this->sqliteCache->complete($path,$isCompleted);
}
public function isCompleted($path)
{
$path=Path::clean($path);
return $this->sqliteCache->isCompleted($path); // TODO: Change the autogenerated stub
}
public function getCompleted($path)
{
$path=Path::clean($path);
return $this->sqliteCache->getCompleted($path); // TODO: Change the autogenerated stub
}
}
@@ -0,0 +1,32 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Service\GoogleDrive;
use As247\CloudStorages\Support\Path;
use Google_Service_Drive_DriveFile;
class GoogleDriveStore extends ArrayStore
{
function mapDirectory($path,$id){
return $this->mapFile($path,$id,GoogleDrive::DIR_MIME);
}
function mapFile($path,$id,$mimeType=''){
$path=Path::clean($path);
if(!$id){
$this->forget($path);
return $this;
}
$file=$id;
if(!$file instanceof Google_Service_Drive_DriveFile){
$file = new Google_Service_Drive_DriveFile();
$file->setId($id);
$file->setMimeType($mimeType);
$file->setPermissions([]);
}
$this->forever($path,$file);
return $this;
}
}
@@ -0,0 +1,86 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Contracts\Cache\PathStore;
class NullStore implements PathStore
{
public function forgetBranch($path)
{
// TODO: Implement forgetBranch() method.
}
public function delete($path)
{
// TODO: Implement delete() method.
}
public function deleteBranch($path)
{
// TODO: Implement deleteBranch() method.
}
public function forgetDir($path)
{
// TODO: Implement forgetDir() method.
}
public function deleteDir($path)
{
// TODO: Implement deleteDir() method.
}
public function move($source, $destination)
{
// TODO: Implement move() method.
}
public function query($path, $match = '*')
{
// TODO: Implement query() method.
}
public function complete($path, $isCompleted = true)
{
// TODO: Implement complete() method.
}
public function isCompleted($path)
{
// TODO: Implement isCompleted() method.
}
public function getCompleted($path)
{
// TODO: Implement getCompleted() method.
}
public function put($path, $data, $seconds = 3600)
{
// TODO: Implement put() method.
}
public function forever($path, $value)
{
// TODO: Implement forever() method.
}
public function get($path)
{
// TODO: Implement get() method.
}
public function forget($path)
{
// TODO: Implement forget() method.
}
public function flush()
{
// TODO: Implement flush() method.
}
}
@@ -0,0 +1,240 @@
<?php
namespace As247\CloudStorages\Cache\Stores;
use As247\CloudStorages\Contracts\Cache\Store;
use Exception;
use PDO;
class SqliteCache implements Store
{
protected $pdo;
/**
* SqliteCache constructor.
* @param null $dataFile
* @throws Exception
*/
public function __construct($dataFile=null)
{
if($dataFile===null){
$dataFile=sys_get_temp_dir().'/'.md5(static::class).'';
}
$isNewDB=!file_exists($dataFile);
$this->pdo=new PDO('sqlite:' . $dataFile);
if($isNewDB) {
$this->createTable();
}else{
$this->checkMalformed();
}
}
/**
* @throws Exception
*/
protected function checkMalformed(){
$this->pdo->prepare("select 1 from cache where 0=1");
$error = $this->pdo->errorInfo();
if($error[0] !=='00000'){
throw new Exception(sprintf("SQLSTATE[%s]: Error [%s] %s",$error[0],$error[1],$error[2]));
}
}
protected function createTable(){
$this->pdo->query("
CREATE TABLE IF NOT EXISTS `cache` (
`key` varchar(500) not null,
`value` text not null,
`expiration` integer(11),
PRIMARY KEY (`key`)
)
");
$this->pdo->query("
CREATE TABLE IF NOT EXISTS `completed` (
`path` varchar(500) not null,
`expiration` integer(11),
PRIMARY KEY (`path`)
)
");
}
public function get($key){
$statement=$this->pdo->prepare("SELECT * FROM cache WHERE key=? limit 1");
$statement->bindValue(1,$key);
$statement->execute();
$cache=$statement->fetch(PDO::FETCH_OBJ);
if(!$cache){
return null;
}
if ($this->currentTime() >= $cache->expiration) {
$this->forget($key);
return null;
}
return unserialize($cache->value);
}
public function put($key, $value, $seconds=3600){
$value=serialize($value);
$statement=$this->pdo->prepare(
"insert into cache (`key`,`value`,`expiration`) values (?,?,?)"
);
if($seconds===-1){
$expire=2147483647;
}else{
$expire=$this->currentTime()+$seconds;
}
$statement->bindValue(1,$key);
$statement->bindValue(2,$value);
$statement->bindValue(3,$expire);
if(!$statement->execute()){
$statement=$this->pdo->prepare("UPDATE cache SET value=:value,expiration=:expiration WHERE key=:key");
$statement->bindValue(':key',$key);
$statement->bindValue(':value',$value);
$statement->bindValue(':expiration',$expire);
return $statement->execute();
}else{
return true;
}
}
public function forget($key){
$statement=$this->pdo->prepare("DELETE FROM cache WHERE key=?");
$statement->bindValue(1,$key);
$statement->execute();
return true;
}
/**
* Store an item in the cache indefinitely.
*
* @param string $path
* @param mixed $id
* @return bool
*/
public function forever($path, $value)
{
return $this->put($path, $value, -1);
}
public function getCompleted($key=''){
if($key) {
$statement = $this->pdo->prepare("SELECT * FROM completed WHERE path like ?");
$statement->bindValue(1, $key . '%');
}else{
$statement = $this->pdo->prepare("SELECT * FROM completed");
}
$statement->execute();
$allRecords=$statement->fetchAll(PDO::FETCH_OBJ);
if(!$allRecords){
return [];
}
$results=[];
foreach ($allRecords as $cache) {
if ($this->currentTime() >= $cache->expiration) {
$this->forget($key);
}else{
$results[$cache->path]=true;
}
}
return $results;
}
public function isCompleted($key){
$statement=$this->pdo->prepare("SELECT * FROM completed WHERE path=? limit 1");
$statement->bindValue(1,$key);
$statement->execute();
$cache=$statement->fetch(PDO::FETCH_OBJ);
if(!$cache){
return false;
}
if ($this->currentTime() >= $cache->expiration) {
$this->complete($key,false);
return false;
}
return true;
}
public function complete($key, $completed=true, $seconds=3600){
if($completed){
$statement=$this->pdo->prepare(
"insert into completed (`path`,`expiration`) values (?,?)"
);
if($seconds===-1){
$expire=2147483647;
}else{
$expire=$this->currentTime()+$seconds;
}
$statement->bindValue(1,$key);
$statement->bindValue(2,$expire);
if(!$statement->execute()){
$statement=$this->pdo->prepare("UPDATE completed SET expiration=:expiration WHERE path=:key");
$statement->bindValue(':key',$key);
$statement->bindValue(':expiration',$expire);
return $statement->execute();
}else{
return true;
}
}else{
return $this->forgetComplete($key);
}
}
protected function forgetComplete($key){
$key.='%';
$statement=$this->pdo->prepare("DELETE FROM completed WHERE path like ?");
$statement->bindValue(1,$key);
return $statement->execute();
}
public function flush(){
$statement=$this->pdo->prepare("DELETE FROM cache");
$statement->execute();
return true;
}
public function clearExpires(){
$statement1=$this->pdo->prepare("DELETE FROM cache WHERE expiration < ?");
$statement2=$this->pdo->prepare("DELETE FROM completed WHERE expiration < ?");
$statement1->bindValue(1,$this->currentTime());
$statement2->bindValue(1,$this->currentTime());
$statement1->execute();
$statement2->execute();
}
public function getPdo(){
return $this->pdo;
}
public function pathQuery($key, $deep=1){
$like=$key.'%';
$notLike='';
if($deep>0){
$notLike=$like;
while($deep-->0){
$notLike.='/%';
}
}
if($notLike){
$statement=$this->pdo->prepare("SELECT * FROM cache WHERE key like ? and key not like ?");
$statement->bindValue(2,$notLike);
}else{
$statement=$this->pdo->prepare("SELECT * FROM cache WHERE key like ?");
}
$statement->bindValue(1,$like);
//$statement->bindValue(2,$notLike);
$statement->execute();
$allRecords=$statement->fetchAll(PDO::FETCH_OBJ);
if(!$allRecords){
return [];
}
$results=[];
foreach ($allRecords as $cache) {
if ($this->currentTime() >= $cache->expiration) {
$this->forget($key);
}else{
$results[$cache->key]=unserialize($cache->value);
}
}
return $results;
}
/**
* Get the current system time as a UNIX timestamp.
*
* @return int
*/
protected function currentTime()
{
return time();
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace As247\CloudStorages\Cache;
use RuntimeException;
class TempCache
{
protected $cacheDir;
function __construct($key)
{
$name=md5(serialize(func_get_args()));
$this->cacheDir=sys_get_temp_dir().'/'.$name;
}
function get($key){
return $this->getPayload($key)['data'] ?? null;
}
function put($key,$value,$expires=3600){
$path=$this->path($key);
if($this->ensureCacheDir()) {
file_put_contents($path,
serialize([
'data' => $value,
'expires' => $expires,
'created'=>time(),
]));
}
}
protected function ensureCacheDir(){
if(is_dir($this->cacheDir)){
return true;
}
@$created=mkdir($this->cacheDir, 0777, true);
if(!$created){
@unlink($this->cacheDir);//It may be a file
@$created=mkdir($this->cacheDir, 0777, true);
}
if(!$created){
throw new RuntimeException('Could not create directory '.$this->cacheDir);
}
return $created;
}
/**
* Retrieve an item and expiry time from the cache by key.
*
* @param string $key
* @return array
*/
protected function getPayload(string $key)
{
$path = $this->path($key);
$payload=[];
if(file_exists($path) && is_file($path)){
$content=file_get_contents($path);
if($content) {
$payload = unserialize($content);
if(!isset($payload['data']) || !isset($payload['expires']) || !isset($payload['created'])){
return [];
}
if($payload['expires']> 0 && $payload['created']+$payload['expires'] < time()){
return [];
}
}
}
return $payload;
}
protected function path($key){
$key=md5($key);
return $this->cacheDir.'/'.$key;
}
public function has($key)
{
return !empty($this->getPayload($key));
}
public function forget($key)
{
$path=$this->path($key);
@unlink($path);
}
public function forever($key, $value)
{
$this->put($key,$value,0);
}
public function flush()
{
if(!is_dir($this->cacheDir)){
return ;
}
if ($dh = opendir($this->cacheDir)) {
while (($file = readdir($dh)) !== false) {
if($file!=='.' && $file !=='..'){
unlink($this->cacheDir.'/'.$file);
}
}
closedir($dh);
}
}
}
@@ -0,0 +1,79 @@
<?php
namespace As247\CloudStorages\Contracts\Cache;
interface PathStore extends Store
{
/**
* Forget path and its parent
* @param $path
* @return mixed
*/
public function forgetBranch($path);
/**
* Set false value for $path
* @param $path
* @return mixed
*/
public function delete($path);
/**
* Delete path and all its parents
* @param $path
* @return mixed
*/
public function deleteBranch($path);
/**
* Forget a path and all its children
* eg if forget /a then /a/b /a/b/c also removed
* @param $path
* @return mixed
*/
public function forgetDir($path);
/**
* Set false value for $path and all existing children
* Eg: If deleteDir('/a') is called and '/a/b','/a/c/e.txt' exists in cache
* Then all of them set to false
* @param $path
* @return mixed
*/
public function deleteDir($path);
/**
* Simulate rename function, we need to move all value from $source tree to $destination
*
* @param $source
* @param $destination
* @return mixed
*/
public function move($source, $destination);
/**
* Query for matching path
* @param $path
* @param string|int $match * content in current directory ** include subdirectory
* @return mixed
*/
public function query($path, $match = '*');
/**
* Mark the path is completed that mean nothing under this path is outside cache, used for listing
* @param $path
* @param bool $isCompleted
* @return mixed
*/
public function complete($path, $isCompleted = true);
/**
* Check if current path is completed
* @param $path
* @return mixed
*/
public function isCompleted($path);
public function getCompleted($path);
}
@@ -0,0 +1,47 @@
<?php
namespace As247\CloudStorages\Contracts\Cache;
interface Store
{
/**
* Put value to cache, update if existing
* @param $path
* @param $data
* @param int $seconds
* @return mixed
*/
public function put($path, $data, $seconds=3600);
/**
* Never expire cache
* @param $path
* @param $value
* @return mixed
*/
public function forever($path, $value);
/**
* Get or return default
* @param $path
* @return mixed
*/
public function get($path);
/**
* For get a path
* @param $path
* @return mixed
*/
public function forget($path);
/**
* Flush cache
* @return mixed
*/
public function flush();
}
@@ -0,0 +1,44 @@
<?php
namespace As247\CloudStorages\Contracts\Storage;
use As247\CloudStorages\Exception\FileNotFoundException;
interface ObjectStorage
{
/**
* @param string $urn the unified resource name used to identify the object
* @return resource stream with the read data
* @throws \Exception when something goes wrong, message will be logged
* @throws FileNotFoundException if file does not exist
* @since 1.0.15
*/
public function readObject($urn);
/**
* @param string $urn the unified resource name used to identify the object
* @param resource $stream stream with the data to write
* @throws \Exception when something goes wrong, message will be logged
* @since 1.0.15
*/
public function writeObject($urn, $stream);
/**
* @param string $urn the unified resource name used to identify the object
* @return void
* @throws \Exception when something goes wrong, message will be logged
* @since 1.0.15
*/
public function deleteObject($urn);
/**
* Check if an object exists in the object store
*
* @param string $urn
* @return bool
* @since 1.0.15
*/
public function objectExists($urn);
}
@@ -0,0 +1,129 @@
<?php
namespace As247\CloudStorages\Contracts\Storage;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\InvalidVisibilityProvided;
use As247\CloudStorages\Exception\UnableToCopyFile;
use As247\CloudStorages\Exception\UnableToCreateDirectory;
use As247\CloudStorages\Exception\UnableToDeleteDirectory;
use As247\CloudStorages\Exception\UnableToDeleteFile;
use As247\CloudStorages\Exception\UnableToMoveFile;
use As247\CloudStorages\Exception\UnableToReadFile;
use As247\CloudStorages\Exception\UnableToRetrieveMetadata;
use As247\CloudStorages\Exception\UnableToWriteFile;
use As247\CloudStorages\Service\GoogleDrive;
use As247\CloudStorages\Service\OneDrive;
use As247\CloudStorages\Support\FileAttributes;
use As247\CloudStorages\Support\StorageAttributes;
use As247\CloudStorages\Support\Config;
use As247\CloudStorages\Exception\FilesystemException;
use Traversable;
interface StorageContract
{
/**
* @const VISIBILITY_PUBLIC public visibility
*/
const VISIBILITY_PUBLIC = 'public';
/**
* @const VISIBILITY_PRIVATE private visibility
*/
const VISIBILITY_PRIVATE = 'private';
/**
* @return mixed | GoogleDrive | OneDrive
*/
public function getService();
/**
* @param string $path
* @param $contents
* @param Config|null $config
* @throws UnableToWriteFile
* @throws FilesystemException
*/
public function writeStream(string $path, $contents, Config $config=null): void;
/**
* @param string $path
* @return resource
* @throws UnableToReadFile
* @throws FileNotFoundException
* @throws FilesystemException
*/
public function readStream(string $path);
/**
* @param string $path
* @throws UnableToDeleteFile
* @throws FilesystemException
* @throws FileNotFoundException
*/
public function delete(string $path): void;
/**
* @param string $path
* @throws UnableToDeleteDirectory
* @throws FilesystemException
* @throws FileNotFoundException
*/
public function deleteDirectory(string $path): void;
/**
* @param string $path
* @param Config|null $config
* @throws UnableToCreateDirectory
* @throws FilesystemException
*/
public function createDirectory(string $path, Config $config=null): void;
/**
* @param string $path
* @param mixed $visibility
* @throws InvalidVisibilityProvided
* @throws FilesystemException
*/
public function setVisibility(string $path, $visibility): void;
/**
* @param string $path
* @param bool $deep
* @return Traversable<StorageAttributes>
* @throws FilesystemException
*/
public function listContents(string $path, bool $deep): Traversable;
/**
* @param string $source
* @param string $destination
* @param Config|null $config
* @throws UnableToMoveFile
* @throws FilesystemException
*/
public function move(string $source, string $destination, Config $config=null): void;
/**
* @param string $source
* @param string $destination
* @param Config|null $config
* @throws UnableToCopyFile
* @throws FilesystemException
*/
public function copy(string $source, string $destination, Config $config=null): void;
/**
* @param $path
* @return FileAttributes
* @throws FileNotFoundException
* @throws UnableToRetrieveMetadata
* @throws FilesystemException
*/
public function getMetadata($path): FileAttributes;
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config=null): string;
}
@@ -0,0 +1,57 @@
<?php
namespace As247\CloudStorages\Controllers;
use Exception;
use Google_Client;
use Google_Service_Drive;
class GoogleDriveController
{
protected $client;
public function __construct($clientId,$clientSecret)
{
$this->client=new Google_Client();
$this->client->setClientId($clientId);
$this->client->setClientSecret($clientSecret);
$this->client->addScope(Google_Service_Drive::DRIVE);
$this->client->setAccessType('offline');
$this->client->setApprovalPrompt("force");
$this->client->setRedirectUri($this->getCurrentUrl());
}
public function dispatch(){
if($code=$this->getCode()){
try{
$result=$this->client->fetchAccessTokenWithAuthCode($code);
$refreshToken=$result['refresh_token'];
}catch (Exception $e){
$refreshToken=$e->getMessage();
}
$this->showRefreshToken($refreshToken);
}else{
$this->redirectTo($this->client->createAuthUrl());
}
}
protected function redirectTo($url){
$redirect='<html lang="en">
<head>
<meta http-equiv="refresh" content="1; url=%1$s">
<title>Redirecting....</title>
</head>
<body>Redirecting to %1$s...</body>
</html>';
printf($redirect,$url);
}
protected function showRefreshToken($refreshToken){
echo '<textarea cols="100" rows="20">', htmlspecialchars($refreshToken,ENT_QUOTES) . '</textarea>';
}
protected function getCode(){
return $_REQUEST['code']??null;
}
protected function getCurrentUrl()
{
return 'http://' . $_SERVER['HTTP_HOST'];
}
}
@@ -0,0 +1,52 @@
<?php
namespace As247\CloudStorages\Controllers;
use As247\CloudStorages\Support\OneDriveOauth;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
class OneDriveController
{
protected $oauth;
public function __construct($clientId,$clientSecret,$tenantId='common')
{
$this->oauth=new OneDriveOauth();
$this->oauth->setClientId($clientId);
$this->oauth->setClientSecret($clientSecret);
$this->oauth->setTenantId($tenantId);
}
public function dispatch(){
if($code=$this->getCode()){
try{
$result=$this->oauth->fetchAccessTokenWithAuthCode($code);
$refreshToken=$result['refresh_token'];
}catch (Exception $e){
$refreshToken=$e->getMessage();
} catch (GuzzleException $e) {
$refreshToken=$e->getMessage();
}
$this->showRefreshToken($refreshToken);
}else{
$this->redirectTo($this->oauth->createAuthUrl());
}
}
protected function redirectTo($url){
$redirect='<html lang="en">
<head>
<meta http-equiv="refresh" content="1; url=%1$s">
<title>Redirecting....</title>
</head>
<body>Redirecting to %1$s...</body>
</html>';
printf($redirect,$url);
}
protected function showRefreshToken($refreshToken){
echo '<textarea cols="100" rows="20">', htmlspecialchars($refreshToken,ENT_QUOTES) . '</textarea>';
}
protected function getCode(){
return $_REQUEST['code']??null;
}
}
@@ -0,0 +1,13 @@
<?php
namespace As247\CloudStorages\Exception;
abstract class AbstractException extends \RuntimeException
{
public $type;
public function __construct($message = "", $type='', \Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
$this->type=$type;
}
}
@@ -0,0 +1,40 @@
<?php
namespace As247\CloudStorages\Exception;
use Throwable;
class FileNotFoundException extends AbstractException
{
/**
* @var string
*/
protected $path;
/**
* Constructor.
*
* @param string $path
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $path, $code = 0, Throwable $previous = null)
{
$this->path = $path;
parent::__construct('File not found at path: ' . $this->getPath(), $code, $previous);
}
/**
* Get the path which was not found.
*
* @return string
*/
public function getPath()
{
return $this->path;
}
public static function create($path){
return new static($path);
}
}
@@ -0,0 +1,8 @@
<?php
namespace As247\CloudStorages\Exception;
class StorageException extends AbstractException
{
}
File diff suppressed because it is too large Load Diff
+535
View File
@@ -0,0 +1,535 @@
<?php
namespace As247\CloudStorages\Service;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Support\StorageAttributes;
use Google\Auth\HttpHandler\Guzzle5HttpHandler;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google_Http_MediaFileUpload;
use Google_Service_Drive;
use Google_Service_Drive_Permission;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Stream;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use Psr\Http\Message\RequestInterface;
use Google_Service_Drive_FileList;
use Google_Service_Drive_DriveFile;
use Psr\Http\Message\StreamInterface;
class GoogleDrive
{
/**
* MIME type of directory
*
* @var string
*/
const DIR_MIME = 'application/vnd.google-apps.folder';
/**
* Default options
*
* @var array
*/
protected static $defaultOptions = [
'additionalFetchField' => '',
'publishPermission' => [
'type' => 'anyone',
'role' => 'reader',
'withLink' => true
],
'appsExportMap' => [
'application/vnd.google-apps.document' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.google-apps.spreadsheet' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.google-apps.drawing' => 'application/pdf',
'application/vnd.google-apps.presentation' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.google-apps.script' => 'application/vnd.google-apps.script+json',
'default' => 'application/pdf'
],
// Default parameters for each command
// see https://developers.google.com/drive/v3/reference/files
// ex. 'defaultParams' => ['files.list' => ['includeTeamDriveItems' => true]]
'defaultParams' => [
'files.list'=>[
'corpora'=>'user'// default is user
],
],
'teamDrive' => false,
'useTrash' => true,
];
protected $options;
protected $publishPermission;
/**
* Fetch fields setting for get
*
* @var string
*/
protected $fetchFieldsGet='id,name,mimeType,modifiedTime,parents,permissions,size,webContentLink,webViewLink';
protected $fetchFieldsList='files({{fieldsGet}}),nextPageToken';
protected $additionalFields;
protected $defaultParams;
protected $service;
//Add prefix / when original root has /
protected $rootPrefix='';
use HasLogger;
public function __construct(Google_Service_Drive $service,$options=[])
{
$this->service=$service;
$this->setupLogger($options);
$this->options = array_replace_recursive(static::$defaultOptions, $options);
$this->publishPermission = $this->options['publishPermission'];
if ($this->options['additionalFetchField']) {
$this->fetchFieldsGet .= ',' . $this->options['additionalFetchField'];
$this->additionalFields = explode(',', $this->options['additionalFetchField']);
}
$this->fetchFieldsList = str_replace('{{fieldsGet}}', $this->fetchFieldsGet, $this->fetchFieldsList);
if (isset($this->options['defaultParams'])) {
$this->defaultParams = $this->options['defaultParams'];
}
if ($this->options['teamDrive']) {
if(is_string($this->options['teamDrive'])){
$this->options['teamDrive']=[
'driveId'=>$this->options['teamDrive'],
'corpora'=>'drive',
'includeItemsFromAllDrives'=>true,
];
}
$this->enableTeamDriveSupport();
}
$root=$options['prefix']??'';
if($root){
$firstChar=substr($root,0,1);
if($firstChar==='/' || $firstChar==='\\'){
$this->rootPrefix=$firstChar;
}
}
}
public function isTeamDrive(){
return $this->options['teamDrive'];
}
public function getTeamDriveId(){
return $this->options['teamDrive']['driveId']??null;
}
public function getClient(){
return $this->service->getClient();
}
public function normalizeMetadata(Google_Service_Drive_DriveFile $object, $path)
{
$id = $object->getId();
$result = [
StorageAttributes::ATTRIBUTE_PATH => is_string($path) ? $this->rootPrefix.ltrim($path,'\/'):null,
StorageAttributes::ATTRIBUTE_TYPE => $object->mimeType === self::DIR_MIME ? StorageAttributes::TYPE_DIRECTORY : StorageAttributes::TYPE_FILE,
StorageAttributes::ATTRIBUTE_LAST_MODIFIED=>strtotime($object->getModifiedTime())
];
$result[StorageAttributes::ATTRIBUTE_MIME_TYPE] = $object->getMimeType();
$result[StorageAttributes::ATTRIBUTE_FILE_SIZE] = (int) $object->getSize();
$result[StorageAttributes::ATTRIBUTE_VISIBILITY]=$this->getVisibility($object);
// attach additional fields
if ($this->additionalFields) {
foreach($this->additionalFields as $field) {
if (property_exists($object, $field)) {
$result['@'.$field] = $object->$field;
}
}
}
$result['@id']=$id;
$result['@shareLink']=$result['@link']=$object->getWebViewLink();
$result['@downloadUrl']=$object->getWebContentLink();
return $result;
}
protected function getVisibility(Google_Service_Drive_DriveFile $object){
$permissions = $object->getPermissions();
$visibility = StorageContract::VISIBILITY_PRIVATE;
foreach ($permissions as $permission) {
if ($permission->type === $this->publishPermission['type'] && $permission->role === $this->publishPermission['role']) {
$visibility = StorageContract::VISIBILITY_PUBLIC;
break;
}
}
return $visibility;
}
/**
* Enables Team Drive support by changing default parameters
*
* @return void
*
* @see https://developers.google.com/drive/v3/reference/files
* @see \Google_Service_Drive_Resource_Files
*/
public function enableTeamDriveSupport()
{
$this->defaultParams = array_merge_recursive(
array_fill_keys([
'files.copy', 'files.create', 'files.delete',
'files.trash', 'files.get', 'files.list', 'files.update',
'files.watch',
'files.permission.create',
'files.permission.delete'
], ['supportsAllDrives' => true]),
$this->defaultParams
);
$this->mergeCommandDefaultParams('files.list',$this->options['teamDrive']);
}
protected function getParams($cmd, ...$params){
$default=$this->getDefaultParams($cmd);
return array_replace($default,...$params);
}
protected function getDefaultParams($cmd){
if(isset($this->defaultParams[$cmd]) && is_array($this->defaultParams[$cmd])){
return $this->defaultParams[$cmd];
}
return [];
}
protected function mergeCommandDefaultParams($cmd,$params){
if(!isset($this->defaultParams[$cmd])){
$this->defaultParams[$cmd]=[];
}
$this->defaultParams[$cmd]=array_replace_recursive($this->defaultParams[$cmd],$params);
return $this;
}
/**
* Create directory
* @param $name
* @param $parentId
* @return bool|Google_Service_Drive_DriveFile|RequestInterface
*/
public function dirCreate($name, $parentId){
$file = new Google_Service_Drive_DriveFile();
$file->setName($name);
$file->setParents([
$parentId
]);
$file->setMimeType(self::DIR_MIME);
return $this->filesCreate($file);
}
/**
* Find files by name in given directory
* @param $name
* @param $parent
* @param $mineType
* @return Google_Service_Drive_FileList|Google_Service_Drive_DriveFile[]
*/
public function filesFindByName($name,$parent, $mineType=null){
if($parent instanceof Google_Service_Drive_DriveFile){
$parent=$parent->getId();
}
$timerStart=microtime(true);
$client=$this->service->getClient();
$collectOthers=false;
$q='trashed = false and "%s" in parents and name = "%s"';
$argsMatchName = [
'pageSize' => 2,
'q' =>sprintf($q,$parent,$name,static::DIR_MIME),
];
if($collectOthers) {
$client->setUseBatch(true);
$batch = $this->service->createBatch();
$filesMatchedName=$this->filesListFiles($argsMatchName);
$q='trashed = false and "%s" in parents';
if($mineType){
$q.=" and mimeType ".$mineType;
}
$argsOthers = [
'pageSize' => 50,
'q' =>sprintf($q,$parent,$name,static::DIR_MIME),
];
$otherFiles=$this->filesListFiles($argsOthers);
$batch->add($filesMatchedName,'matched');
$batch->add($otherFiles,'others');
$results = $batch->execute();
$files=[];
$isFullResult=empty($mineType);//if limited to a mime type so it is not full result
if(!isset($results['response-others'])){
$isFullResult=false;
}
foreach ($results as $key => $result) {
if ($result instanceof Google_Service_Drive_FileList) {
if($key==='response-matched'){
if(count($result)>1){
throw new StorageException("Duplicated file ".$name.' in '.$parent);
}
}
foreach ($result as $file) {
if (!isset($files[$file->id])) {
$files[$file->id] = $file;
}
}
if ($key === 'response-others' && $result->nextPageToken) {
$isFullResult = false;
}
}
}
$client->setUseBatch(false);
}else{
$isFullResult=false;
$list=$this->filesListFiles($argsMatchName);
$files=$list->getFiles();
}
$this->logRequest('files.list.by_name',[
'query'=>'find for '.$name.' in '.$parent,
'duration'=>microtime(true)-$timerStart]);
$list=new Google_Service_Drive_FileList();
$list->setFiles($files);
return [$list,$isFullResult];
}
/**
* @param array $optParams
* @return Google_Service_Drive_FileList | RequestInterface
*/
public function filesListFiles($optParams = array()){
$timerStart=microtime(true);
$optParams=$this->getParams('files.list',['fields' => $this->fetchFieldsList],$optParams);
$result= $this->service->files->listFiles($optParams);
if(!$this->service->getClient()->shouldDefer()) {
$this->logRequest('files.list', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
}
return $result;
}
/**
* @param $fileId
* @param array $optParams
* @return Google_Service_Drive_DriveFile|RequestInterface
*/
public function filesGet($fileId, $optParams = array()){
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$timerStart=microtime(true);
$optParams=$this->getParams('files.get',['fields' => $this->fetchFieldsGet],$optParams);
$result= $this->service->files->get($fileId,$optParams);
$this->logRequest('files.get', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
/**
* @param Google_Service_Drive_DriveFile $postBody
* @param array $optParams
* @return Google_Service_Drive_DriveFile | RequestInterface
*/
public function filesCreate(Google_Service_Drive_DriveFile $postBody, $optParams = array()){
$timerStart=microtime(true);
$optParams=$this->getParams('files.create',['fields' => $this->fetchFieldsGet],$optParams);
$result= $this->service->files->create($postBody,$optParams);
$this->logRequest('files.create', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
public function filesUpdate($fileId, Google_Service_Drive_DriveFile $postBody, $optParams = array()){
$timerStart=microtime(true);
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$optParams=$this->getParams('files.update',['fields' => $this->fetchFieldsGet],$optParams);
$result= $this->service->files->update($fileId,$postBody,$optParams);
$this->logRequest('files.update', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
public function filesCopy($fileId, Google_Service_Drive_DriveFile $postBody, $optParams = array()){
$timerStart=microtime(true);
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$optParams=$this->getParams('files.copy',['fields' => $this->fetchFieldsGet],$optParams);
$result= $this->service->files->copy($fileId,$postBody,$optParams);
$this->logRequest('files.copy', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
public function filesDelete($fileId, $optParams = array()){
$timerStart=microtime(true);
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$optParams=$this->getParams('files.delete',$optParams);
if($this->options['useTrash']){
$fileUpdate=new Google_Service_Drive_DriveFile();
$fileUpdate->setTrashed(true);
$result=$this->service->files->update($fileId,$fileUpdate,$optParams);
}else {
$result = $this->service->files->delete($fileId, $optParams);
}
$this->logRequest('files.delete', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $result;
}
/**
* @param $fileId
* @return resource|null
* @throws GuzzleException
*/
public function filesRead($fileId){
$timerStart=microtime(true);
if($fileId instanceof Google_Service_Drive_DriveFile){
$fileId=$fileId->getId();
}
$this->service->getClient()->setUseBatch(true);
$request=$this->filesGet($fileId, ['alt' => 'media']);
$this->service->getClient()->setUseBatch(false);
$stream=null;
$client=$this->service->getClient()->authorize();
$handler=HttpHandlerFactory::build($client);
if($handler instanceof Guzzle5HttpHandler){
//Handler v5 still working but stream read all to buffer
//We use native method to read
$stream=$this->fileReadNative($request);
}else {
$response = $handler($request, ['stream' => true]);
if ($response->getBody() instanceof Stream) {
$stream = $response->getBody()->detach();
}
}
$this->logRequest('files.read', [
'query'=>func_get_args(),
'duration'=>microtime(true)-$timerStart,
]);
return $stream;
}
protected function fileReadNative(RequestInterface $request){
$token=$this->service->getClient()->getAccessToken();
$token=$token['access_token'];
$url=$request->getUri()->__toString();
$auth = "Authorization: Bearer $token";
$opts = array (
'http' => array (
'method' => "GET",
'header' => $auth,
'user_agent' => 'as247/cloud-storages',
)
);
$context = stream_context_create($opts);
$fp = fopen($url, 'rb', false, $context);
return $fp;
}
/**
* Publish file
* @param Google_Service_Drive_DriveFile $file
*/
public function publish(Google_Service_Drive_DriveFile $file)
{
if ($this->getVisibility($file) === StorageContract::VISIBILITY_PUBLIC) {//already published
return;
}
$timerStart=microtime(true);
$permission = new Google_Service_Drive_Permission($this->publishPermission);
$optParams=$this->getParams('files.permission.create');
if ($newPermission=$this->service->permissions->create($file->getId(), $permission, $optParams)) {
$permissions=$file->getPermissions();
$permissions=array_merge($permissions,[$newPermission]);
$file->setPermissions($permissions);
}
$this->logRequest('files.permission.create', [
'query'=>$file->getId(),
'duration'=>microtime(true)-$timerStart,
]);
}
/**
* Un-publish specified path item
* @param Google_Service_Drive_DriveFile $file
*/
public function unPublish(Google_Service_Drive_DriveFile $file)
{
$timerStart=microtime(true);
$permissions = $file->getPermissions();
$optParams=$this->getParams('files.permission.delete');
foreach ($permissions as $index=> $permission) {
if ($permission->type === 'anyone' && $permission->role === 'reader') {
$this->service->permissions->delete($file->getId(), $permission->getId(), $optParams);
unset($permissions[$index]);
}
}
$file->setPermissions($permissions);
$this->logRequest('files.permission.create', [
'query'=>$file->getId(),
'duration'=>microtime(true)-$timerStart,
]);
}
public function filesUploadChunk(Google_Service_Drive_DriveFile $file,StreamInterface $contents,$chunk){
$client = $this->service->getClient();
$client->setDefer(true);
if (!$file->getId()) {
$request = $this->filesCreate($file);
} else {
$update=new Google_Service_Drive_DriveFile();
$update->setMimeType($file->getMimeType());
$request = $this->filesUpdate($file->getId(), $update);
}
$mime=$file->getMimeType();
// Create a media file upload to represent our upload process.
$media = new Google_Http_MediaFileUpload($client, $request, $mime, null, true, $chunk);
$media->setFileSize($contents->getSize());
// Upload the various chunks. $status will be false until the process is
// complete.
$status = false;
$contents->rewind();
while (!$status && !$contents->eof()) {
$status = $media->nextChunk($contents->read($chunk));
}
$client->setDefer(false);
return $status;
}
/**
* @param Google_Service_Drive_DriveFile $file
* @param $contents
* @return Google_Service_Drive_DriveFile|RequestInterface
*/
public function filesUploadSimple(Google_Service_Drive_DriveFile $file,StreamInterface $contents){
$params = [
'data' => $contents->getContents(),
'uploadType' => 'media',
];
if (!$file->getId()) {
$obj = $this->filesCreate($file, $params);
} else {
$update=new Google_Service_Drive_DriveFile();
$update->setMimeType($file->getMimeType());
$obj = $this->filesUpdate($file->getId(), $update, $params);
}
return $obj;
}
protected function logRequest($cmd, $query){
$this->logger->request($cmd,$query);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace As247\CloudStorages\Service;
trait HasLogger
{
protected $logger;
public function getLogger(){
return $this->logger;
}
public function setLogger($logger){
$this->logger=$logger;
return $this;
}
protected function setupLogger($options){
$dir=__DIR__.'/../../';
$log=$options['log']??false;
if(is_string($log)){
$dir=$log;
$log=true;
}
$dir=$options['log.dir']??$dir;
$this->logger=new Logger($dir);
$this->logger->enable($log);
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace As247\CloudStorages\Service;
class Logger
{
protected $logDir;
protected $enabled=false;
protected $num_requests=0;
protected $withTrace=false;
public function __construct($logDir='')
{
$this->logDir=$logDir;
}
public function enable($flag=true){
$previous= $this->enabled;
$this->enabled=$flag;
return $previous;
}
public function withTrace($flag=true){
$this->withTrace=$flag;
return $this;
}
function log($message, $level='debug'){
if(!$this->enabled){
return $this;
}
if(is_array($message) || is_object($message)){
$message=json_encode($message,JSON_PRETTY_PRINT);
}
$this->write("[$level] $message",'debug');
return $this;
}
function request($cmd, $query){
if(!$this->enabled){
return $this;
}
if(is_array($query) || is_object($query)) {
$query = json_encode($query, JSON_PRETTY_PRINT);
}
$this->write("{$cmd}\n$query",'query');
$this->num_requests++;
return $this;
}
public function getNumRequests(){
return $this->num_requests;
}
protected function write($line,$file){
if(!$this->enabled || !$this->logDir){
return ;
}
$time=date('Y-m-d h:i:s');
$trace='';
if($this->withTrace){
$trace=PHP_EOL.
'Trace:'.
$this->debugBacktraceSummary(null,1);
}
file_put_contents($this->logDir."/$file.log",
$time.' '. $line.$trace
.PHP_EOL.PHP_EOL,FILE_APPEND);
}
protected function debugBacktraceSummary( $ignore_class = null, $skip_frames = 0, $pretty = true ) {
static $truncate_paths;
$trace = debug_backtrace( false );
$caller = array();
$check_class = ! is_null( $ignore_class );
$skip_frames++; // Skip this function.
if ( ! isset( $truncate_paths ) ) {
$truncate_paths = array(
);
}
foreach ( $trace as $call ) {
if ( $skip_frames > 0 ) {
$skip_frames--;
} elseif ( isset( $call['class'] ) ) {
if ( $check_class && $ignore_class == $call['class'] ) {
continue; // Filter out calls.
}
$caller[] = "{$call['class']}{$call['type']}{$call['function']}";
} else {
if ( in_array( $call['function'], array( 'do_action', 'apply_filters', 'do_action_ref_array', 'apply_filters_ref_array' ), true ) ) {
$caller[] = "{$call['function']}('{$call['args'][0]}')";
} elseif ( in_array( $call['function'], array( 'include', 'include_once', 'require', 'require_once' ), true ) ) {
$filename = isset( $call['args'][0] ) ? $call['args'][0] : '';
$caller[] = $call['function'] . "('" . str_replace( $truncate_paths, '', ( $filename ) ) . "')";
} else {
$caller[] = $call['function'];
}
}
}
if ( $pretty ) {
return join( ', ', array_reverse( $caller ) );
} else {
return $caller;
}
}
}
+328
View File
@@ -0,0 +1,328 @@
<?php
namespace As247\CloudStorages\Service;
use ArrayObject;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Support\Path;
use As247\CloudStorages\Support\StorageAttributes;
use Generator;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use InvalidArgumentException;
use Microsoft\Graph\Exception\GraphException;
use Microsoft\Graph\Graph;
use Microsoft\Graph\Http\GraphRequest;
class OneDrive
{
protected $graph;
const ROOT = '/me/drive/root';
protected $publishPermission = [
'role' => 'read',
'scope' => 'anonymous',
'withLink' => true
];
protected $options;
//Add prefix / when original root has /
protected $rootPrefix='';
use HasLogger;
public function __construct(Graph $graph,$options=[])
{
$this->options=$options;
$root=$options['root']??'';
if($root){
$firstChar=substr($root,0,1);
if($firstChar==='/' || $firstChar==='\\'){
$this->rootPrefix=$firstChar;
}
}
$this->setupLogger($options);
$this->graph=$graph;
}
function normalizeMetadata(array $response, string $path): array
{
$permissions=$response['permissions']??[];
$visibility = StorageContract::VISIBILITY_PRIVATE;
$shareLink=null;
foreach ($permissions as $permission) {
if(!isset($permission['link']['scope']) || !isset($permission['roles'])){
continue;
}
if(in_array($this->publishPermission['role'],$permission['roles'])
&& $permission['link']['scope']==$this->publishPermission['scope']){
$visibility = StorageContract::VISIBILITY_PUBLIC;
$shareLink=$permission['link']['webUrl']??null;
break;
}
}
$meta= [
StorageAttributes::ATTRIBUTE_PATH => $this->rootPrefix.ltrim($path,'\/'),
StorageAttributes::ATTRIBUTE_LAST_MODIFIED => strtotime($response['lastModifiedDateTime']),
StorageAttributes::ATTRIBUTE_FILE_SIZE => $response['size'],
StorageAttributes::ATTRIBUTE_TYPE => isset($response['file']) ? 'file' : 'dir',
StorageAttributes::ATTRIBUTE_MIME_TYPE => $response['file']['mimeType'] ?? null,
StorageAttributes::ATTRIBUTE_VISIBILITY=>$visibility,
'@id'=>$response['id']??null,
'@link' => $response['webUrl'] ?? null,
'@shareLink'=>$shareLink,
'@downloadUrl' => $response['@microsoft.graph.downloadUrl']?? null,
];
return $meta;
}
function getEndpoint($path='',$action='',$params=[]){
$this->validatePath($path);
$path=Path::clean($path);
$path=trim($path,'\\/');
$path=static::ROOT.':/'.$path;
/**
* Path should not end with /
* /me/drive/root:/path/to/file
* /me/drive/root
*/
$path=rtrim($path,':/');
if($action===true){//path reference
if(strpos($path,':')===false) {
$path .= ':';//root path should end with :
}
}
if ($action && is_string($action)) {
/**
* Append action to path
* /me/drive/root:/path:/action
* trim : for root
* /me/drive/root/action
*/
$path= rtrim($path,':');
if(strpos($path,':')!==false) {
$path .=':/' . $action;//root:/path:/action
}else{
$path .= '/' . $action;//root/action
}
}
if($params){
$path.='?'.http_build_query($params);
}
return $path;
}
/**
* @param $path
* @param $newPath
* @return array|null
* @throws GraphException
*/
public function copy($path,$newPath){
$endpoint = $this->getEndpoint($path,'copy');
$name=basename($newPath);
$this->createDirectory(dirname($newPath));
$newPathParent=$this->getEndpoint(dirname($newPath),true);
$body=[
'name' => $name,
'parentReference' => [
'path' => $newPathParent,
],
];
return $this->createRequest('POST', $endpoint)
->attachBody($body)
->execute()->getBody();
}
/**
* @param $path
* @return array|null
* @throws GraphException
*/
public function createDirectory($path){
$path=Path::clean($path);
if($path==='/'){
return $this->getItem('/');
}
$endpoint=$this->getEndpoint($path);
return $this->createRequest('PATCH', $endpoint)
->attachBody([
'folder' => new ArrayObject(),
])->execute()->getBody();
}
/**
* @param $path
* @return array|null
* @throws GraphException
*/
public function delete($path){
$endpoint=$this->getEndpoint($path);
return $this->createRequest('DELETE', $endpoint)->execute()->getBody();
}
/**
* @param $path
* @param null $format
* @return resource|null
* @throws GraphException
*/
public function download($path,$format=null){
$args=[];
if($format){
if(is_string($format)){
$args=['format'=>$format];
}elseif(is_array($format)){
$args=$format;
}
}
$endpoint=$this->getEndpoint($path,'content',$args);
$response=$this->createRequest('GET',$endpoint)->setReturnType('GuzzleHttp\Psr7\Stream')->execute();
/**
* @var StreamWrapper $response
*/
return $response->detach();
}
/**
* @param $path
* @param array $args
* @return array|null
* @throws GraphException
*/
public function getItem($path,$args=[]){
$endpoint=$this->getEndpoint($path,'',$args);
$response = $this->createRequest('GET', $endpoint)->execute();
return $response->getBody();
}
/**
* @param $path
* @return Generator
* @throws GraphException
*/
public function listChildren($path){
$endpoint = $this->getEndpoint($path,'children');
$nextPage=null;
do {
if ($nextPage) {
$endpoint = $nextPage;
}
$response = $this->createRequest('GET', $endpoint)
->execute();
$nextPage = $response->getNextLink();
$items = $response->getBody()['value']??[];
if(!is_array($items)){
$items=[];
}
yield from $items;
}while($nextPage);
}
/**
* @param $path
* @param $newPath
* @return array|null
* @throws GraphException
*/
public function move($path,$newPath){
$endpoint = $this->getEndpoint($path);
$name=basename($newPath);
$this->createDirectory(dirname($newPath));
$newPathParent=$this->getEndpoint(dirname($newPath),true);
$body=[
'name' => $name,
'parentReference' => [
'path' => $newPathParent,
],
];
return $this->createRequest('PATCH', $endpoint)
->attachBody($body)
->execute()->getBody();
}
/**
* @param $path
* @param $contents
* @return array|null
* @throws GraphException
*/
public function upload($path,$contents){
$endpoint = $this->getEndpoint($path,'content');
try {
$stream = StreamWrapper::wrap($contents);
}catch (InvalidArgumentException $e){
throw new StorageException("Invalid contents. ".$e->getMessage());
}
$this->createDirectory(dirname($path));
return $this->createRequest('PUT', $endpoint)
->attachBody($stream)
->execute()->getBody();
}
/**
* @param $path
* @return array|mixed
* @throws GraphException
*/
public function getPermissions($path){
$endpoint=$this->getEndpoint($path,'permissions');
$response = $this->createRequest('GET', $endpoint)->execute();
return $response->getBody()['value']??[];
}
/**
* @param $path
* @return array
* @throws GraphException
*/
function publish($path){
$endpoint=$this->getEndpoint($path,'createLink');
$body=['type'=>'view','scope'=>'anonymous'];
$response = $this->createRequest('POST', $endpoint)
->attachBody($body)->execute();
return $response->getBody();
}
/**
* @param $path
* @throws GraphException
*/
function unPublish($path){
$permissions=$this->getPermissions($path);
$idToRemove='';
foreach ($permissions as $permission){
if(in_array($this->publishPermission['role'],$permission['roles'])
&& $permission['link']['scope']==$this->publishPermission['scope']){
$idToRemove=$permission['id'];
break;
}
}
if(!$idToRemove){
return ;
}
$endpoint=$this->getEndpoint($path,'permissions/'.$idToRemove);
$this->createRequest('DELETE', $endpoint)->execute();
}
/**
* @param $requestType
* @param $endpoint
* @return GraphRequest
* @throws GraphException
*/
protected function createRequest($requestType, $endpoint){
$this->logger->request($requestType,$endpoint);
return $this->graph->createRequest($requestType,$endpoint);
}
protected function validatePath($path){
$invalidChars=['"','*',':','<','>','?', '|'];
foreach ($invalidChars as $char){
if(strpos($path,$char)!==false){
throw new StorageException("Invalid character $char in path $path");
}
}
}
}
@@ -0,0 +1,22 @@
<?php
namespace As247\CloudStorages\Service;
use GuzzleHttp\Psr7\Utils;
/**
* Custom implement Psr7 Stream to fix AssemblyStream
* Google require min chunk size: 262144 but assembly stream may have smaller chunk size
* Eg NextCloud chunk size: 8192 and you will got following error
* Invalid request. The number of bytes uploaded is required to be equal or greater than 262144, except for the final request (it's recommended to be the exact multiple of 262144). The received request contained 8192 bytes, which does not meet this requirement.
* Class Stream
* @package As247\CloudStorages\Service
*/
class StreamWrapper
{
public static function wrap($stream,$options=[]){
return Utils::streamFor($stream,$options);
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Service\AListService;
use As247\CloudStorages\Support\Config;
use As247\CloudStorages\Support\FileAttributes;
use As247\CloudStorages\Support\Path;
use Traversable;
class AList extends Storage
{
protected $service;
protected $root = '';
protected $fakeVisibility=[];
public function __construct($url, $options = [])
{
$this->service = new AListService($url, $options);
$this->root=Path::clean($options['root']??'');
}
public function getService()
{
return $this->service;
}
public function writeStream(string $path, $contents, Config $config = null): void
{
$this->service->put($path, $contents);
if($config){
$this->setVisibility($path,$config->get('visibility'));
}
}
public function readStream(string $path)
{
$contents = $this->service->read($path);
if ($contents) {
return $contents;
}
throw new StorageException('Unable to read file: ' . $path,'readStream');
}
public function delete(string $path): void
{
if(Path::clean($path)===$this->root){
return ;
}
$this->service->remove(dirname($path),basename($path));
}
public function deleteDirectory(string $path): void
{
$this->delete($path);
}
public function createDirectory(string $path, Config $config = null): void
{
$created=$this->service->mkdir($path);
if(!$created){
throw new StorageException('Unable to create directory: ' . $path,'createDirectory');
}
}
public function setVisibility(string $path, $visibility): void
{
$this->getMetadata($path);
$this->fakeVisibility[$path]=$visibility;
//throw new StorageException('setVisibility is not supported by AList','setVisibility');
}
public function listContents(string $path, bool $deep): Traversable
{
$results = $this->service->listContents($path);
foreach ($results as $id => $result) {
$result = $this->service->normalizeMetadata($result, rtrim($path,'\/') . '/' . $result['name']);
yield $id => $result;
if ($deep && $result['type'] === 'dir') {
yield from $this->listContents($result['path'], $deep);
}
}
}
public function move(string $source, string $destination, Config $config = null): void
{
if(Path::clean($source)===Path::clean($destination)){
throw new StorageException('Source and destination are the same: ' . $source,'move');
}
$this->getMetadata($source);
$this->copy($source,$destination,$config);
$this->delete($source);
}
public function copy(string $source, string $destination, Config $config = null): void
{
$srcDir=dirname($source);
$dstDir=dirname($destination);
$tmpDir=Path::join($srcDir,'.tmp');
$this->createDirectory($tmpDir);
$this->createDirectory($dstDir);
$this->service->copy($srcDir,$tmpDir,basename($source));
$this->service->rename(Path::join($tmpDir,basename($source)),basename($destination));
$this->service->move($tmpDir,$dstDir,basename($destination));
if($config){
$this->setVisibility($destination,$this->getMetadata($source)->visibility());
}
}
/**
* @param $path
* @return FileAttributes
*/
public function getMetadata($path): FileAttributes
{
$meta=$this->service->get($path);
if(!$meta){
throw FileNotFoundException::create($path);
}
$attributes=$this->service->normalizeMetadata($meta,$path);
if(isset($this->fakeVisibility[$path])){
$attributes['visibility']=$this->fakeVisibility[$path];
}
return new FileAttributes($attributes);
}
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config = null): string
{
$expire=$expiresAt->getTimestamp();
return $this->service->getDownloadUrl($path,$expire);
}
}
+631
View File
@@ -0,0 +1,631 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Cache\PathCache;
use As247\CloudStorages\Cache\Stores\GoogleDrivePersistentStore;
use As247\CloudStorages\Cache\Stores\GoogleDriveStore;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Service\StreamWrapper;
use As247\CloudStorages\Support\Config;
use As247\CloudStorages\Support\FileAttributes;
use As247\CloudStorages\Support\Path;
use Exception;
use Generator;
use Google_Service_Drive;
use Google_Service_Drive_DriveFile;
use As247\CloudStorages\Service\GoogleDrive as GoogleDriveService;
use Google_Service_Drive_FileList;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use Traversable;
class GoogleDrive extends Storage
{
/**
* Google_Service_Drive instance
*/
protected $service;
protected $root;//Root id
protected $maxFolderLevel = 128;
public function __construct(Google_Service_Drive $service, $options)
{
if (is_string($options)) {
$options = ['root' => $options];
}
if(isset($options['teamDrive'])){
if($options['teamDrive']===true){
$options['teamDrive']=$options['root'];//Team drive same as root if boolean
}elseif(empty($options['root']) && is_string($options['teamDrive'])){
$options['root']=$options['teamDrive'];
}
}
$this->service = new GoogleDriveService($service, $options);
$this->setLogger($this->service->getLogger());
$this->setRoot($options);
}
/**
* Gets the service (Google_Service_Drive)
*
* @return object Google_Service_Drive
*/
public function getService(){
return $this->service;
}
public function getCache(){
return $this->cache;
}
protected function setRoot($options)
{
$root = $options['root'];
if(empty($root)) {
throw new StorageException("Root folder id is required");
}
$this->root = $root;
if(isset($options['cache']) && is_string($options['cache'])){
$options['cache'] = new PathCache(new GoogleDrivePersistentStore($options['cache']));
}else {
$options['cache'] = new PathCache(new GoogleDriveStore());
}
$this->setupCache($options);
}
public function getRoot(){
return $this->root;
}
protected function initializeCacheRoot(){
$this->cache->getStore()->mapDirectory('/', $this->root);
}
/**
* @param string|array $path create directory structure
* @return string folder id
*/
protected function ensureDirectory($path)
{
$path = Path::clean($path);
if ($this->isFile($path)) {
throw new StorageException("File already exists at $path");
}
if (isset($this->maxFolderLevel)) {
$nestedFolderLevel = count(explode('/', $path)) - 1;
if ($nestedFolderLevel > $this->maxFolderLevel) {// -1 for /
throw new StorageException("Maximum nesting folder exceeded");
}
}
$this->logger->log("mkdir: $path");
list($parent, $paths, $currentPaths) = $this->detectPath($path);
if (count($paths) != 0) {
while (null !== ($name = array_shift($paths))) {
$currentPaths[] = $name;
$currentPathString = join('/', $currentPaths);
if ($this->isFile($currentPaths)) {
throw new StorageException("File already exists at $currentPathString");
}
$created = $this->service->dirCreate($name, $parent);
//echo 'Created: '.print_r($currentPaths);
$this->cache->put($currentPaths, $created);
$this->cache->complete($currentPaths);
$parent = $created->getId();
}
}
return $parent;
}
/**
* @inheritDoc
*/
public function writeStream(string $path, $contents, Config $config = null): void
{
$this->upload($path, $contents, $config);
}
/**
* Delete file only
* @param $path
* @return void
* @throws FileNotFoundException
*/
public function delete(string $path): void
{
if ($this->isDirectory($path)) {
throw new StorageException("$path is directory");
}
$file = $this->find($path);
if (!$file) {//already deleted
throw FileNotFoundException::create($path);
}
if ($file->getId() === $this->root) {
throw new StorageException("Root directory cannot be deleted");
}
$this->service->filesDelete($file);
$this->cache->delete($path);
$this->logger->log("Deleted $path");
}
/**
* @inheritDoc
*/
public function deleteDirectory(string $path): void
{
if ($this->isFile($path)) {
throw new StorageException("$path is file");
}
$file = $this->find($path);
if (!$file) {//already deleted
throw FileNotFoundException::create($path);
}
if ($file->getId() === $this->root) {
throw new StorageException("Root directory cannot be deleted");
}
$this->service->filesDelete($file);
$this->cache->deleteDir($path);
$this->logger->log("Deleted $path");
}
/**
* Find for path
* @param $path
* @return false|Google_Service_Drive_DriveFile
*/
protected function find($path)
{
if ($path instanceof Google_Service_Drive_DriveFile) {
return $path;
}
list(, $paths) = $this->detectPath($path);
if (count($paths) >= 2) {
//remaining 2 segments /A/B/C/file.txt
//C not exists mean file.txt also not exists
return false;
}
if (null!==($cached=$this->cache->get($path))) {
return $cached;
}
return false;
}
public function createDirectory(string $path, Config $config = null): void
{
$this->ensureDirectory(Path::clean($path));
$result = $this->getMetadata($path);
if ($config && $visibility = $config->get('visibility')) {
$this->setVisibility($path, $visibility);
$result['visibility'] = $visibility;
}
}
public function copy(string $fromPath, string $toPath, Config $config = null): void
{
$fromPath = Path::clean($fromPath);
$toPath = Path::clean($toPath);
$from = $this->find($fromPath);
if (!$from) {
throw new StorageException("$fromPath not found");
}
if ($this->isDirectory($fromPath)) {
throw new StorageException("$fromPath is directory");
}
if ($this->isDirectory($toPath)) {
throw new StorageException("$toPath is directory");
}
if ($this->has($toPath)) {
$this->delete($toPath);
}
$paths = $this->parsePath($toPath);
$fileName = array_pop($paths);
$dirName = $paths;
$parents = [$this->ensureDirectory($dirName)];
$file = new Google_Service_Drive_DriveFile();
$file->setName($fileName);
$file->setParents($parents);
$newFile = $this->service->filesCopy($from->id, $file);
$visibilityForNewFile=$this->getMetadata($fromPath)->visibility();
if($config && $visibilityFromConfig = $config->get('visibility')){
$visibilityForNewFile=$visibilityFromConfig;
}
$this->cache->put($toPath, $newFile);
$this->setVisibility($toPath, $visibilityForNewFile);
$this->logger->log("Copied file: $fromPath -> $toPath");
}
public function move(string $fromPath, string $toPath, Config $config = null): void
{
$fromPath = Path::clean($fromPath);
$toPath = Path::clean($toPath);
if ($fromPath === $toPath) {
return;
}
$from = $this->find($fromPath);
if (!$from) {
throw new StorageException("$fromPath not found");
}
$oldParent = $from->getParents()[0];
$newParentId = null;
if ($this->isFile($from)) {//we moving file
if ($this->has($toPath)) {
if ($this->isDirectory($toPath)) {//Destination path is directory
throw new StorageException("$toPath is directory and cannot be overwritten");
} else {
$this->delete($toPath);
}
}
} else {//we moving directory
if ($this->has($toPath)) {
if ($this->isFile($toPath)) {//Destination path is file
throw new StorageException("$toPath is file and cannot be overwritten");
} else {
$this->deleteDirectory($toPath);//overwrite, remove it first
}
}
}
$paths = $this->parsePath($toPath);
$fileName = array_pop($paths);
$dirName = $paths;
$newParentId = $this->ensureDirectory($dirName);
$file = new Google_Service_Drive_DriveFile();
$file->setName($fileName);
$opts=[];
if ($newParentId !== $oldParent) {
$opts['addParents'] = $newParentId;
$opts['removeParents'] = $oldParent;
}
$result=$this->service->filesUpdate($from->getId(), $file, $opts);
if(!in_array($newParentId,$result->getParents())){
throw new StorageException("Service update failure");
}
$this->cache->move($fromPath, $toPath);
$this->logger->log("Moved file: $fromPath -> $toPath");
}
/**
* Upload|Update item
*
* @param string $path
* @param $contents
* @param Config|null $config
* @return FileAttributes
* @throws FileNotFoundException
*/
protected function upload(string $path, $contents, Config $config = null)
{
try {
$contents = StreamWrapper::wrap($contents);
}catch (InvalidArgumentException $e){
throw new StorageException("Invalid contents. ".$e->getMessage());
}
$contents->rewind();
if ($this->isDirectory($path)) {
throw new StorageException("$path is directory");
}
$paths = $this->parsePath($path);
$fileName = array_pop($paths);
$dirName = $paths;
//Try to find file before, because if it was removed before, ensure directory will recreate same directory and it may available again
$parentId = $this->ensureDirectory($dirName);
if (!$parentId) {
throw new StorageException("Not able to create parent directory ".join('/',$dirName));
}
$file = $this->find($path);
if (!$file) {
$file = new Google_Service_Drive_DriveFile();
$file->setName($fileName);
$file->setParents([
$parentId
]);
}
$newMimeType = $config ? $config->get('mimetype', $config->get('mime_type')) : null;
if ($newMimeType) {
$file->setMimeType($newMimeType);
}
$size5MB = 5 * 1024 * 1024;
$chunkSize = $config ? $config->get('chunk_size', $size5MB) : $size5MB;
if ($contents->getSize() <= $size5MB) {
$obj = $this->service->filesUploadSimple($file, $contents);
} else {
$obj = $this->service->filesUploadChunk($file, $contents, $chunkSize);
}
$this->logger->log("Uploaded: $path");
if ($obj instanceof Google_Service_Drive_DriveFile) {
$this->cache->put($path, $obj);//update cache first
if ($config && $visibility = $config->get('visibility')) {
$this->setVisibility($path, $visibility);
}
return $this->getMetadata($path);
}
throw new StorageException("Unable to upload file: $path");
}
/**
* @param string $directory
* @param bool $recursive
* @return Generator
*/
public function listContents(string $directory, bool $recursive = false): Traversable
{
if (!$this->isDirectory($directory)) {
yield from [];
return;
}
$results = $this->fetchDirectory($directory, 1000);
foreach ($results as $id => $result) {
yield $id => $result;
if ($recursive && $result['type'] === 'dir') {
yield from $this->listContents($result['path'], $recursive);
}
}
}
protected function fetchDirectory($directory, $pageSize = 1000)
{
//echo 'Fetching: '.$directory;
if ($this->cache->isCompleted($directory)) {
foreach ($this->cache->query($directory) as $path => $file) {
if ($file instanceof Google_Service_Drive_DriveFile) {
yield $file->getId() => $this->service->normalizeMetadata($file, $path);
}
}
return null;
}
list($itemId) = $this->detectPath($directory);
$pageSize = min($pageSize, 1000);//limit range of page size
$pageSize = max($pageSize, 1);//
$parameters = [
'pageSize' => $pageSize,
'q' => sprintf('trashed = false and "%s" in parents', $itemId)
];
$pageToken = NULL;
do {
try {
if ($pageToken) {
$parameters['pageToken'] = $pageToken;
}
$fileObjs = $this->service->filesListFiles($parameters);
if ($fileObjs instanceof Google_Service_Drive_FileList) {
foreach ($fileObjs as $obj) {
$id = $obj->getId();
$result = $this->service->normalizeMetadata($obj, rtrim($directory,'\/') . '/' . $obj->getName());
yield $id => $result;
$this->cache->put($result['path'], $obj);
}
$pageToken = $fileObjs->getNextPageToken();
} else {
$pageToken = NULL;
}
} catch (Exception $e) {
$pageToken = NULL;
}
} while ($pageToken);
$this->cache->complete($directory);
}
/**
* Publish specified path item
*
* @param string $path
*/
protected function publish(string $path)
{
if (!$file = $this->find($path)) {
throw new StorageException("File $path not found");
}
$this->service->publish($file);
$this->cache->put($path,$file);
}
/**
* Un-publish specified path item
*
* @param string $path
*/
protected function unPublish(string $path)
{
if (!$file = $this->find($path)) {
throw new StorageException("File $path not found");
}
$this->service->unPublish($file);
$this->cache->put($path,$file);
}
/**
* @param string $path
* @param mixed $visibility
*/
public function setVisibility(string $path, $visibility): void
{
if ($visibility === Storage::VISIBILITY_PUBLIC) {
$this->publish($path);
} elseif ($visibility === Storage::VISIBILITY_PRIVATE) {
$this->unPublish($path);
} else {
throw new InvalidArgumentException('Visibility must be either "public" or "private".');
}
}
public function readStream(string $path)
{
$file = $this->find($path);
if (!$this->isFile($path)) {
throw FileNotFoundException::create($path);
}
try {
return $this->service->filesRead($file);
} catch (GuzzleException $e) {
throw new StorageException("Unable to read file: $path", 'readStream', $e);
}
}
protected function parsePath($path)
{
$paths = Path::explode($path);
$directory = [];
$file = [];
$level = 0;
foreach ($paths as $path) {
if ($level++ > $this->maxFolderLevel) {
$file[] = $path;
} else {
$directory[] = $path;
}
}
if (!$file) {
$file[] = array_pop($directory);
}
$file = join('/', $file);
$directory[] = $file;
return $directory;
}
/**
* Travel through the path tree then return folder id, remaining path, current path
* eg: /path/to/the/file/text.txt
* - if we have directory /path/to then it return [path_to_id, ['the','file','text.txt'], ['path','to']
* - if we have /path/to/the/file/text.txt then it return [id_of_path_to_the_file, ['text.txt'], ['path','to','the','file'] ]
* @param $path
* @return array
*/
protected function detectPath($path)
{
$paths = $this->parsePath($path);
$this->logger->log("Path finding: " . join(', ',$paths));
$currentPaths = [];
if($this->cache->get('/')===null) {
$this->initializeCacheRoot();
}
$parent = $this->cache->get('/');
while (null !== ($name = array_shift($paths))) {
$parentPaths = $currentPaths;
$currentPaths[] = $name;
$foundDir = $this->cache->get($currentPaths);
if (!is_null($foundDir)) {
if ($foundDir && $this->isDirectory($foundDir)) {
$parent = $foundDir;
continue;
} else {
//echo 'break at...'.implode($currentPaths);
array_pop($currentPaths);
array_unshift($paths, $name);
break;
}
}
list($files, $isFull) = $this->service->filesFindByName($name, $parent);
if ($isFull) {
$this->cache->complete($parentPaths);
}
$foundDir = false;
//Set current path as not exists, it will be updated again when we got matched file
$this->cache->put($currentPaths, false);
if ($files->count()) {
foreach ($files as $file) {
if ($file instanceof Google_Service_Drive_DriveFile) {
$fileFound = $parentPaths;
$fileFound[]=$file->getName();
$this->cache->put($fileFound, $file);
if ($this->isDirectory($file) && $file->getName() === $name) {
$foundDir = $file;
}
}
}
}
if (!$foundDir) {
array_pop($currentPaths);
array_unshift($paths, $name);
break;
}
$parent = $foundDir;
}
$parent = $parent->getId();
$this->logger->log("Found: " . $parent . '(' . join('/',$currentPaths) . ") " . join('/',$paths));
return [$parent, $paths, $currentPaths];
}
/**
* Check if given path exists
* @param $path
* @return bool
*/
protected function has($path)
{
try {
$this->getMetadata($path);
return true;
}catch (FileNotFoundException $e){
return false;
}
}
protected function isDirectory($path)
{
try {
$meta = $this->getMetadata($path);
return $meta->isDir();
}catch (FileNotFoundException $e){
return false;
}
}
protected function isFile($path)
{
try {
$meta = $this->getMetadata($path);
return $meta->isFile();
}catch (FileNotFoundException $e){
return false;
}
}
/**
* @param $path
* @return FileAttributes
* @throws FileNotFoundException
*/
public function getMetadata($path):FileAttributes
{
if ($obj = $this->find($path)) {
if ($path instanceof Google_Service_Drive_DriveFile) {
$path = null;
}
if ($obj instanceof Google_Service_Drive_DriveFile) {
$attributes = $this->service->normalizeMetadata($obj, $path);
return FileAttributes::fromArray($attributes);
}
throw new StorageException("Unable to get metadata for file at path: $path");
}
throw FileNotFoundException::create(Path::clean($path));
}
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config = null): string
{
// TODO: Implement temporaryUrl() method.
}
}
@@ -0,0 +1,100 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Cache\Stores\GoogleDrivePersistentStore;
use As247\CloudStorages\Cache\Stores\NullStore;
use As247\CloudStorages\Cache\Stores\SqliteCache;
use As247\CloudStorages\Contracts\Storage\ObjectStorage;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
class GoogleDriveObjectStorage implements ObjectStorage
{
protected $storage;
protected $cache;
protected $storageCache;
public function __construct(GoogleDrive $drive,$cacheFilePath=null)
{
$this->storage=$drive;
if($cacheFilePath===null) {
$cacheFilePath = sys_get_temp_dir() . '/' . $drive->getRoot() . '.sqlite';
}
if($cacheFilePath) {
$this->cache = new SqliteCache($cacheFilePath);
}else{
$this->cache = new NullStore();
}
$this->storageCache=$drive->getCache();
if($this->storageCache->getStore() instanceof GoogleDrivePersistentStore){
throw new StorageException('Persistent cache not work with object storage');
}
}
public function readObject($urn)
{
$urn=$this->sanitizeUrn($urn);
$this->mapCache($urn);
return $this->storage->readStream($urn);
}
public function writeObject($urn, $stream)
{
$urn=$this->sanitizeUrn($urn);
$this->mapCache($urn);
try {
$this->storage->writeStream($urn, $stream);
} catch (StorageException $e) {
$this->cache->forget($urn);
throw $e;
}
//File write success update cache
$this->updateCache($urn);
}
public function deleteObject($urn)
{
$urn=$this->sanitizeUrn($urn);
$this->mapCache($urn);
try {
$this->storage->delete($urn);
} catch (FileNotFoundException $e) {
//Already deleted do nothing
} finally {
$this->cache->forget($urn);
}
}
public function objectExists($urn)
{
$urn=$this->sanitizeUrn($urn);
try {
$this->updateCache($urn);
return true;
} catch (FileNotFoundException $e) {
return false;
}
}
protected function mapCache($urn){
$cached=$this->cache->get($urn);
if($cached){
$this->storageCache->mapFile($urn,$cached);
}
}
protected function updateCache($urn){
try {
$meta=$this->storage->getMetadata($urn);
$this->cache->put($urn,$meta['@id']);
return true;
} catch (FileNotFoundException $e) {
$this->cache->forget($urn);
throw $e;
}
}
protected function sanitizeUrn($urn){
return str_replace(['/','\\'],':',$urn);
}
}
+203
View File
@@ -0,0 +1,203 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Support\Config;
use As247\CloudStorages\Support\FileAttributes;
use Generator;
use GuzzleHttp\Exception\ClientException;
use Microsoft\Graph\Exception\GraphException;
use As247\CloudStorages\Service\OneDrive as OneDriveService;
use Microsoft\Graph\Graph;
use Throwable;
use Traversable;
class OneDrive extends Storage
{
/** @var Graph */
protected $service;
public function __construct(Graph $graph,$options=[])
{
$this->service = new OneDriveService($graph,$options);
$this->setLogger($this->service->getLogger());
$this->setupCache($options);
}
public function getService()
{
return $this->service;
}
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config = null): string
{
return $this->getMetadata($path)['@downloadUrl']??'';
}
/**
* @param string $directory
* @param bool $recursive
* @return Generator
* @throws GraphException
*/
public function listContents(string $directory = '', bool $recursive = false): Traversable
{
try {
$results = $this->service->listChildren($directory);
foreach ($results as $id => $result) {
$result = $this->service->normalizeMetadata($result, rtrim($directory,'\/') . '/' . $result['name']);
yield $id => $result;
if ($recursive && $result['type'] === 'dir') {
yield from $this->listContents($result['path'], $recursive);
}
}
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 404) {
yield from [];
}
}
}
public function writeStream(string $path, $contents, Config $config = null): void
{
try {
$this->service->upload($path, $contents);
$this->cache->forgetBranch($path);
if ($config && $visibility = $config->get('visibility')) {
$this->setVisibility($path, $visibility);
}
} catch (ClientException $e) {
throw new StorageException($e->getMessage(),'writeStream',$e);
} catch (GraphException $e) {
throw new StorageException($e->getMessage(),'writeStream',$e);
}
}
public function readStream(string $path)
{
try {
return $this->service->download($path);
} catch (ClientException $e) {
throw new StorageException($e->getMessage(),'readStream',$e);
} catch (GraphException $e) {
throw new StorageException($e->getMessage(),'readStream',$e);
}
}
public function delete(string $path): void
{
try {
$this->service->delete($path);
$this->cache->delete($path);
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 404) {
throw FileNotFoundException::create($path);
}
throw new StorageException($e->getMessage(),'delete',$e);
} catch (GraphException $e) {
throw new StorageException($e->getMessage(),'delete',$e);
}
}
public function deleteDirectory(string $path): void
{
$this->delete($path);
$this->cache->deleteDir($path);
}
public function createDirectory(string $path, Config $config = null): void
{
try {
$response = $this->service->createDirectory($path);
$this->cache->forgetBranch($path);
$file = FileAttributes::fromArray($this->service->normalizeMetadata($response, $path));
if (!$file->isDir()) {
throw new StorageException('File already exists at '.$path,'createDirectory');
}
} catch (GraphException $e) {
throw new StorageException($e->getMessage(),'createDirectory');
} catch (ClientException $e) {
throw new StorageException($e->getMessage(),'createDirectory');
}
}
/**
* @param string $path
* @param mixed $visibility
* @throws GraphException
*/
public function setVisibility(string $path, $visibility): void
{
if ($visibility === Storage::VISIBILITY_PUBLIC) {
$this->service->publish($path);
$this->cache->forget($path);
} elseif ($visibility === Storage::VISIBILITY_PRIVATE) {
$this->service->unPublish($path);
$this->cache->forget($path);
} else {
throw new \InvalidArgumentException('Unknown visibility: ' . $visibility);
}
}
/**
* @param string $source
* @param string $destination
* @param Config|null $config
* @throws GraphException
*/
public function move(string $source, string $destination, Config $config = null): void
{
$this->service->move($source, $destination);
$this->cache->move($source,$destination);
}
/**
* @param string $source
* @param string $destination
* @param Config|null $config
* @throws GraphException
*/
public function copy(string $source, string $destination, Config $config = null): void
{
$this->service->copy($source, $destination);
$this->cache->forgetBranch($destination);
$this->setVisibility($destination, $this->getMetadata($source)->visibility());
}
/**
* @param $path
* @return FileAttributes
* @throws FileNotFoundException
*/
public function getMetadata($path): FileAttributes
{
try {
$meta=$this->cache->get($path);
if(!is_null($meta) && !$meta){
throw new FileNotFoundException($path);
}
if(!isset($meta)) {
$meta = $this->service->getItem($path, ['expand' => 'permissions']);
$this->cache->put($path,$meta);
}
$attributes = $this->service->normalizeMetadata($meta, $path);
return FileAttributes::fromArray($attributes);
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 404) {
$this->cache->put($path,false);
throw new FileNotFoundException($path, 0, $e);
}
throw new StorageException($e->getMessage(),'getMetadata',$e);
} catch (FileNotFoundException $e){
throw $e;
}catch (Throwable $e) {
throw new StorageException($e->getMessage(),'getMetadata',$e);
}
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace As247\CloudStorages\Storage;
use As247\CloudStorages\Cache\PathCache;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use As247\CloudStorages\Service\HasLogger;
use Closure;
abstract class Storage implements StorageContract
{
/**
* @var PathCache
*/
protected $cache;
use HasLogger;
protected function setupCache($options){
$cache=$options['cache']??null;
if($cache instanceof Closure){
$cache=$cache();
}
if(!$cache instanceof PathCache){
$cache=new PathCache();
}
$this->setCache($cache);
}
public function setCache(PathCache $cache){
$this->cache=$cache;
return $this;
}
public function getCache(){
return $this->cache;
}
}
@@ -0,0 +1,64 @@
<?php
namespace As247\CloudStorages\Support;
use RuntimeException;
trait AttributesAccess
{
protected $attributes=[];
/**
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return array_key_exists($offset,$this->attributes);
}
/**
* @param mixed $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->attributes[$offset];
}
/**
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet($offset, $value): void
{
throw new RuntimeException('Properties can not be manipulated');
}
/**
* @param mixed $offset
*/
public function offsetUnset($offset): void
{
throw new RuntimeException('Properties can not be manipulated');
}
public function __get($name)
{
return $this->offsetGet($name);
}
public function __set($name, $value)
{
$this->offsetSet($name,$value);
}
public function __isset($name)
{
return $this->offsetExists($name);
}
public function __unset($name)
{
$this->offsetUnset($name);
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace As247\CloudStorages\Support;
class Config
{
/**
* @var array
*/
protected $settings = [];
/**
* @var Config|null
*/
protected $fallback;
/**
* Constructor.
*
* @param array $settings
*/
public function __construct(array $settings = [])
{
$this->settings = $settings;
}
/**
* Get a setting.
*
* @param string $key
* @param mixed $default
*
* @return mixed config setting or default when not found
*/
public function get(string $key, $default = null)
{
if ( ! array_key_exists($key, $this->settings)) {
return $this->getDefault($key, $default);
}
return $this->settings[$key];
}
/**
* Check if an item exists by key.
*
* @param string $key
*
* @return bool
*/
public function has(string $key)
{
if (array_key_exists($key, $this->settings)) {
return true;
}
return $this->fallback instanceof Config
? $this->fallback->has($key)
: false;
}
/**
* Try to retrieve a default setting from a config fallback.
*
* @param string $key
* @param mixed $default
*
* @return mixed config setting or default when not found
*/
protected function getDefault(string $key, $default)
{
if ( ! $this->fallback) {
return $default;
}
return $this->fallback->get($key, $default);
}
/**
* Set a setting.
*
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function set(string $key, $value)
{
$this->settings[$key] = $value;
return $this;
}
/**
* Set the fallback.
*
* @param Config $fallback
*
* @return $this
*/
public function setFallback(Config $fallback)
{
$this->fallback = $fallback;
return $this;
}
}
@@ -0,0 +1,26 @@
<?php
namespace As247\CloudStorages\Support;
trait ConfigConverter
{
protected function convertConfig($config=null)
{
if($config){
if(is_array($config)){
return new \As247\CloudStorages\Support\Config($config);
}
if(method_exists($config,'toArray')) {
return new \As247\CloudStorages\Support\Config($config->toArray());
}else{
return new \As247\CloudStorages\Support\Config([
'copy_destination_same_as_source'=>$config->get('copy_destination_same_as_source'),
'move_destination_same_as_source'=>$config->get('move_destination_same_as_source'),
'visibility'=>$config->get('visibility'),
'directory_visibility'=>$config->get('directory_visibility'),
]);
}
}
return new \As247\CloudStorages\Support\Config();
}
}
@@ -0,0 +1,89 @@
<?php
namespace As247\CloudStorages\Support;
/**
* Class FileAttributes
* @package As247\CloudStorages\Support
* @property $path
* @property $type
* @property $file_size
* @property $visibility
* @property $last_modified
* @property $mime_type
*/
class FileAttributes implements StorageAttributes
{
use AttributesAccess;
public function __construct(array $attributes) {
$this->attributes=$attributes;
if(!isset($this->attributes[static::ATTRIBUTE_TYPE])) {
$this->attributes[static::ATTRIBUTE_TYPE] = StorageAttributes::TYPE_FILE;
}
}
public function type(): string
{
return $this->type;
}
public function path(): string
{
return $this->path;
}
public function fileSize(): ?int
{
return $this->file_size;
}
public function visibility(): ?string
{
return $this->visibility;
}
public function lastModified(): ?int
{
return $this->last_modified;
}
public function mimeType(): ?string
{
return $this->mime_type;
}
public function isFile(): bool
{
return $this->type===StorageAttributes::TYPE_FILE;
}
public function isDir(): bool
{
return $this->type===StorageAttributes::TYPE_DIRECTORY;
}
public static function fromArray(array $attributes): FileAttributes
{
return new FileAttributes($attributes);
}
public function toArray(){
return $this->attributes;
}
public function toArrayV1(){
return array_merge($this->attributes,
[
'size'=>$this->file_size??0,
'mimetype'=>$this->mime_type??0,
'timestamp'=>$this->last_modified??null,
]
);
}
public function jsonSerialize(): array
{
return $this->toArray();
}
}
@@ -0,0 +1,22 @@
<?php
namespace As247\CloudStorages\Support;
trait GetTemporaryUrl
{
public function getTemporaryUrl($path, $expiration=null, $options=[])
{
if($expiration===null){
$expiration=time()+3600;
}
if(is_int($expiration)){
$expiration=(new \DateTime())->setTimestamp($expiration);
}
if(isset($this->prefixer)){
$path=$this->prefixer->prefixPath($path);
}else{
$path=$this->applyPathPrefix($path);
}
return $this->storage->temporaryUrl($path,$expiration,$this->convertConfig($options));
}
}
@@ -0,0 +1,317 @@
<?php
/**
* Created by PhpStorm.
* User: alt
* Date: 17-Oct-18
* Time: 9:41 PM
*/
namespace As247\CloudStorages\Support;
use As247\CloudStorages\Cache\TempCache;
use As247\CloudStorages\Contracts\Cache\Store;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
class OneDriveOauth
{
protected $clientId;
protected $clientSecret;
protected $accessToken;
protected $refreshToken;
protected $tenantId='common';
protected $httpClient;
/**
* @var Store
*/
protected $cache;
protected $tokenEndpoint = 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token';
protected $oauthUrl = 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize';
protected $oauthParams = [
'access_type' => 'offline',
'redirect_uri' => 'http://localhost:53682',
'response_type' => 'code',
'scope' => 'files.readwrite.all offline_access',
];
public function __construct(Store $cache = null)
{
$this->cache = $cache;
}
public function createAuthUrl()
{
$this->oauthParams['client_id'] = $this->clientId;
$this->oauthParams['redirect_uri'] = $this->getCurrentUrl();
$this->oauthParams['state'] = time();
return str_replace('{tenant}', $this->tenantId, $this->oauthUrl) . '?' . http_build_query($this->oauthParams);
}
protected function getCurrentUrl()
{
return 'http://' . $_SERVER['HTTP_HOST'];
}
/**
* @param $code
* @return mixed
* @throws GuzzleException
*/
public function fetchAccessTokenWithAuthCode($code)
{
$postKey = 'form_params';
$response = $this->getHttpClient()->post(
str_replace('{tenant}', $this->tenantId, $this->tokenEndpoint),
[
'headers' => ['Accept' => 'application/json'],
$postKey => [
'client_id' => $this->clientId, 'client_secret' => $this->clientSecret,
'grant_type' => 'authorization_code',
'redirect_uri' => $this->getCurrentUrl(),
'code' => $code,
],
]);
return json_decode($response->getBody(), true);
}
/**
* Fetch new access token or return existing one
* @param int $minLifetime If token life time smaller this value then it will fetch new token
* eg. current token expires in 50 sec, but we need 60, then it fetch new token
* @return mixed
* @throws GuzzleException
*/
function getAccessToken($minLifetime = 600)
{
$key = $this->clientId . $this->clientSecret . $this->refreshToken;
$token = $this->getCache()->get($key);
if (!$token) {
$token = [];
$token['refresh_token'] = $this->refreshToken;
}
$token_created_at = isset($token['created_at']) ? $token['created_at'] : 0;
$token_expired_in = isset($token['expires_in']) ? $token['expires_in'] : 0;
$willBeExpireIn = $token_expired_in + $token_created_at - time();
if (empty($token['access_token']) || $willBeExpireIn <= $minLifetime) {
$token = $this->fetchAccessTokenWithRefreshToken($this->refreshToken);
$token['created_at'] = time();
if (!empty($token['access_token'])) {
$this->getCache()->put($key, $token, 0);
}
}
return $token['access_token'];
}
/**
* @param string $refresh_token
* @return mixed
* @throws GuzzleException
*/
function fetchAccessTokenWithRefreshToken($refresh_token = '')
{
$refresh_token = $refresh_token ?: $this->refreshToken;
$postKey = 'form_params';
$response = $this->getHttpClient()->post(
str_replace('{tenant}', $this->tenantId, $this->tokenEndpoint),
[
'headers' => ['Accept' => 'application/json'],
$postKey => [
'client_id' => $this->clientId, 'client_secret' => $this->clientSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refresh_token,
],
]);
return json_decode($response->getBody(), true);
}
/**
* Get a instance of the Guzzle HTTP client.
*
* @return Client
*/
protected function getHttpClient()
{
if (is_null($this->httpClient)) {
$this->httpClient = new Client();
}
return $this->httpClient;
}
/**
* @return TempCache|Store
*/
public function getCache()
{
if (!$this->cache) {
$this->cache = new TempCache(static::class);
}
return $this->cache;
}
/**
* @param string $clientId
* @return OneDriveOauth
*/
public function setClientId(string $clientId): OneDriveOauth
{
$this->clientId = $clientId;
return $this;
}
/**
* @param string $clientSecret
* @return OneDriveOauth
*/
public function setClientSecret(string $clientSecret): OneDriveOauth
{
$this->clientSecret = $clientSecret;
return $this;
}
/**
* @param mixed $accessToken
* @return OneDriveOauth
*/
public function setAccessToken($accessToken)
{
$this->accessToken = $accessToken;
return $this;
}
/**
* @param string $refreshToken
* @return OneDriveOauth
*/
public function setRefreshToken(string $refreshToken): OneDriveOauth
{
$this->refreshToken = $refreshToken;
return $this;
}
/**
* @param mixed $httpClient
* @return OneDriveOauth
*/
public function setHttpClient($httpClient)
{
$this->httpClient = $httpClient;
return $this;
}
/**
* @param $cache
* @return OneDriveOauth
*/
public function setCache(Store $cache): OneDriveOauth
{
$this->cache = $cache;
return $this;
}
/**
* @param string $tokenEndpoint
* @return OneDriveOauth
*/
public function setTokenEndpoint(string $tokenEndpoint): OneDriveOauth
{
$this->tokenEndpoint = $tokenEndpoint;
return $this;
}
/**
* @return mixed
*/
public function getTenantId()
{
return $this->tenantId;
}
/**
* @param mixed $tenantId
* @return OneDriveOauth
*/
public function setTenantId($tenantId)
{
$this->tenantId = $tenantId;
return $this;
}
/**
* @return string
*/
public function getOauthUrl(): string
{
return $this->oauthUrl;
}
/**
* @param string $oauthUrl
* @return OneDriveOauth
*/
public function setOauthUrl(string $oauthUrl): OneDriveOauth
{
$this->oauthUrl = $oauthUrl;
return $this;
}
/**
* @return string
*/
public function getClientId(): string
{
return $this->clientId;
}
/**
* @return string
*/
public function getClientSecret(): string
{
return $this->clientSecret;
}
/**
* @return string
*/
public function getRefreshToken(): string
{
return $this->refreshToken;
}
/**
* @return string
*/
public function getTokenEndpoint(): string
{
return $this->tokenEndpoint;
}
/**
* @return string[]
*/
public function getOauthParams(): array
{
return $this->oauthParams;
}
/**
* @param string[] $oauthParams
* @return OneDriveOauth
*/
public function setOauthParams(array $oauthParams): OneDriveOauth
{
$this->oauthParams = $oauthParams;
return $this;
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
namespace As247\CloudStorages\Support;
use As247\CloudStorages\Exception\PathOutsideRootException;
use LogicException;
class Path
{
/**
* Clean the path
* ./a/b/d/..//c.txt will return /a/b/c.txt
* @param $path
* @return string
*/
public static function clean($path){
return static::cleanPath($path);
}
public static function countSegments($path){
return static::cleanPath($path,'count');
}
/**
* Explode path
* /a/b/c.txt will return ['/','a','b','c.txt']
* @param $path
* @return array
*/
public static function explode($path){
return static::cleanPath($path,'array');
}
public static function join(...$paths){
return static::cleanPath($paths);
}
protected static function cleanPath($path,$return='string'){
if(is_array($path)){
$path=join('/',$path);
}
if(!is_string($path)){
$path=(string)$path;
}
$path=static::normalizeRelativePath($path);
if($return==='string'){
$path='/'.join('/',$path);
}elseif($return==='count'){
return count($path);
}else{
array_unshift($path,'/');
}
return $path;
}
public static function replace($search, $replace, $subject){
$pos = strpos($subject, $search);
if ($pos === 0) {
$subject = substr_replace($subject, $replace, $pos, strlen($search));
}
return $subject;
}
/**
* Normalize relative directories in a path.
*
* @param string $path
*
* @throws LogicException
*
* @return array
*/
protected static function normalizeRelativePath(string $path)
{
$path = str_replace('\\', '/', $path);
$path = static::removeFunkyWhiteSpace($path);
$parts = [];
foreach (explode('/', $path) as $part) {
switch ($part) {
case '':
case '.':
break;
case '..':
if (empty($parts)) {
throw PathOutsideRootException::atLocation($path);
}
array_pop($parts);
break;
default:
$parts[] = $part;
break;
}
}
return $parts;
}
/**
* Removes unprintable characters and invalid unicode characters.
*
* @param string $path
*
* @return string $path
*/
protected static function removeFunkyWhiteSpace(string $path)
{
// We do this check in a loop, since removing invalid unicode characters
// can lead to new characters being created.
while (preg_match('#\p{C}+|^\./#u', $path)) {
$path = preg_replace('#\p{C}+|^\./#u', '', $path);
}
return $path;
}
}
@@ -0,0 +1,30 @@
<?php
namespace As247\CloudStorages\Support;
use ArrayAccess;
use JsonSerializable;
interface StorageAttributes extends JsonSerializable, ArrayAccess
{
public const TYPE_FILE = 'file';
public const TYPE_DIRECTORY = 'dir';
public const ATTRIBUTE_PATH = 'path';
public const ATTRIBUTE_TYPE = 'type';
public const ATTRIBUTE_VISIBILITY = 'visibility';
public const ATTRIBUTE_LAST_MODIFIED = 'last_modified';
public const ATTRIBUTE_FILE_SIZE = 'file_size';
public const ATTRIBUTE_MIME_TYPE = 'mime_type';
public function path(): string;
public function type(): string;
public function visibility(): ?string;
public static function fromArray(array $attributes);
public function isFile(): bool;
public function isDir(): bool;
}
@@ -0,0 +1,243 @@
<?php
namespace As247\CloudStorages\Support;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Storage\AList;
use As247\CloudStorages\Storage\GoogleDrive;
use As247\CloudStorages\Storage\OneDrive;
use As247\CloudStorages\Support\StorageAttributes;
use GuzzleHttp\Psr7\Utils;
use League\Flysystem\Config;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\FileAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnableToWriteFile;
trait StorageToAdapter
{
use ConfigConverter;
/**
* @var StorageContract
*/
protected $storage;
/**
* @return StorageContract|OneDrive|GoogleDrive|AList
*/
public function getStorage()
{
return $this->storage;
}
public function fileExists(string $path): bool
{
try {
return $this->getMetadata($path) instanceof FileAttributes;
} catch (\Exception $e) {
return false;
}
}
public function directoryExists(string $path): bool
{
try {
return $this->getMetadata($path) instanceof DirectoryAttributes;
} catch (\Exception $e) {
return false;
}
}
public function write(string $path, string $contents, Config $config): void
{
$this->writeStream($path, Utils::streamFor($contents), $config);
}
public function writeStream(string $path, $contents, Config $config): void
{
try {
$config = $this->convertConfig($config);
$this->storage->writeStream($this->prefixer->prefixPath($path), $contents, $config);
}catch (\Exception $e){
throw UnableToWriteFile::atLocation($path, $e->getMessage(), $e);
}
}
public function read(string $path): string
{
return stream_get_contents($this->readStream($path));
}
public function readStream(string $path)
{
try {
return $this->storage->readStream($this->prefixer->prefixPath($path));
} catch (\Exception $e) {
throw \League\Flysystem\UnableToReadFile::fromLocation($path, $e->getMessage(), $e);
}
}
public function delete(string $path): void
{
if ($this->isRootPath($path)) {
return ;
}
try {
$this->storage->delete($this->prefixer->prefixPath($path));
}catch (FileNotFoundException $e){
return ;
}
catch (\Exception $e) {
throw \League\Flysystem\UnableToDeleteFile::atLocation($path, $e->getMessage(), $e);
}
}
public function deleteDirectory(string $path): void
{
if ($this->isRootPath($path)) {
return ;
}
try {
$this->storage->deleteDirectory($this->prefixer->prefixPath($path));
} catch(\Exception $e){
throw \League\Flysystem\UnableToDeleteDirectory::atLocation($path, $e->getMessage(), $e);
}
}
public function createDirectory(string $path, Config $config): void
{
try {
$config = $this->convertConfig($config);
$this->storage->createDirectory($this->prefixer->prefixPath($path), $config);
} catch (\Exception $e) {
throw \League\Flysystem\UnableToCreateDirectory::atLocation($path, $e->getMessage(), $e);
}
}
public function setVisibility(string $path, string $visibility): void
{
try {
$this->storage->setVisibility($this->prefixer->prefixPath($path), $visibility);
}catch (\Exception $e){
throw \League\Flysystem\UnableToSetVisibility::atLocation($path, $e->getMessage(), $e);
}
}
public function visibility(string $path): FileAttributes
{
return $this->getMetadata($path);
}
public function mimeType(string $path): FileAttributes
{
$meta=$this->getMetadata($path);
if($meta instanceof FileAttributes){
if($meta->mimeType()!=='application/octet-stream') {
return $meta;
}
}
throw UnableToRetrieveMetadata::create($path, 'mimeType');
}
public function lastModified(string $path): FileAttributes
{
return $this->getMetadata($path);
}
public function fileSize(string $path): FileAttributes
{
$meta= $this->getMetadata($path);
if($meta instanceof FileAttributes){
return $meta;
}
throw UnableToRetrieveMetadata::create($path, 'fileSize');
}
public function listContents(string $path, bool $deep): iterable
{
$contents=$this->storage->listContents($this->prefixer->prefixPath($path), $deep);
foreach ($contents as $key=>$content){
$path=$this->prefixer->stripPrefix($content['path']);
$visibility=$content[StorageAttributes::ATTRIBUTE_VISIBILITY];
$lastModified=$content[StorageAttributes::ATTRIBUTE_LAST_MODIFIED];
$fileSize=$content[StorageAttributes::ATTRIBUTE_FILE_SIZE];
$isDirectory=$content[StorageAttributes::ATTRIBUTE_TYPE]===StorageAttributes::TYPE_DIRECTORY;
yield $isDirectory ?
new DirectoryAttributes($path, $visibility, $lastModified) :
new FileAttributes(
str_replace('\\', '/', $path),
$fileSize,
$visibility,
$lastModified
);
}
}
public function move(string $source, string $destination, Config $config): void
{
try {
$source = $this->prefixer->prefixPath($source);
$destination = $this->prefixer->prefixPath($destination);
$this->storage->move($source, $destination, $this->convertConfig($config));
} catch (\Exception $e) {
throw \League\Flysystem\UnableToMoveFile::fromLocationTo($source, $destination, $e);
}
}
public function copy(string $source, string $destination, Config $config): void
{
try {
$config = $this->convertConfig($config);
$source = $this->prefixer->prefixPath($source);
//Flysystem expect to overwrite the file then we try to delete the destination file first
$this->delete($destination);
$destination = $this->prefixer->prefixPath($destination);
$this->storage->copy($source, $destination, $config);
} catch (\Exception $e) {
throw \League\Flysystem\UnableToCopyFile::fromLocationTo($source, $destination, $e);
}
}
/**
* @param $path
* @return FileAttributes|DirectoryAttributes|bool
*/
public function getMetadata($path)
{
try {
$meta = $this->storage->getMetadata($this->prefixer->prefixPath($path));
return $meta->type()===StorageAttributes::TYPE_DIRECTORY ?
new DirectoryAttributes($path, $meta->visibility(), $meta->lastModified()) :
new FileAttributes(
str_replace('\\', '/', $path),
$meta->fileSize(),
$meta->visibility(),
$meta->lastModified(),
$meta->mimeType()
);
} catch (\Exception $e) {
throw UnableToRetrieveMetadata::create($path, 'metadata', $e);
}
}
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config): string
{
try {
return $this->storage->temporaryUrl($this->prefixer->prefixPath($path), $expiresAt, $this->convertConfig($config));
} catch (\Exception $e) {
throw UnableToRetrieveMetadata::create($path, 'temporaryUrl', $e);
}
}
protected function isRootPath($path)
{
if ($this->prefixer->prefixPath($path) === $this->prefixer->prefixPath('')) {
return true;
}
return false;
}
}
@@ -0,0 +1,321 @@
<?php
namespace As247\CloudStorages\Support;
use As247\CloudStorages\Contracts\Storage\StorageContract;
use As247\CloudStorages\Exception\FileNotFoundException;
use As247\CloudStorages\Exception\StorageException;
use As247\CloudStorages\Storage\GoogleDrive;
use As247\CloudStorages\Storage\OneDrive;
use GuzzleHttp\Psr7\Utils;
use League\Flysystem\Config;
use League\Flysystem\Util;
trait StorageToAdapterV1
{
use ConfigConverter;
/**
* @var StorageContract
*/
protected $storage;
protected $throwException = false;
protected $exceptExceptions = [
FileNotFoundException::class,
];
/**
* @return StorageContract|OneDrive|GoogleDrive
*/
public function getStorage()
{
return $this->storage;
}
/**
* @inheritDoc
*/
public function write($path, $contents, Config $config = null)
{
return $this->writeStream($path, Utils::streamFor($contents), $config);
}
/**
* @inheritDoc
*/
public function writeStream($path, $resource, Config $config)
{
try {
$config = $this->convertConfig($config);
$this->storage->writeStream($this->applyPathPrefix($path), $resource, $config);
return $this->getMetadata($path);
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function update($path, $contents, Config $config)
{
return $this->write($path, $contents, $config);
}
/**
* @inheritDoc
*/
public function updateStream($path, $resource, Config $config)
{
return $this->writeStream($path, $resource, $config);
}
/**
* @inheritDoc
*/
public function rename($path, $newpath)
{
try {
$path = $this->applyPathPrefix($path);
$newpath = $this->applyPathPrefix($newpath);
$this->storage->move($path, $newpath, $this->convertConfig(new Config()));
return true;
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function copy($path, $newpath)
{
try {
$config = $this->convertConfig(new Config());
$path = $this->applyPathPrefix($path);
$newpath = $this->applyPathPrefix($newpath);
$this->storage->copy($path, $newpath, $config);
return true;
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function delete($path)
{
if ($this->isRootPath($path)) {
return false;
}
try {
$this->storage->delete($this->applyPathPrefix($path));
return true;
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
} catch (FileNotFoundException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function deleteDir($dirname)
{
if ($this->isRootPath($dirname)) {
return false;
}
try {
$this->storage->deleteDirectory($this->applyPathPrefix($dirname));
return true;
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
} catch (FileNotFoundException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function createDir($dirname, Config $config)
{
try {
$config = $this->convertConfig($config);
$this->storage->createDirectory($this->applyPathPrefix($dirname), $config);
return $this->getMetadata($dirname);
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function setVisibility($path, $visibility)
{
$this->storage->setVisibility($this->applyPathPrefix($path), $visibility);
return $this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function has($path)
{
return (bool)$this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function read($path)
{
$stream = $this->readStream($path);
return ['contents' => stream_get_contents($stream['stream'])];
}
/**
* @inheritDoc
*/
public function readStream($path)
{
try {
return ['stream' => $this->storage->readStream($this->applyPathPrefix($path))];
} catch (StorageException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function listContents($directory = '', $recursive = false)
{
$contents = array_values(iterator_to_array($this->storage->listContents($this->applyPathPrefix($directory), $recursive), false));
$contents = array_map(function ($v) {
$v['path'] = $this->removePathPrefix($v['path']);
return $v;
}, $contents);
return $contents;
}
/**
* @inheritDoc
*/
public function getMetadata($path)
{
try {
$meta = $this->storage->getMetadata($this->applyPathPrefix($path));
return $meta->toArrayV1();
} catch (FileNotFoundException $e) {
if ($this->shouldThrowException($e)) {
throw $e;
}
return false;
}
}
/**
* @inheritDoc
*/
public function getSize($path)
{
return $this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function getMimetype($path)
{
return $this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function getTimestamp($path)
{
return $this->getMetadata($path);
}
/**
* @inheritDoc
*/
public function getVisibility($path)
{
return $this->getMetadata($path);
}
public function setPathPrefix($path)
{
parent::setPathPrefix(Util::normalizePath($path));
}
public function applyPathPrefix($path)
{
return Util::normalizePath(parent::applyPathPrefix($path));
}
protected function isRootPath($path)
{
if ($this->applyPathPrefix($path) === $this->applyPathPrefix('')) {
return true;
}
return false;
}
protected function shouldThrowException($e)
{
if(!$this->throwException){
return false;
}
if (empty($this->exceptExceptions)) {
return $this->throwException;
}
return !in_array(get_class($e), $this->exceptExceptions);
}
public function setExcerptExceptions($exceptions)
{
$this->exceptExceptions = $exceptions;
return $this;
}
public function getExcerptExceptions()
{
return $this->exceptExceptions;
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"name": "as247/flysystem-google-drive",
"description": "Google Drive Adapter for Flysystem",
"keywords": ["php", "storage", "drive","google","google-drive"],
"homepage": "https://github.com/as247",
"license": "MIT",
"authors": [
{
"name": "As247",
"email": "as247@vui360.com",
"homepage": "http://as247.vui360.com"
}
],
"require": {
"php": ">=7.1",
"ext-json": "*",
"as247/cloud-storages": "^1.2.4",
"league/flysystem": "^3.23",
"google/apiclient": "^2.0"
},
"autoload": {
"psr-4": {
"As247\\Flysystem\\GoogleDrive\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"As247\\Flysystem\\GoogleDrive\\Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
+38
View File
@@ -0,0 +1,38 @@
# Flysystem Adapter for Google Drive
[![Author](https://img.shields.io/badge/author-as247-orange)](http://as247.vui360.com/)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
## Installation
```bash
composer require as247/flysystem-google-drive
```
## Usage
#### Follow [Google Docs](https://developers.google.com/drive/v3/web/enable-sdk) to obtain your `ClientId, ClientSecret & refreshToken`
#### In addition, you can also check these easy-to-follow tutorial by [@ivanvermeyen](https://github.com/ivanvermeyen/laravel-google-drive-demo)
- [Getting your Client ID and Secret](https://github.com/ivanvermeyen/laravel-google-drive-demo/blob/master/README/1-getting-your-dlient-id-and-secret.md)
- [Getting your Refresh Token](https://github.com/ivanvermeyen/laravel-google-drive-demo/blob/master/README/2-getting-your-refresh-token.md)
```php
$client = new \Google_Client();
$client->setClientId('[app client id].apps.googleusercontent.com');
$client->setClientSecret('[app client secret]');
$client->fetchAccessTokenWithRefreshToken('[your refresh token]');
$service = new \Google_Service_Drive($client);
$options=[
'root'=>'[Root folder id]',
'teamDrive'=>'[Team drive id]'//If your root folder inside team drive
'google_drive_adapter_prefix'=>'[Path prefix]',//Path prefix inside root folder
];
$adapter = new \As247\Flysystem\GoogleDrive\GoogleDriveAdapter($service, $options);
$filesystem = new \League\Flysystem\Filesystem($adapter);
```
@@ -0,0 +1,29 @@
<?php
/**
* Created by PhpStorm.
* User: alt
* Date: 04-Oct-18
* Time: 10:46 PM
*/
namespace As247\Flysystem\GoogleDrive;
use As247\CloudStorages\Storage\GoogleDrive;
use As247\CloudStorages\Support\StorageToAdapter;
use Google_Service_Drive;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\PathPrefixer;
class GoogleDriveAdapter implements FilesystemAdapter
{
use StorageToAdapter;
protected $prefixer;
public function __construct(Google_Service_Drive $service, $options = [])
{
if(!is_array($options)){
$options=['root'=>$options];
}
$this->storage = new GoogleDrive($service,$options);
$this->prefixer = new PathPrefixer($options['google_drive_adapter_prefix']??'', DIRECTORY_SEPARATOR);
}
}
Vendored Executable
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../sabre/vobject/bin/generate_vcards)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards');
}
}
return include __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards';
Vendored Executable
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env sh
# Support bash to support `source` with fallback on $0 if this does not run with bash
# https://stackoverflow.com/a/35006505/6512
selfArg="$BASH_SOURCE"
if [ -z "$selfArg" ]; then
selfArg="$0"
fi
self=$(realpath $selfArg 2> /dev/null)
if [ -z "$self" ]; then
self="$selfArg"
fi
dir=$(cd "${self%[/\\]*}" > /dev/null; cd '../sabre/dav/bin' && pwd)
if [ -d /proc/cygdrive ]; then
case $(which php) in
$(readlink -n /proc/cygdrive)/*)
# We are in Cygwin using Windows php, so the path must be translated
dir=$(cygpath -m "$dir");
;;
esac
fi
export COMPOSER_RUNTIME_BIN_DIR="$(cd "${self%[/\\]*}" > /dev/null; pwd)"
# If bash is sourcing this file, we have to source the target as well
bashSource="$BASH_SOURCE"
if [ -n "$bashSource" ]; then
if [ "$bashSource" != "$0" ]; then
source "${dir}/naturalselection" "$@"
return
fi
fi
exec "${dir}/naturalselection" "$@"
Vendored Executable
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env sh
# Support bash to support `source` with fallback on $0 if this does not run with bash
# https://stackoverflow.com/a/35006505/6512
selfArg="$BASH_SOURCE"
if [ -z "$selfArg" ]; then
selfArg="$0"
fi
self=$(realpath $selfArg 2> /dev/null)
if [ -z "$self" ]; then
self="$selfArg"
fi
dir=$(cd "${self%[/\\]*}" > /dev/null; cd '../sabre/dav/bin' && pwd)
if [ -d /proc/cygdrive ]; then
case $(which php) in
$(readlink -n /proc/cygdrive)/*)
# We are in Cygwin using Windows php, so the path must be translated
dir=$(cygpath -m "$dir");
;;
esac
fi
export COMPOSER_RUNTIME_BIN_DIR="$(cd "${self%[/\\]*}" > /dev/null; pwd)"
# If bash is sourcing this file, we have to source the target as well
bashSource="$BASH_SOURCE"
if [ -n "$bashSource" ]; then
if [ "$bashSource" != "$0" ]; then
source "${dir}/sabredav" "$@"
return
fi
fi
exec "${dir}/sabredav" "$@"
Vendored Executable
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../sabre/vobject/bin/vobject)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/vobject');
}
}
return include __DIR__ . '/..'.'/sabre/vobject/bin/vobject';
File diff suppressed because it is too large Load Diff
+12 -3
View File
@@ -7,23 +7,32 @@ $baseDir = dirname($vendorDir);
return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'2b9d0f43f9552984cfa82fee95491826' => $vendorDir . '/sabre/event/lib/coroutine.php',
'd81bab31d3feb45bfe2f283ea3c8fdf7' => $vendorDir . '/sabre/event/lib/Loop/functions.php',
'a1cce3d26cc15c00fcd0b3354bd72c88' => $vendorDir . '/sabre/event/lib/Promise/functions.php',
'3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php',
'93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php',
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
'b33e3d135e5d9e47d845c576147bda89' => $vendorDir . '/php-di/php-di/src/functions.php',
'1f87db08236948d07391152dccb70f04' => $vendorDir . '/google/apiclient-services/autoload.php',
'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php',
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'a8d3953fd9959404dd22d3dfcd0a79f0' => $vendorDir . '/google/apiclient/src/aliases.php',
'23dd7ece5822da3d0100ef3deb0ef55f' => $vendorDir . '/laravel/agent-detector/src/functions.php',
'47e1160838b5e5a10346ac4084b58c23' => $vendorDir . '/laravel/prompts/src/helpers.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
+14
View File
@@ -47,6 +47,12 @@ return array(
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
'Spatie\\Permission\\' => array($vendorDir . '/spatie/laravel-permission/src'),
'Spatie\\LaravelPackageTools\\' => array($vendorDir . '/spatie/laravel-package-tools/src'),
'Sabre\\Xml\\' => array($vendorDir . '/sabre/xml/lib'),
'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'),
'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'),
'Sabre\\HTTP\\' => array($vendorDir . '/sabre/http/lib'),
'Sabre\\Event\\' => array($vendorDir . '/sabre/event/lib'),
'Sabre\\' => array($vendorDir . '/sabre/dav/lib'),
'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
'Psy\\' => array($vendorDir . '/psy/psysh/src'),
@@ -57,6 +63,7 @@ return array(
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
@@ -65,6 +72,7 @@ return array(
'Mockery\\' => array($vendorDir . '/mockery/mockery/library/Mockery'),
'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'),
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
'League\\Flysystem\\WebDAV\\' => array($vendorDir . '/league/flysystem-webdav'),
'League\\Flysystem\\Local\\' => array($vendorDir . '/league/flysystem-local'),
'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'),
'League\\Config\\' => array($vendorDir . '/league/config/src'),
@@ -83,7 +91,11 @@ return array(
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'),
'Google\\Service\\' => array($vendorDir . '/google/apiclient-services/src'),
'Google\\Auth\\' => array($vendorDir . '/google/auth/src'),
'Google\\' => array($vendorDir . '/google/apiclient/src'),
'Fruitcake\\Cors\\' => array($vendorDir . '/fruitcake/php-cors/src'),
'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
'Faker\\' => array($vendorDir . '/fakerphp/faker/src/Faker'),
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
@@ -100,5 +112,7 @@ return array(
'Carbon\\Doctrine\\' => array($vendorDir . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine'),
'Carbon\\' => array($vendorDir . '/nesbot/carbon/src/Carbon'),
'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
'As247\\Flysystem\\GoogleDrive\\' => array($vendorDir . '/as247/flysystem-google-drive/src'),
'As247\\CloudStorages\\' => array($vendorDir . '/as247/cloud-storages/src'),
'App\\' => array($baseDir . '/app', $vendorDir . '/laravel/pint/app'),
);
+34426 -3
View File
File diff suppressed because it is too large Load Diff
+929
View File
@@ -1,5 +1,115 @@
{
"packages": [
{
"name": "as247/cloud-storages",
"version": "v1.2.9",
"version_normalized": "1.2.9.0",
"source": {
"type": "git",
"url": "https://github.com/as247/cloud-storages.git",
"reference": "6a8ef430f7f07a2e88ea6b7cfd32230da493c1f8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/as247/cloud-storages/zipball/6a8ef430f7f07a2e88ea6b7cfd32230da493c1f8",
"reference": "6a8ef430f7f07a2e88ea6b7cfd32230da493c1f8",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=7.1"
},
"time": "2024-03-01T10:56:22+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"As247\\CloudStorages\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "As247",
"email": "as247@vui360.com",
"homepage": "http://as247.vui360.com"
}
],
"description": "Storages wrapper with path support for drive services: google drive, onedrive,...",
"homepage": "https://github.com/as247",
"keywords": [
"drive",
"google",
"google drive",
"one drive",
"php",
"storage"
],
"support": {
"issues": "https://github.com/as247/cloud-storages/issues",
"source": "https://github.com/as247/cloud-storages/tree/v1.2.9"
},
"install-path": "../as247/cloud-storages"
},
{
"name": "as247/flysystem-google-drive",
"version": "v3.0.2",
"version_normalized": "3.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/as247/flysystem-google-drive.git",
"reference": "7d2600c5e085cafbb49c9874d1efb6795d36cb2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/as247/flysystem-google-drive/zipball/7d2600c5e085cafbb49c9874d1efb6795d36cb2f",
"reference": "7d2600c5e085cafbb49c9874d1efb6795d36cb2f",
"shasum": ""
},
"require": {
"as247/cloud-storages": "^1.2.4",
"ext-json": "*",
"google/apiclient": "^2.0",
"league/flysystem": "^3.23",
"php": ">=7.1"
},
"time": "2024-05-30T14:41:37+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"As247\\Flysystem\\GoogleDrive\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "As247",
"email": "as247@vui360.com",
"homepage": "http://as247.vui360.com"
}
],
"description": "Google Drive Adapter for Flysystem",
"homepage": "https://github.com/as247",
"keywords": [
"drive",
"google",
"google-drive",
"php",
"storage"
],
"support": {
"issues": "https://github.com/as247/flysystem-google-drive/issues",
"source": "https://github.com/as247/flysystem-google-drive/tree/v3.0.2"
},
"install-path": "../as247/flysystem-google-drive"
},
{
"name": "brick/math",
"version": "0.14.8",
@@ -796,6 +906,73 @@
],
"install-path": "../filp/whoops"
},
{
"name": "firebase/php-jwt",
"version": "v7.0.5",
"version_normalized": "7.0.5.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/php-jwt.git",
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
"reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpfastcache/phpfastcache": "^9.2",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"time": "2026-04-01T20:38:03+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
],
"support": {
"issues": "https://github.com/googleapis/php-jwt/issues",
"source": "https://github.com/googleapis/php-jwt/tree/v7.0.5"
},
"install-path": "../firebase/php-jwt"
},
{
"name": "fruitcake/php-cors",
"version": "v1.4.0",
@@ -870,6 +1047,192 @@
],
"install-path": "../fruitcake/php-cors"
},
{
"name": "google/apiclient",
"version": "v2.19.3",
"version_normalized": "2.19.3.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client.git",
"reference": "a1f02761994fd9defb20f6f1449205fd66f450de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/a1f02761994fd9defb20f6f1449205fd66f450de",
"reference": "a1f02761994fd9defb20f6f1449205fd66f450de",
"shasum": ""
},
"require": {
"firebase/php-jwt": "^6.0||^7.0",
"google/apiclient-services": "~0.350",
"google/auth": "^1.37",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.6",
"monolog/monolog": "^2.9||^3.0",
"php": "^8.1"
},
"require-dev": {
"cache/filesystem-adapter": "^1.1",
"composer/composer": "^2.9",
"phpcompatibility/php-compatibility": "^9.2",
"phpspec/prophecy-phpunit": "^2.1",
"phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.8",
"symfony/css-selector": "~2.1",
"symfony/dom-crawler": "~2.1"
},
"suggest": {
"cache/filesystem-adapter": "For caching certs and tokens (using Google\\Client::setCache)"
},
"time": "2026-05-04T21:00:36+00:00",
"type": "library",
"extra": {
"component": {
"entry": "src/Client.php"
},
"branch-alias": {
"dev-main": "2.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"src/aliases.php"
],
"psr-4": {
"Google\\": "src/"
},
"classmap": [
"src/aliases.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Client library for Google APIs",
"homepage": "http://developers.google.com/api-client-library/php",
"keywords": [
"google"
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client/issues",
"source": "https://github.com/googleapis/google-api-php-client/tree/v2.19.3"
},
"install-path": "../google/apiclient"
},
{
"name": "google/apiclient-services",
"version": "v0.442.0",
"version_normalized": "0.442.0.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
"reference": "3afe7b5c9d1cebf2a5be018820ce62eeb906b81f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/3afe7b5c9d1cebf2a5be018820ce62eeb906b81f",
"reference": "3afe7b5c9d1cebf2a5be018820ce62eeb906b81f",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6"
},
"time": "2026-05-25T01:34:24+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"autoload.php"
],
"psr-4": {
"Google\\Service\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Client library for Google APIs",
"homepage": "http://developers.google.com/api-client-library/php",
"keywords": [
"google"
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.442.0"
},
"install-path": "../google/apiclient-services"
},
{
"name": "google/auth",
"version": "v1.50.1",
"version_normalized": "1.50.1.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-auth-library-php.git",
"reference": "870c17ee3a1d73338d39a9ffa77a700ba77f5a83"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/870c17ee3a1d73338d39a9ffa77a700ba77f5a83",
"reference": "870c17ee3a1d73338d39a9ffa77a700ba77f5a83",
"shasum": ""
},
"require": {
"firebase/php-jwt": "^6.0||^7.0",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.4.5",
"php": "^8.1",
"psr/cache": "^2.0||^3.0",
"psr/http-message": "^1.1||^2.0",
"psr/log": "^2.0||^3.0"
},
"require-dev": {
"guzzlehttp/promises": "^2.0",
"kelvinmo/simplejwt": "^1.1.0",
"phpseclib/phpseclib": "^3.0.35",
"phpspec/prophecy-phpunit": "^2.1",
"phpunit/phpunit": "^9.6",
"sebastian/comparator": ">=1.2.3",
"squizlabs/php_codesniffer": "^4.0",
"symfony/filesystem": "^6.3||^7.3",
"symfony/process": "^6.0||^7.0",
"webmozart/assert": "^1.11||^2.0"
},
"suggest": {
"phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
},
"time": "2026-03-18T20:03:29+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Google\\Auth\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Google Auth Library for PHP",
"homepage": "https://github.com/google/google-auth-library-php",
"keywords": [
"Authentication",
"google",
"oauth2"
],
"support": {
"docs": "https://cloud.google.com/php/docs/reference/auth/latest",
"issues": "https://github.com/googleapis/google-auth-library-php/issues",
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.50.1"
},
"install-path": "../google/auth"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.4",
@@ -2477,6 +2840,57 @@
},
"install-path": "../league/flysystem-local"
},
{
"name": "league/flysystem-webdav",
"version": "3.31.0",
"version_normalized": "3.31.0.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-webdav.git",
"reference": "1705b5d4aab6d755a78ea850e523dda9c58ad20f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-webdav/zipball/1705b5d4aab6d755a78ea850e523dda9c58ad20f",
"reference": "1705b5d4aab6d755a78ea850e523dda9c58ad20f",
"shasum": ""
},
"require": {
"league/flysystem": "^3.6.0",
"php": "^8.0.2",
"sabre/dav": "^4.6.0"
},
"time": "2026-01-23T15:30:45+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"League\\Flysystem\\WebDAV\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
],
"description": "WebDAV filesystem adapter for Flysystem.",
"keywords": [
"Flysystem",
"WebDAV",
"file",
"files",
"filesystem"
],
"support": {
"source": "https://github.com/thephpleague/flysystem-webdav/tree/3.31.0"
},
"install-path": "../league/flysystem-webdav"
},
{
"name": "league/mime-type-detection",
"version": "1.16.0",
@@ -4290,6 +4704,58 @@
],
"install-path": "../phpunit/phpunit"
},
{
"name": "psr/cache",
"version": "3.0.0",
"version_normalized": "3.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"time": "2021-02-03T23:26:27+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/3.0.0"
},
"install-path": "../psr/cache"
},
{
"name": "psr/clock",
"version": "1.0.0",
@@ -5015,6 +5481,469 @@
},
"install-path": "../ramsey/uuid"
},
{
"name": "sabre/dav",
"version": "4.7.0",
"version_normalized": "4.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/dav.git",
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-iconv": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"ext-spl": "*",
"lib-libxml": ">=2.7.0",
"php": "^7.1.0 || ^8.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"sabre/event": "^5.0",
"sabre/http": "^5.0.5",
"sabre/uri": "^2.0",
"sabre/vobject": "^4.2.1",
"sabre/xml": "^2.0.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.19",
"monolog/monolog": "^1.27 || ^2.0",
"phpstan/phpstan": "^0.12 || ^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"suggest": {
"ext-curl": "*",
"ext-imap": "*",
"ext-pdo": "*"
},
"time": "2024-10-29T11:46:02+00:00",
"bin": [
"bin/sabredav",
"bin/naturalselection"
],
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Sabre\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "WebDAV Framework for PHP",
"homepage": "http://sabre.io/",
"keywords": [
"CalDAV",
"CardDAV",
"WebDAV",
"framework",
"iCalendar"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/dav/issues",
"source": "https://github.com/fruux/sabre-dav"
},
"install-path": "../sabre/dav"
},
{
"name": "sabre/event",
"version": "5.1.8",
"version_normalized": "5.1.8.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/event.git",
"reference": "1dd5f55421b0092006510264131a4d632d02d2c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/event/zipball/1dd5f55421b0092006510264131a4d632d02d2c1",
"reference": "1dd5f55421b0092006510264131a4d632d02d2c1",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.17.1||^3.95",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"time": "2026-04-27T04:19:36+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"lib/coroutine.php",
"lib/Loop/functions.php",
"lib/Promise/functions.php"
],
"psr-4": {
"Sabre\\Event\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "sabre/event is a library for lightweight event-based programming",
"homepage": "http://sabre.io/event/",
"keywords": [
"EventEmitter",
"async",
"coroutine",
"eventloop",
"events",
"hooks",
"plugin",
"promise",
"reactor",
"signal"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/event/issues",
"source": "https://github.com/fruux/sabre-event"
},
"install-path": "../sabre/event"
},
{
"name": "sabre/http",
"version": "5.1.13",
"version_normalized": "5.1.13.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/http.git",
"reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/http/zipball/7c2a14097d1a0de2347dcbdc91a02f38e338f4db",
"reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-curl": "*",
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabre/event": ">=4.0 <6.0",
"sabre/uri": "^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.17.1||3.63.2",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"suggest": {
"ext-curl": " to make http requests with the Client class"
},
"time": "2025-09-09T10:21:47+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Sabre\\HTTP\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
"homepage": "https://github.com/fruux/sabre-http",
"keywords": [
"http"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/http/issues",
"source": "https://github.com/fruux/sabre-http"
},
"install-path": "../sabre/http"
},
{
"name": "sabre/uri",
"version": "2.3.4",
"version_normalized": "2.3.4.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/uri.git",
"reference": "b76524c22de90d80ca73143680a8e77b1266c291"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291",
"reference": "b76524c22de90d80ca73143680a8e77b1266c291",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.63",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^1.12",
"phpstan/phpstan-phpunit": "^1.4",
"phpstan/phpstan-strict-rules": "^1.6",
"phpunit/phpunit": "^9.6"
},
"time": "2024-08-27T12:18:16+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Sabre\\Uri\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "Functions for making sense out of URIs.",
"homepage": "http://sabre.io/uri/",
"keywords": [
"rfc3986",
"uri",
"url"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/uri/issues",
"source": "https://github.com/fruux/sabre-uri"
},
"install-path": "../sabre/uri"
},
{
"name": "sabre/vobject",
"version": "4.5.8",
"version_normalized": "4.5.8.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/vobject.git",
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabre/xml": "^2.1 || ^3.0 || ^4.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.17.1",
"phpstan/phpstan": "^0.12 || ^1.12 || ^2.0",
"phpunit/php-invoker": "^2.0 || ^3.1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"suggest": {
"hoa/bench": "If you would like to run the benchmark scripts"
},
"time": "2026-01-12T10:45:19+00:00",
"bin": [
"bin/vobject",
"bin/generate_vcards"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Sabre\\VObject\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Dominik Tobschall",
"email": "dominik@fruux.com",
"homepage": "http://tobschall.de/",
"role": "Developer"
},
{
"name": "Ivan Enderlin",
"email": "ivan.enderlin@hoa-project.net",
"homepage": "http://mnt.io/",
"role": "Developer"
}
],
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
"homepage": "http://sabre.io/vobject/",
"keywords": [
"availability",
"freebusy",
"iCalendar",
"ical",
"ics",
"jCal",
"jCard",
"recurrence",
"rfc2425",
"rfc2426",
"rfc2739",
"rfc4770",
"rfc5545",
"rfc5546",
"rfc6321",
"rfc6350",
"rfc6351",
"rfc6474",
"rfc6638",
"rfc6715",
"rfc6868",
"vCalendar",
"vCard",
"vcf",
"xCal",
"xCard"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/vobject/issues",
"source": "https://github.com/fruux/sabre-vobject"
},
"install-path": "../sabre/vobject"
},
{
"name": "sabre/xml",
"version": "2.2.11",
"version_normalized": "2.2.11.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/xml.git",
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"lib-libxml": ">=2.6.20",
"php": "^7.1 || ^8.0",
"sabre/uri": ">=1.0,<3.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.17.1||3.63.2",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
},
"time": "2024-09-06T07:37:46+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"lib/Deserializer/functions.php",
"lib/Serializer/functions.php"
],
"psr-4": {
"Sabre\\Xml\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Markus Staab",
"email": "markus.staab@redaxo.de",
"role": "Developer"
}
],
"description": "sabre/xml is an XML library that you may not hate.",
"homepage": "https://sabre.io/xml/",
"keywords": [
"XMLReader",
"XMLWriter",
"dom",
"xml"
],
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/xml/issues",
"source": "https://github.com/fruux/sabre-xml"
},
"install-path": "../sabre/xml"
},
{
"name": "sebastian/cli-parser",
"version": "4.2.0",
+128 -2
View File
@@ -3,13 +3,31 @@
'name' => 'laravel/laravel',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '6ea8e8ac86f4025309af9618852e888df488ea81',
'reference' => '3471befb1af62975088d115a46aa180e13652a0d',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'as247/cloud-storages' => array(
'pretty_version' => 'v1.2.9',
'version' => '1.2.9.0',
'reference' => '6a8ef430f7f07a2e88ea6b7cfd32230da493c1f8',
'type' => 'library',
'install_path' => __DIR__ . '/../as247/cloud-storages',
'aliases' => array(),
'dev_requirement' => false,
),
'as247/flysystem-google-drive' => array(
'pretty_version' => 'v3.0.2',
'version' => '3.0.2.0',
'reference' => '7d2600c5e085cafbb49c9874d1efb6795d36cb2f',
'type' => 'library',
'install_path' => __DIR__ . '/../as247/flysystem-google-drive',
'aliases' => array(),
'dev_requirement' => false,
),
'brick/math' => array(
'pretty_version' => '0.14.8',
'version' => '0.14.8.0',
@@ -121,6 +139,15 @@
'aliases' => array(),
'dev_requirement' => true,
),
'firebase/php-jwt' => array(
'pretty_version' => 'v7.0.5',
'version' => '7.0.5.0',
'reference' => '47ad26bab5e7c70ae8a6f08ed25ff83631121380',
'type' => 'library',
'install_path' => __DIR__ . '/../firebase/php-jwt',
'aliases' => array(),
'dev_requirement' => false,
),
'fruitcake/php-cors' => array(
'pretty_version' => 'v1.4.0',
'version' => '1.4.0.0',
@@ -130,6 +157,33 @@
'aliases' => array(),
'dev_requirement' => false,
),
'google/apiclient' => array(
'pretty_version' => 'v2.19.3',
'version' => '2.19.3.0',
'reference' => 'a1f02761994fd9defb20f6f1449205fd66f450de',
'type' => 'library',
'install_path' => __DIR__ . '/../google/apiclient',
'aliases' => array(),
'dev_requirement' => false,
),
'google/apiclient-services' => array(
'pretty_version' => 'v0.442.0',
'version' => '0.442.0.0',
'reference' => '3afe7b5c9d1cebf2a5be018820ce62eeb906b81f',
'type' => 'library',
'install_path' => __DIR__ . '/../google/apiclient-services',
'aliases' => array(),
'dev_requirement' => false,
),
'google/auth' => array(
'pretty_version' => 'v1.50.1',
'version' => '1.50.1.0',
'reference' => '870c17ee3a1d73338d39a9ffa77a700ba77f5a83',
'type' => 'library',
'install_path' => __DIR__ . '/../google/auth',
'aliases' => array(),
'dev_requirement' => false,
),
'graham-campbell/result-type' => array(
'pretty_version' => 'v1.1.4',
'version' => '1.1.4.0',
@@ -427,7 +481,7 @@
'laravel/laravel' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '6ea8e8ac86f4025309af9618852e888df488ea81',
'reference' => '3471befb1af62975088d115a46aa180e13652a0d',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -523,6 +577,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'league/flysystem-webdav' => array(
'pretty_version' => '3.31.0',
'version' => '3.31.0.0',
'reference' => '1705b5d4aab6d755a78ea850e523dda9c58ad20f',
'type' => 'library',
'install_path' => __DIR__ . '/../league/flysystem-webdav',
'aliases' => array(),
'dev_requirement' => false,
),
'league/mime-type-detection' => array(
'pretty_version' => '1.16.0',
'version' => '1.16.0.0',
@@ -736,6 +799,15 @@
'aliases' => array(),
'dev_requirement' => true,
),
'psr/cache' => array(
'pretty_version' => '3.0.0',
'version' => '3.0.0.0',
'reference' => 'aa5030cfa5405eccfdcb1083ce040c2cb8d253bf',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/cache',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/clock' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
@@ -901,6 +973,60 @@
0 => '4.9.2',
),
),
'sabre/dav' => array(
'pretty_version' => '4.7.0',
'version' => '4.7.0.0',
'reference' => '074373bcd689a30bcf5aaa6bbb20a3395964ce7a',
'type' => 'library',
'install_path' => __DIR__ . '/../sabre/dav',
'aliases' => array(),
'dev_requirement' => false,
),
'sabre/event' => array(
'pretty_version' => '5.1.8',
'version' => '5.1.8.0',
'reference' => '1dd5f55421b0092006510264131a4d632d02d2c1',
'type' => 'library',
'install_path' => __DIR__ . '/../sabre/event',
'aliases' => array(),
'dev_requirement' => false,
),
'sabre/http' => array(
'pretty_version' => '5.1.13',
'version' => '5.1.13.0',
'reference' => '7c2a14097d1a0de2347dcbdc91a02f38e338f4db',
'type' => 'library',
'install_path' => __DIR__ . '/../sabre/http',
'aliases' => array(),
'dev_requirement' => false,
),
'sabre/uri' => array(
'pretty_version' => '2.3.4',
'version' => '2.3.4.0',
'reference' => 'b76524c22de90d80ca73143680a8e77b1266c291',
'type' => 'library',
'install_path' => __DIR__ . '/../sabre/uri',
'aliases' => array(),
'dev_requirement' => false,
),
'sabre/vobject' => array(
'pretty_version' => '4.5.8',
'version' => '4.5.8.0',
'reference' => 'd554eb24d64232922e1eab5896cc2f84b3b9ffb1',
'type' => 'library',
'install_path' => __DIR__ . '/../sabre/vobject',
'aliases' => array(),
'dev_requirement' => false,
),
'sabre/xml' => array(
'pretty_version' => '2.2.11',
'version' => '2.2.11.0',
'reference' => '01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc',
'type' => 'library',
'install_path' => __DIR__ . '/../sabre/xml',
'aliases' => array(),
'dev_requirement' => false,
),
'sebastian/cli-parser' => array(
'pretty_version' => '4.2.0',
'version' => '4.2.0.0',
+251
View File
@@ -0,0 +1,251 @@
# Changelog
## [7.0.5](https://github.com/firebase/php-jwt/compare/v7.0.4...v7.0.5) (2026-03-31)
### Bug Fixes
* RSA from JWK sometimes returns empty Instance ([#628](https://github.com/firebase/php-jwt/issues/628)) ([b4c78aa](https://github.com/firebase/php-jwt/commit/b4c78aa731664122198ad36c0033aa29e807397a))
## [7.0.4](https://github.com/firebase/php-jwt/compare/v7.0.3...v7.0.4) (2026-03-27)
### Bug Fixes
* readme examples, add tests for all examples ([#626](https://github.com/firebase/php-jwt/issues/626)) ([510a00c](https://github.com/firebase/php-jwt/commit/510a00c0e6353bc7d68412fab67e57a13954cb46))
* use urlsafeB64Decode everywhere ([#627](https://github.com/firebase/php-jwt/issues/627)) ([b889495](https://github.com/firebase/php-jwt/commit/b889495c83ddc3f3885ca3f0b65b41b1cb37a3b1))
## [7.0.3](https://github.com/firebase/php-jwt/compare/v7.0.2...v7.0.3) (2026-02-18)
### Miscellaneous Chores
* add environment for Release Please job ([#619](https://github.com/firebase/php-jwt/issues/619)) ([300fd02](https://github.com/firebase/php-jwt/commit/300fd02c883f096c9067df652dbd23f62cb5e2a7))
## [7.0.2](https://github.com/firebase/php-jwt/compare/v7.0.1...v7.0.2) (2025-12-16)
### Bug Fixes
* add key length validation for ec keys ([#615](https://github.com/firebase/php-jwt/issues/615)) ([7044f9a](https://github.com/firebase/php-jwt/commit/7044f9ae7e7d175d28cca71714feb236f1c0e252))
## [7.0.0](https://github.com/firebase/php-jwt/compare/v6.11.1...v7.0.0) (2025-12-15)
### ⚠️ ⚠️ ⚠️ Security Fixes ⚠️ ⚠️ ⚠️
* add key size validation ([#613](https://github.com/firebase/php-jwt/issues/613)) ([6b80341](https://github.com/firebase/php-jwt/commit/6b80341bf57838ea2d011487917337901cd71576))
**NOTE**: This fix will cause keys with a size below the minimally allowed size to break.
### Features
* add SensitiveParameter attribute to security-critical parameters ([#603](https://github.com/firebase/php-jwt/issues/603)) ([4dbfac0](https://github.com/firebase/php-jwt/commit/4dbfac0260eeb0e9e643063c99998e3219cc539b))
* store timestamp in `ExpiredException` ([#604](https://github.com/firebase/php-jwt/issues/604)) ([f174826](https://github.com/firebase/php-jwt/commit/f1748260d218a856b6a0c23715ac7fae1d7ca95b))
### Bug Fixes
* validate iat and nbf on payload ([#568](https://github.com/firebase/php-jwt/issues/568)) ([953b2c8](https://github.com/firebase/php-jwt/commit/953b2c88bb445b7e3bb82a5141928f13d7343afd))
## [6.11.1](https://github.com/firebase/php-jwt/compare/v6.11.0...v6.11.1) (2025-04-09)
### Bug Fixes
* update error text for consistency ([#528](https://github.com/firebase/php-jwt/issues/528)) ([c11113a](https://github.com/firebase/php-jwt/commit/c11113afa13265e016a669e75494b9203b8a7775))
## [6.11.0](https://github.com/firebase/php-jwt/compare/v6.10.2...v6.11.0) (2025-01-23)
### Features
* support octet typed JWK ([#587](https://github.com/firebase/php-jwt/issues/587)) ([7cb8a26](https://github.com/firebase/php-jwt/commit/7cb8a265fa81edf2fa6ef8098f5bc5ae573c33ad))
### Bug Fixes
* refactor constructor Key to use PHP 8.0 syntax ([#577](https://github.com/firebase/php-jwt/issues/577)) ([29fa2ce](https://github.com/firebase/php-jwt/commit/29fa2ce9e0582cd397711eec1e80c05ce20fabca))
## [6.10.2](https://github.com/firebase/php-jwt/compare/v6.10.1...v6.10.2) (2024-11-24)
### Bug Fixes
* Mitigate PHP8.4 deprecation warnings ([#570](https://github.com/firebase/php-jwt/issues/570)) ([76808fa](https://github.com/firebase/php-jwt/commit/76808fa227f3811aa5cdb3bf81233714b799a5b5))
* support php 8.4 ([#583](https://github.com/firebase/php-jwt/issues/583)) ([e3d68b0](https://github.com/firebase/php-jwt/commit/e3d68b044421339443c74199edd020e03fb1887e))
## [6.10.1](https://github.com/firebase/php-jwt/compare/v6.10.0...v6.10.1) (2024-05-18)
### Bug Fixes
* ensure ratelimit expiry is set every time ([#556](https://github.com/firebase/php-jwt/issues/556)) ([09cb208](https://github.com/firebase/php-jwt/commit/09cb2081c2c3bc0f61e2f2a5fbea5741f7498648))
* ratelimit cache expiration ([#550](https://github.com/firebase/php-jwt/issues/550)) ([dda7250](https://github.com/firebase/php-jwt/commit/dda725033585ece30ff8cae8937320d7e9f18bae))
## [6.10.0](https://github.com/firebase/php-jwt/compare/v6.9.0...v6.10.0) (2023-11-28)
### Features
* allow typ header override ([#546](https://github.com/firebase/php-jwt/issues/546)) ([79cb30b](https://github.com/firebase/php-jwt/commit/79cb30b729a22931b2fbd6b53f20629a83031ba9))
## [6.9.0](https://github.com/firebase/php-jwt/compare/v6.8.1...v6.9.0) (2023-10-04)
### Features
* add payload to jwt exception ([#521](https://github.com/firebase/php-jwt/issues/521)) ([175edf9](https://github.com/firebase/php-jwt/commit/175edf958bb61922ec135b2333acf5622f2238a2))
## [6.8.1](https://github.com/firebase/php-jwt/compare/v6.8.0...v6.8.1) (2023-07-14)
### Bug Fixes
* accept float claims but round down to ignore them ([#492](https://github.com/firebase/php-jwt/issues/492)) ([3936842](https://github.com/firebase/php-jwt/commit/39368423beeaacb3002afa7dcb75baebf204fe7e))
* different BeforeValidException messages for nbf and iat ([#526](https://github.com/firebase/php-jwt/issues/526)) ([0a53cf2](https://github.com/firebase/php-jwt/commit/0a53cf2986e45c2bcbf1a269f313ebf56a154ee4))
## [6.8.0](https://github.com/firebase/php-jwt/compare/v6.7.0...v6.8.0) (2023-06-14)
### Features
* add support for P-384 curve ([#515](https://github.com/firebase/php-jwt/issues/515)) ([5de4323](https://github.com/firebase/php-jwt/commit/5de4323f4baf4d70bca8663bd87682a69c656c3d))
### Bug Fixes
* handle invalid http responses ([#508](https://github.com/firebase/php-jwt/issues/508)) ([91c39c7](https://github.com/firebase/php-jwt/commit/91c39c72b22fc3e1191e574089552c1f2041c718))
## [6.7.0](https://github.com/firebase/php-jwt/compare/v6.6.0...v6.7.0) (2023-06-14)
### Features
* add ed25519 support to JWK (public keys) ([#452](https://github.com/firebase/php-jwt/issues/452)) ([e53979a](https://github.com/firebase/php-jwt/commit/e53979abae927de916a75b9d239cfda8ce32be2a))
## [6.6.0](https://github.com/firebase/php-jwt/compare/v6.5.0...v6.6.0) (2023-06-13)
### Features
* allow get headers when decoding token ([#442](https://github.com/firebase/php-jwt/issues/442)) ([fb85f47](https://github.com/firebase/php-jwt/commit/fb85f47cfaeffdd94faf8defdf07164abcdad6c3))
### Bug Fixes
* only check iat if nbf is not used ([#493](https://github.com/firebase/php-jwt/issues/493)) ([398ccd2](https://github.com/firebase/php-jwt/commit/398ccd25ea12fa84b9e4f1085d5ff448c21ec797))
## [6.5.0](https://github.com/firebase/php-jwt/compare/v6.4.0...v6.5.0) (2023-05-12)
### Bug Fixes
* allow KID of '0' ([#505](https://github.com/firebase/php-jwt/issues/505)) ([9dc46a9](https://github.com/firebase/php-jwt/commit/9dc46a9c3e5801294249cfd2554c5363c9f9326a))
### Miscellaneous Chores
* drop support for PHP 7.3 ([#495](https://github.com/firebase/php-jwt/issues/495))
## [6.4.0](https://github.com/firebase/php-jwt/compare/v6.3.2...v6.4.0) (2023-02-08)
### Features
* add support for W3C ES256K ([#462](https://github.com/firebase/php-jwt/issues/462)) ([213924f](https://github.com/firebase/php-jwt/commit/213924f51936291fbbca99158b11bd4ae56c2c95))
* improve caching by only decoding jwks when necessary ([#486](https://github.com/firebase/php-jwt/issues/486)) ([78d3ed1](https://github.com/firebase/php-jwt/commit/78d3ed1073553f7d0bbffa6c2010009a0d483d5c))
## [6.3.2](https://github.com/firebase/php-jwt/compare/v6.3.1...v6.3.2) (2022-11-01)
### Bug Fixes
* check kid before using as array index ([bad1b04](https://github.com/firebase/php-jwt/commit/bad1b040d0c736bbf86814c6b5ae614f517cf7bd))
## [6.3.1](https://github.com/firebase/php-jwt/compare/v6.3.0...v6.3.1) (2022-11-01)
### Bug Fixes
* casing of GET for PSR compat ([#451](https://github.com/firebase/php-jwt/issues/451)) ([60b52b7](https://github.com/firebase/php-jwt/commit/60b52b71978790eafcf3b95cfbd83db0439e8d22))
* string interpolation format for php 8.2 ([#446](https://github.com/firebase/php-jwt/issues/446)) ([2e07d8a](https://github.com/firebase/php-jwt/commit/2e07d8a1524d12b69b110ad649f17461d068b8f2))
## 6.3.0 / 2022-07-15
- Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399))
- Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435))
## 6.2.0 / 2022-05-14
- Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397))
- Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)).
## 6.1.0 / 2022-03-23
- Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0
- Add parameter typing and return types where possible
## 6.0.0 / 2022-01-24
- **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information.
- New Key object to prevent key/algorithm type confusion (#365)
- Add JWK support (#273)
- Add ES256 support (#256)
- Add ES384 support (#324)
- Add Ed25519 support (#343)
## 5.0.0 / 2017-06-26
- Support RS384 and RS512.
See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)!
- Add an example for RS256 openssl.
See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)!
- Detect invalid Base64 encoding in signature.
See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)!
- Update `JWT::verify` to handle OpenSSL errors.
See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)!
- Add `array` type hinting to `decode` method
See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)!
- Add all JSON error types.
See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)!
- Bugfix 'kid' not in given key list.
See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)!
- Miscellaneous cleanup, documentation and test fixes.
See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115),
[#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and
[#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman),
[@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)!
## 4.0.0 / 2016-07-17
- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)!
- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)!
- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)!
- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)!
## 3.0.0 / 2015-07-22
- Minimum PHP version updated from `5.2.0` to `5.3.0`.
- Add `\Firebase\JWT` namespace. See
[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to
[@Dashron](https://github.com/Dashron)!
- Require a non-empty key to decode and verify a JWT. See
[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to
[@sjones608](https://github.com/sjones608)!
- Cleaner documentation blocks in the code. See
[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to
[@johanderuijter](https://github.com/johanderuijter)!
## 2.2.0 / 2015-06-22
- Add support for adding custom, optional JWT headers to `JWT::encode()`. See
[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to
[@mcocaro](https://github.com/mcocaro)!
## 2.1.0 / 2015-05-20
- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew
between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)!
- Add support for passing an object implementing the `ArrayAccess` interface for
`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)!
## 2.0.0 / 2015-04-01
- **Note**: It is strongly recommended that you update to > v2.0.0 to address
known security vulnerabilities in prior versions when both symmetric and
asymmetric keys are used together.
- Update signature for `JWT::decode(...)` to require an array of supported
algorithms to use when verifying token signatures.
+30
View File
@@ -0,0 +1,30 @@
Copyright (c) 2011, Neuman Vong
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of the copyright holder nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+425
View File
@@ -0,0 +1,425 @@
![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg)
[![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt)
[![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt)
[![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt)
PHP-JWT
=======
A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519).
Installation
------------
Use composer to manage your dependencies and download PHP-JWT:
```bash
composer require firebase/php-jwt
```
Optionally, install the `paragonie/sodium_compat` package from composer if your
php env does not have libsodium installed:
```bash
composer require paragonie/sodium_compat
```
## Example
```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$key = 'example_key_of_sufficient_length';
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
/**
* IMPORTANT:
* You must specify supported algorithms for your application. See
* https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
* for a list of spec-compliant algorithms.
*/
$jwt = JWT::encode($payload, $key, 'HS256');
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
print_r($decoded);
// Pass a stdClass in as the third parameter to get the decoded header values
$headers = new stdClass();
$decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers);
print_r($headers);
/*
NOTE: This will now be an object instead of an associative array. To get
an associative array, you will need to cast it as such:
*/
$decoded_array = (array) $decoded;
/**
* You can add a leeway to account for when there is a clock skew times between
* the signing and verifying servers. It is recommended that this leeway should
* not be bigger than a few minutes.
*
* Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef
*/
JWT::$leeway = 60; // $leeway in seconds
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
```
## Example encode/decode headers
Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by
this library. This is because without verifying the JWT, the header values could have been tampered with.
Any value pulled from an unverified header should be treated as if it could be any string sent in from an
attacker. If this is something you still want to do in your application for whatever reason, it's possible to
decode the header values manually simply by calling `json_decode` and `base64_decode` on the JWT
header part:
```php
use Firebase\JWT\JWT;
$key = 'example_key_of_sufficient_length';
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$headers = [
'x-forwarded-for' => 'www.google.com'
];
// Encode headers in the JWT string
$jwt = JWT::encode($payload, $key, 'HS256', null, $headers);
// Decode headers from the JWT string WITHOUT validation
// **IMPORTANT**: This operation is vulnerable to attacks, as the JWT has not yet been verified.
// These headers could be any value sent by an attacker.
list($headersB64, $payloadB64, $sig) = explode('.', $jwt);
$decoded = json_decode(base64_decode($headersB64), true);
print_r($decoded);
```
## Example with RS256 (openssl)
```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$privateKey = <<<EOD
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuzWHNM5f+amCjQztc5QTfJfzCC5J4nuW+L/aOxZ4f8J3Frew
M2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJhzkPYLae7bTVro3hok0zDITR8F6S
JGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548tu4czCuqU8BGVOlnp6IqBHhAswNMM
78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vSopcT51koWOgiTf3C7nJUoMWZHZI5
HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTzTTqo1SCSH2pooJl9O8at6kkRYsrZ
WwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/BwQIDAQABAoIBAFtGaOqNKGwggn9k
6yzr6GhZ6Wt2rh1Xpq8XUz514UBhPxD7dFRLpbzCrLVpzY80LbmVGJ9+1pJozyWc
VKeCeUdNwbqkr240Oe7GTFmGjDoxU+5/HX/SJYPpC8JZ9oqgEA87iz+WQX9hVoP2
oF6EB4ckDvXmk8FMwVZW2l2/kd5mrEVbDaXKxhvUDf52iVD+sGIlTif7mBgR99/b
c3qiCnxCMmfYUnT2eh7Vv2LhCR/G9S6C3R4lA71rEyiU3KgsGfg0d82/XWXbegJW
h3QbWNtQLxTuIvLq5aAryV3PfaHlPgdgK0ft6ocU2de2FagFka3nfVEyC7IUsNTK
bq6nhAECgYEA7d/0DPOIaItl/8BWKyCuAHMss47j0wlGbBSHdJIiS55akMvnAG0M
39y22Qqfzh1at9kBFeYeFIIU82ZLF3xOcE3z6pJZ4Dyvx4BYdXH77odo9uVK9s1l
3T3BlMcqd1hvZLMS7dviyH79jZo4CXSHiKzc7pQ2YfK5eKxKqONeXuECgYEAyXlG
vonaus/YTb1IBei9HwaccnQ/1HRn6MvfDjb7JJDIBhNClGPt6xRlzBbSZ73c2QEC
6Fu9h36K/HZ2qcLd2bXiNyhIV7b6tVKk+0Psoj0dL9EbhsD1OsmE1nTPyAc9XZbb
OPYxy+dpBCUA8/1U9+uiFoCa7mIbWcSQ+39gHuECgYAz82pQfct30aH4JiBrkNqP
nJfRq05UY70uk5k1u0ikLTRoVS/hJu/d4E1Kv4hBMqYCavFSwAwnvHUo51lVCr/y
xQOVYlsgnwBg2MX4+GjmIkqpSVCC8D7j/73MaWb746OIYZervQ8dbKahi2HbpsiG
8AHcVSA/agxZr38qvWV54QKBgCD5TlDE8x18AuTGQ9FjxAAd7uD0kbXNz2vUYg9L
hFL5tyL3aAAtUrUUw4xhd9IuysRhW/53dU+FsG2dXdJu6CxHjlyEpUJl2iZu/j15
YnMzGWHIEX8+eWRDsw/+Ujtko/B7TinGcWPz3cYl4EAOiCeDUyXnqnO1btCEUU44
DJ1BAoGBAJuPD27ErTSVtId90+M4zFPNibFP50KprVdc8CR37BE7r8vuGgNYXmnI
RLnGP9p3pVgFCktORuYS2J/6t84I3+A17nEoB4xvhTLeAinAW/uTQOUmNicOP4Ek
2MsLL2kHgL8bLTmvXV4FX+PXphrDKg1XxzOYn0otuoqdAQrkK4og
-----END RSA PRIVATE KEY-----
EOD;
$publicKey = <<<EOD
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzWHNM5f+amCjQztc5QT
fJfzCC5J4nuW+L/aOxZ4f8J3FrewM2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJ
hzkPYLae7bTVro3hok0zDITR8F6SJGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548t
u4czCuqU8BGVOlnp6IqBHhAswNMM78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vS
opcT51koWOgiTf3C7nJUoMWZHZI5HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTz
TTqo1SCSH2pooJl9O8at6kkRYsrZWwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/B
wQIDAQAB
-----END PUBLIC KEY-----
EOD;
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$jwt = JWT::encode($payload, $privateKey, 'RS256');
echo "Encode:\n" . print_r($jwt, true) . "\n";
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
/*
NOTE: This will now be an object instead of an associative array. To get
an associative array, you will need to cast it as such:
*/
$decoded_array = (array) $decoded;
echo "Decode:\n" . print_r($decoded_array, true) . "\n";
```
## Example with a passphrase
```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Your passphrase
$passphrase = '[YOUR_PASSPHRASE]';
// Your private key file with passphrase
// Can be generated with "ssh-keygen -t rsa -m pem"
$privateKeyFile = '/path/to/key-with-passphrase.pem';
/** @var OpenSSLAsymmetricKey $privateKey */
$privateKey = openssl_pkey_get_private(
file_get_contents($privateKeyFile),
$passphrase
);
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$jwt = JWT::encode($payload, $privateKey, 'RS256');
echo "Encode:\n" . print_r($jwt, true) . "\n";
// Get public key from the private key, or pull from from a file.
$publicKey = openssl_pkey_get_details($privateKey)['key'];
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
```
## Example with EdDSA (libsodium and Ed25519 signature)
```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Public and private keys are expected to be Base64 encoded. The last
// non-empty line is used so that keys can be generated with
// sodium_crypto_sign_keypair(). The secret keys generated by other tools may
// need to be adjusted to match the input expected by libsodium.
$keyPair = sodium_crypto_sign_keypair();
$privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
$publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$jwt = JWT::encode($payload, $privateKey, 'EdDSA');
echo "Encode:\n" . print_r($jwt, true) . "\n";
$decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA'));
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
```
## Example with multiple keys
```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Example RSA keys from previous example
// $privateRsKey = '...';
// $publicRsKey = '...';
// Example EdDSA keys from previous example
// $privateEcKey = '...';
// $publicEcKey = '...';
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$jwt1 = JWT::encode($payload, $privateRsKey, 'RS256', 'kid1');
$jwt2 = JWT::encode($payload, $privateEcKey, 'EdDSA', 'kid2');
echo "Encode 1:\n" . print_r($jwt1, true) . "\n";
echo "Encode 2:\n" . print_r($jwt2, true) . "\n";
$keys = [
'kid1' => new Key($publicRsKey, 'RS256'),
'kid2' => new Key($publicEcKey, 'EdDSA'),
];
$decoded1 = JWT::decode($jwt1, $keys);
$decoded2 = JWT::decode($jwt2, $keys);
echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n";
echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n";
```
## Using JWKs
```php
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
// Set of keys. The "keys" key is required. For example, the JSON response to
// this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk
$jwks = ['keys' => []];
// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key
// objects. Pass this as the second parameter to JWT::decode.
$decoded = JWT::decode($jwt, JWK::parseKeySet($jwks));
print_r($decoded);
```
## Using Cached Key Sets
The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI.
This has the following advantages:
1. The results are cached for performance.
2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation.
3. If rate limiting is enabled, the JWKS URI will not make more than 10 requests a second.
```php
use Firebase\JWT\CachedKeySet;
use Firebase\JWT\JWT;
// The URI for the JWKS you wish to cache the results from
$jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk';
// Create an HTTP client (can be any PSR-7 compatible HTTP client)
$httpClient = new GuzzleHttp\Client();
// Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory)
$httpFactory = new GuzzleHttp\Psr7\HttpFactory();
// Create a cache item pool (can be any PSR-6 compatible cache item pool)
$cacheItemPool = Phpfastcache\CacheManager::getInstance('files');
$keySet = new CachedKeySet(
$jwksUri,
$httpClient,
$httpFactory,
$cacheItemPool,
null, // $expiresAfter int seconds to set the JWKS to expire
true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys
);
$jwt = 'eyJhbGci...'; // Some JWT signed by a key from the $jwkUri above
$decoded = JWT::decode($jwt, $keySet);
```
Miscellaneous
-------------
#### Exception Handling
When a call to `JWT::decode` is invalid, it will throw one of the following exceptions:
```php
use Firebase\JWT\JWT;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
use Firebase\JWT\ExpiredException;
use DomainException;
use InvalidArgumentException;
use UnexpectedValueException;
try {
$decoded = JWT::decode($jwt, $keys);
} catch (InvalidArgumentException $e) {
// provided key/key-array is empty or malformed.
} catch (DomainException $e) {
// provided algorithm is unsupported OR
// provided key is invalid OR
// unknown error thrown in openSSL or libsodium OR
// libsodium is required but not available.
} catch (SignatureInvalidException $e) {
// provided JWT signature verification failed.
} catch (BeforeValidException $e) {
// provided JWT is trying to be used before "nbf" claim OR
// provided JWT is trying to be used before "iat" claim.
} catch (ExpiredException $e) {
// provided JWT is trying to be used after "exp" claim.
} catch (UnexpectedValueException $e) {
// provided JWT is malformed OR
// provided JWT is missing an algorithm / using an unsupported algorithm OR
// provided JWT algorithm does not match provided key OR
// provided key ID in key/key-array is empty or invalid.
}
```
All exceptions in the `Firebase\JWT` namespace extend `UnexpectedValueException`, and can be simplified
like this:
```php
use Firebase\JWT\JWT;
use UnexpectedValueException;
try {
$decoded = JWT::decode($jwt, $keys);
} catch (LogicException $e) {
// errors having to do with environmental setup or malformed JWT Keys
} catch (UnexpectedValueException $e) {
// errors having to do with JWT signature and claims
}
```
#### Casting to array
The return value of `JWT::decode` is the generic PHP object `stdClass`. If you'd like to handle with arrays
instead, you can do the following:
```php
// return type is stdClass
$decoded = JWT::decode($jwt, $keys);
// cast to array
$decoded = json_decode(json_encode($decoded), true);
```
Tests
-----
Run the tests using phpunit:
```bash
$ composer update
$ vendor/bin/phpunit -c phpunit.xml.dist
PHPUnit 3.7.10 by Sebastian Bergmann.
.....
Time: 0 seconds, Memory: 2.50Mb
OK (5 tests, 5 assertions)
```
New Lines in private keys
-----
If your private key contains `\n` characters, be sure to wrap it in double quotes `""`
and not single quotes `''` in order to properly interpret the escaped characters.
License
-------
[3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause).
+43
View File
@@ -0,0 +1,43 @@
{
"name": "firebase/php-jwt",
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"php",
"jwt"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"license": "BSD-3-Clause",
"require": {
"php": "^8.0"
},
"suggest": {
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
"ext-sodium": "Support EdDSA (Ed25519) signatures"
},
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"phpfastcache/phpfastcache": "^9.2"
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Firebase\JWT;
class BeforeValidException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface
{
private object $payload;
public function setPayload(object $payload): void
{
$this->payload = $payload;
}
public function getPayload(): object
{
return $this->payload;
}
}
+275
View File
@@ -0,0 +1,275 @@
<?php
namespace Firebase\JWT;
use ArrayAccess;
use InvalidArgumentException;
use LogicException;
use OutOfBoundsException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use RuntimeException;
use UnexpectedValueException;
/**
* @implements ArrayAccess<string, Key>
*/
class CachedKeySet implements ArrayAccess
{
/**
* @var string
*/
private $jwksUri;
/**
* @var ClientInterface
*/
private $httpClient;
/**
* @var RequestFactoryInterface
*/
private $httpFactory;
/**
* @var CacheItemPoolInterface
*/
private $cache;
/**
* @var ?int
*/
private $expiresAfter;
/**
* @var ?CacheItemInterface
*/
private $cacheItem;
/**
* @var array<string, array<mixed>>
*/
private $keySet;
/**
* @var string
*/
private $cacheKey;
/**
* @var string
*/
private $cacheKeyPrefix = 'jwks';
/**
* @var int
*/
private $maxKeyLength = 64;
/**
* @var bool
*/
private $rateLimit;
/**
* @var string
*/
private $rateLimitCacheKey;
/**
* @var int
*/
private $maxCallsPerMinute = 10;
/**
* @var string|null
*/
private $defaultAlg;
public function __construct(
string $jwksUri,
ClientInterface $httpClient,
RequestFactoryInterface $httpFactory,
CacheItemPoolInterface $cache,
?int $expiresAfter = null,
bool $rateLimit = false,
?string $defaultAlg = null
) {
$this->jwksUri = $jwksUri;
$this->httpClient = $httpClient;
$this->httpFactory = $httpFactory;
$this->cache = $cache;
$this->expiresAfter = $expiresAfter;
$this->rateLimit = $rateLimit;
$this->defaultAlg = $defaultAlg;
$this->setCacheKeys();
}
/**
* @param string $keyId
* @return Key
*/
public function offsetGet($keyId): Key
{
if (!$this->keyIdExists($keyId)) {
throw new OutOfBoundsException('Key ID not found');
}
return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
}
/**
* @param string $keyId
* @return bool
*/
public function offsetExists($keyId): bool
{
return $this->keyIdExists($keyId);
}
/**
* @param string $offset
* @param Key $value
*/
public function offsetSet($offset, $value): void
{
throw new LogicException('Method not implemented');
}
/**
* @param string $offset
*/
public function offsetUnset($offset): void
{
throw new LogicException('Method not implemented');
}
/**
* @return array<mixed>
*/
private function formatJwksForCache(string $jwks): array
{
$jwks = json_decode($jwks, true);
if (!isset($jwks['keys'])) {
throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
}
if (empty($jwks['keys'])) {
throw new InvalidArgumentException('JWK Set did not contain any keys');
}
$keys = [];
foreach ($jwks['keys'] as $k => $v) {
$kid = isset($v['kid']) ? $v['kid'] : $k;
$keys[(string) $kid] = $v;
}
return $keys;
}
private function keyIdExists(string $keyId): bool
{
if (null === $this->keySet) {
$item = $this->getCacheItem();
// Try to load keys from cache
if ($item->isHit()) {
// item found! retrieve it
$this->keySet = $item->get();
// If the cached item is a string, the JWKS response was cached (previous behavior).
// Parse this into expected format array<kid, jwk> instead.
if (\is_string($this->keySet)) {
$this->keySet = $this->formatJwksForCache($this->keySet);
}
}
}
if (!isset($this->keySet[$keyId])) {
if ($this->rateLimitExceeded()) {
return false;
}
$request = $this->httpFactory->createRequest('GET', $this->jwksUri);
$jwksResponse = $this->httpClient->sendRequest($request);
if ($jwksResponse->getStatusCode() !== 200) {
throw new UnexpectedValueException(
\sprintf(
'HTTP Error: %d %s for URI "%s"',
$jwksResponse->getStatusCode(),
$jwksResponse->getReasonPhrase(),
$this->jwksUri,
),
$jwksResponse->getStatusCode()
);
}
$this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
if (!isset($this->keySet[$keyId])) {
return false;
}
$item = $this->getCacheItem();
$item->set($this->keySet);
if ($this->expiresAfter) {
$item->expiresAfter($this->expiresAfter);
}
$this->cache->save($item);
}
return true;
}
private function rateLimitExceeded(): bool
{
if (!$this->rateLimit) {
return false;
}
$cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
$cacheItemData = [];
if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) {
$cacheItemData = $data;
}
$callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0;
$expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC'));
if (++$callsPerMinute > $this->maxCallsPerMinute) {
return true;
}
$cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]);
$cacheItem->expiresAt($expiry);
$this->cache->save($cacheItem);
return false;
}
private function getCacheItem(): CacheItemInterface
{
if (\is_null($this->cacheItem)) {
$this->cacheItem = $this->cache->getItem($this->cacheKey);
}
return $this->cacheItem;
}
private function setCacheKeys(): void
{
if (empty($this->jwksUri)) {
throw new RuntimeException('JWKS URI is empty');
}
// ensure we do not have illegal characters
$key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
// add prefix
$key = $this->cacheKeyPrefix . $key;
// Hash keys if they exceed $maxKeyLength of 64
if (\strlen($key) > $this->maxKeyLength) {
$key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
}
$this->cacheKey = $key;
if ($this->rateLimit) {
// add prefix
$rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
// Hash keys if they exceed $maxKeyLength of 64
if (\strlen($rateLimitKey) > $this->maxKeyLength) {
$rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
}
$this->rateLimitCacheKey = $rateLimitKey;
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Firebase\JWT;
class ExpiredException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface
{
private object $payload;
private ?int $timestamp = null;
public function setPayload(object $payload): void
{
$this->payload = $payload;
}
public function getPayload(): object
{
return $this->payload;
}
public function setTimestamp(int $timestamp): void
{
$this->timestamp = $timestamp;
}
public function getTimestamp(): ?int
{
return $this->timestamp;
}
}
+363
View File
@@ -0,0 +1,363 @@
<?php
namespace Firebase\JWT;
use DomainException;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* JSON Web Key implementation, based on this spec:
* https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
*
* PHP version 5
*
* @category Authentication
* @package Authentication_JWT
* @author Bui Sy Nguyen <nguyenbs@gmail.com>
* @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
* @link https://github.com/firebase/php-jwt
*/
class JWK
{
private const OID = '1.2.840.10045.2.1';
private const ASN1_OBJECT_IDENTIFIER = 0x06;
private const ASN1_SEQUENCE = 0x10; // also defined in JWT
private const ASN1_BIT_STRING = 0x03;
private const EC_CURVES = [
'P-256' => '1.2.840.10045.3.1.7', // Len: 64
'secp256k1' => '1.3.132.0.10', // Len: 64
'P-384' => '1.3.132.0.34', // Len: 96
// 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
];
// For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype.
// This library supports the following subtypes:
private const OKP_SUBTYPES = [
'Ed25519' => true, // RFC 8037
];
/**
* Parse a set of JWK keys
*
* @param array<mixed> $jwks The JSON Web Key Set as an associative array
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
* JSON Web Key Set
*
* @return array<string, Key> An associative array of key IDs (kid) to Key objects
*
* @throws InvalidArgumentException Provided JWK Set is empty
* @throws UnexpectedValueException Provided JWK Set was invalid
* @throws DomainException OpenSSL failure
*
* @uses parseKey
*/
public static function parseKeySet(#[\SensitiveParameter] array $jwks, ?string $defaultAlg = null): array
{
$keys = [];
if (!isset($jwks['keys'])) {
throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
}
if (empty($jwks['keys'])) {
throw new InvalidArgumentException('JWK Set did not contain any keys');
}
foreach ($jwks['keys'] as $k => $v) {
$kid = isset($v['kid']) ? $v['kid'] : $k;
if ($key = self::parseKey($v, $defaultAlg)) {
$keys[(string) $kid] = $key;
}
}
if (0 === \count($keys)) {
throw new UnexpectedValueException('No supported algorithms found in JWK Set');
}
return $keys;
}
/**
* Parse a JWK key
*
* @param array<mixed> $jwk An individual JWK
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
* JSON Web Key Set
*
* @return Key The key object for the JWK
*
* @throws InvalidArgumentException Provided JWK is empty
* @throws UnexpectedValueException Provided JWK was invalid
* @throws DomainException OpenSSL failure
*
* @uses createPemFromModulusAndExponent
*/
public static function parseKey(#[\SensitiveParameter] array $jwk, ?string $defaultAlg = null): ?Key
{
if (empty($jwk)) {
throw new InvalidArgumentException('JWK must not be empty');
}
if (!isset($jwk['kty'])) {
throw new UnexpectedValueException('JWK must contain a "kty" parameter');
}
if (!isset($jwk['alg'])) {
if (\is_null($defaultAlg)) {
// The "alg" parameter is optional in a KTY, but an algorithm is required
// for parsing in this library. Use the $defaultAlg parameter when parsing the
// key set in order to prevent this error.
// @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
throw new UnexpectedValueException('JWK must contain an "alg" parameter');
}
$jwk['alg'] = $defaultAlg;
}
switch ($jwk['kty']) {
case 'RSA':
if (!empty($jwk['d'])) {
throw new UnexpectedValueException('RSA private keys are not supported');
}
if (!isset($jwk['n']) || !isset($jwk['e'])) {
throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
}
$pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
$publicKey = \openssl_pkey_get_public($pem);
if (false === $publicKey) {
throw new DomainException(
'OpenSSL error: ' . \openssl_error_string()
);
}
return new Key($publicKey, $jwk['alg']);
case 'EC':
if (isset($jwk['d'])) {
// The key is actually a private key
throw new UnexpectedValueException('Key data must be for a public key');
}
if (empty($jwk['crv'])) {
throw new UnexpectedValueException('crv not set');
}
if (!isset(self::EC_CURVES[$jwk['crv']])) {
throw new DomainException('Unrecognised or unsupported EC curve');
}
if (empty($jwk['x']) || empty($jwk['y'])) {
throw new UnexpectedValueException('x and y not set');
}
$publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
return new Key($publicKey, $jwk['alg']);
case 'OKP':
if (isset($jwk['d'])) {
// The key is actually a private key
throw new UnexpectedValueException('Key data must be for a public key');
}
if (!isset($jwk['crv'])) {
throw new UnexpectedValueException('crv not set');
}
if (empty(self::OKP_SUBTYPES[$jwk['crv']])) {
throw new DomainException('Unrecognised or unsupported OKP key subtype');
}
if (empty($jwk['x'])) {
throw new UnexpectedValueException('x not set');
}
// This library works internally with EdDSA keys (Ed25519) encoded in standard base64.
$publicKey = JWT::convertBase64urlToBase64($jwk['x']);
return new Key($publicKey, $jwk['alg']);
case 'oct':
if (!isset($jwk['k'])) {
throw new UnexpectedValueException('k not set');
}
return new Key(JWT::urlsafeB64Decode($jwk['k']), $jwk['alg']);
default:
break;
}
return null;
}
/**
* Converts the EC JWK values to pem format.
*
* @param string $crv The EC curve (only P-256 & P-384 is supported)
* @param string $x The EC x-coordinate
* @param string $y The EC y-coordinate
*
* @return string
*/
private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
{
$pem =
self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(
self::ASN1_OBJECT_IDENTIFIER,
self::encodeOID(self::OID)
)
. self::encodeDER(
self::ASN1_OBJECT_IDENTIFIER,
self::encodeOID(self::EC_CURVES[$crv])
)
) .
self::encodeDER(
self::ASN1_BIT_STRING,
\chr(0x00) . \chr(0x04)
. JWT::urlsafeB64Decode($x)
. JWT::urlsafeB64Decode($y)
)
);
return \sprintf(
"-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
wordwrap(base64_encode($pem), 64, "\n", true)
);
}
/**
* Create a public key represented in PEM format from RSA modulus and exponent information
*
* @param string $n The RSA modulus encoded in Base64
* @param string $e The RSA exponent encoded in Base64
*
* @return string The RSA public key represented in PEM format
*
* @uses encodeLength
*/
private static function createPemFromModulusAndExponent(
string $n,
string $e
): string {
$mod = JWT::urlsafeB64Decode($n);
$exp = JWT::urlsafeB64Decode($e);
// Correct encoding for ASN1, as ints are represented as unsigned in jwk
// but signed in ASN1. Prepending null byte makes it unsigned.
if (\strlen($mod) > 0 && \ord($mod[0]) >= 128) {
$mod = \chr(0) . $mod;
}
if (\strlen($exp) > 0 && \ord($exp[0]) >= 128) {
$exp = \chr(0) . $exp;
}
$modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
$publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
$rsaPublicKey = \pack(
'Ca*a*a*',
48,
self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
$modulus,
$publicExponent
);
// sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
$rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
$rsaPublicKey = \chr(0) . $rsaPublicKey;
$rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
$rsaPublicKey = \pack(
'Ca*a*',
48,
self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
$rsaOID . $rsaPublicKey
);
return "-----BEGIN PUBLIC KEY-----\r\n" .
\chunk_split(\base64_encode($rsaPublicKey), 64) .
'-----END PUBLIC KEY-----';
}
/**
* DER-encode the length
*
* DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
* {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
*
* @param int $length
* @return string
*/
private static function encodeLength(int $length): string
{
if ($length <= 0x7F) {
return \chr($length);
}
$temp = \ltrim(\pack('N', $length), \chr(0));
return \pack('Ca*', 0x80 | \strlen($temp), $temp);
}
/**
* Encodes a value into a DER object.
* Also defined in Firebase\JWT\JWT
*
* @param int $type DER tag
* @param string $value the value to encode
* @return string the encoded object
*/
private static function encodeDER(int $type, string $value): string
{
$tag_header = 0;
if ($type === self::ASN1_SEQUENCE) {
$tag_header |= 0x20;
}
// Type
$der = \chr($tag_header | $type);
// Length
$der .= \chr(\strlen($value));
return $der . $value;
}
/**
* Encodes a string into a DER-encoded OID.
*
* @param string $oid the OID string
* @return string the binary DER-encoded OID
*/
private static function encodeOID(string $oid): string
{
$octets = explode('.', $oid);
// Get the first octet
$first = (int) array_shift($octets);
$second = (int) array_shift($octets);
$oid = \chr($first * 40 + $second);
// Iterate over subsequent octets
foreach ($octets as $octet) {
if ($octet == 0) {
$oid .= \chr(0x00);
continue;
}
$bin = '';
while ($octet) {
$bin .= \chr(0x80 | ($octet & 0x7f));
$octet >>= 7;
}
$bin[0] = $bin[0] & \chr(0x7f);
// Convert to big endian if necessary
if (pack('V', 65534) == pack('L', 65534)) {
$oid .= strrev($bin);
} else {
$oid .= $bin;
}
}
return $oid;
}
}
+745
View File
@@ -0,0 +1,745 @@
<?php
namespace Firebase\JWT;
use ArrayAccess;
use DateTime;
use DomainException;
use Exception;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use stdClass;
use UnexpectedValueException;
/**
* JSON Web Token implementation, based on this spec:
* https://tools.ietf.org/html/rfc7519
*
* PHP version 5
*
* @category Authentication
* @package Authentication_JWT
* @author Neuman Vong <neuman@twilio.com>
* @author Anant Narayanan <anant@php.net>
* @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
* @link https://github.com/firebase/php-jwt
*/
class JWT
{
private const ASN1_INTEGER = 0x02;
private const ASN1_SEQUENCE = 0x10;
private const ASN1_BIT_STRING = 0x03;
private const RSA_KEY_MIN_LENGTH = 2048;
/**
* When checking nbf, iat or expiration times,
* we want to provide some extra leeway time to
* account for clock skew.
*
* @var int
*/
public static $leeway = 0;
/**
* Allow the current timestamp to be specified.
* Useful for fixing a value within unit testing.
* Will default to PHP time() value if null.
*
* @var ?int
*/
public static $timestamp = null;
/**
* @var array<string, string[]>
*/
public static $supported_algs = [
'ES384' => ['openssl', 'SHA384'],
'ES256' => ['openssl', 'SHA256'],
'ES256K' => ['openssl', 'SHA256'],
'HS256' => ['hash_hmac', 'SHA256'],
'HS384' => ['hash_hmac', 'SHA384'],
'HS512' => ['hash_hmac', 'SHA512'],
'RS256' => ['openssl', 'SHA256'],
'RS384' => ['openssl', 'SHA384'],
'RS512' => ['openssl', 'SHA512'],
'EdDSA' => ['sodium_crypto', 'EdDSA'],
];
/**
* Decodes a JWT string into a PHP object.
*
* @param string $jwt The JWT
* @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray The Key or associative array of key IDs
* (kid) to Key objects.
* If the algorithm used is asymmetric, this is
* the public key.
* Each Key object contains an algorithm and
* matching key.
* Supported algorithms are 'ES384','ES256',
* 'HS256', 'HS384', 'HS512', 'RS256', 'RS384'
* and 'RS512'.
* @param stdClass $headers Optional. Populates stdClass with headers.
*
* @return stdClass The JWT's payload as a PHP object
*
* @throws InvalidArgumentException Provided key/key-array was empty or malformed
* @throws DomainException Provided JWT is malformed
* @throws UnexpectedValueException Provided JWT was invalid
* @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
* @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
* @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'
* @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim
*
* @uses jsonDecode
* @uses urlsafeB64Decode
*/
public static function decode(
string $jwt,
#[\SensitiveParameter] $keyOrKeyArray,
?stdClass &$headers = null
): stdClass {
// Validate JWT
$timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
if (empty($keyOrKeyArray)) {
throw new InvalidArgumentException('Key may not be empty');
}
$tks = \explode('.', $jwt);
if (\count($tks) !== 3) {
throw new UnexpectedValueException('Wrong number of segments');
}
list($headb64, $bodyb64, $cryptob64) = $tks;
$headerRaw = static::urlsafeB64Decode($headb64);
if (null === ($header = static::jsonDecode($headerRaw))) {
throw new UnexpectedValueException('Invalid header encoding');
}
if ($headers !== null) {
$headers = $header;
}
$payloadRaw = static::urlsafeB64Decode($bodyb64);
if (null === ($payload = static::jsonDecode($payloadRaw))) {
throw new UnexpectedValueException('Invalid claims encoding');
}
if (\is_array($payload)) {
// prevent PHP Fatal Error in edge-cases when payload is empty array
$payload = (object) $payload;
}
if (!$payload instanceof stdClass) {
throw new UnexpectedValueException('Payload must be a JSON object');
}
if (isset($payload->iat) && !\is_numeric($payload->iat)) {
throw new UnexpectedValueException('Payload iat must be a number');
}
if (isset($payload->nbf) && !\is_numeric($payload->nbf)) {
throw new UnexpectedValueException('Payload nbf must be a number');
}
if (isset($payload->exp) && !\is_numeric($payload->exp)) {
throw new UnexpectedValueException('Payload exp must be a number');
}
$sig = static::urlsafeB64Decode($cryptob64);
if (empty($header->alg)) {
throw new UnexpectedValueException('Empty algorithm');
}
if (empty(static::$supported_algs[$header->alg])) {
throw new UnexpectedValueException('Algorithm not supported');
}
$key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);
// Check the algorithm
if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
// See issue #351
throw new UnexpectedValueException('Incorrect key for this algorithm');
}
if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) {
// OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures
$sig = self::signatureToDER($sig);
}
if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) {
throw new SignatureInvalidException('Signature verification failed');
}
// Check the nbf if it is defined. This is the time that the
// token can actually be used. If it's not yet that time, abort.
if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) {
$ex = new BeforeValidException(
'Cannot handle token with nbf prior to ' . \date(DateTime::ATOM, (int) floor($payload->nbf))
);
$ex->setPayload($payload);
throw $ex;
}
// Check that this token has been created before 'now'. This prevents
// using tokens that have been created for later use (and haven't
// correctly used the nbf claim).
if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) {
$ex = new BeforeValidException(
'Cannot handle token with iat prior to ' . \date(DateTime::ATOM, (int) floor($payload->iat))
);
$ex->setPayload($payload);
throw $ex;
}
// Check if this token has expired.
if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
$ex = new ExpiredException('Expired token');
$ex->setPayload($payload);
$ex->setTimestamp($timestamp);
throw $ex;
}
return $payload;
}
/**
* Converts and signs a PHP array into a JWT string.
*
* @param array<mixed> $payload PHP array
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
* @param string $keyId
* @param array<string, string|string[]> $head An array with header elements to attach
*
* @return string A signed JWT
*
* @uses jsonEncode
* @uses urlsafeB64Encode
*/
public static function encode(
array $payload,
#[\SensitiveParameter] $key,
string $alg,
?string $keyId = null,
?array $head = null
): string {
$header = ['typ' => 'JWT'];
if (isset($head)) {
$header = \array_merge($header, $head);
}
$header['alg'] = $alg;
if ($keyId !== null) {
$header['kid'] = $keyId;
}
$segments = [];
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));
$signing_input = \implode('.', $segments);
$signature = static::sign($signing_input, $key, $alg);
$segments[] = static::urlsafeB64Encode($signature);
return \implode('.', $segments);
}
/**
* Sign a string with a given key and algorithm.
*
* @param string $msg The message to sign
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256',
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
*
* @return string An encrypted message
*
* @throws DomainException Unsupported algorithm or bad key was specified
*/
public static function sign(
string $msg,
#[\SensitiveParameter] $key,
string $alg
): string {
if (empty(static::$supported_algs[$alg])) {
throw new DomainException('Algorithm not supported');
}
list($function, $algorithm) = static::$supported_algs[$alg];
switch ($function) {
case 'hash_hmac':
if (!\is_string($key)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
self::validateHmacKeyLength($key, $algorithm);
return \hash_hmac($algorithm, $msg, $key, true);
case 'openssl':
$signature = '';
if (!$key = openssl_pkey_get_private($key)) {
throw new DomainException('OpenSSL unable to validate key');
}
if (str_starts_with($alg, 'RS')) {
self::validateRsaKeyLength($key);
} elseif (str_starts_with($alg, 'ES')) {
self::validateEcKeyLength($key, $alg);
}
$success = \openssl_sign($msg, $signature, $key, $algorithm);
if (!$success) {
throw new DomainException('OpenSSL unable to sign data');
}
if ($alg === 'ES256' || $alg === 'ES256K') {
$signature = self::signatureFromDER($signature, 256);
} elseif ($alg === 'ES384') {
$signature = self::signatureFromDER($signature, 384);
}
return $signature;
case 'sodium_crypto':
try {
return sodium_crypto_sign_detached($msg, self::validateEdDSAKey($key));
} catch (Exception $e) {
throw new DomainException($e->getMessage(), 0, $e);
}
}
throw new DomainException('Algorithm not supported');
}
/**
* Verify a signature with the message, key and method. Not all methods
* are symmetric, so we must have a separate verify and sign method.
*
* @param string $msg The original message (header and body)
* @param string $signature The original signature
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
* @param string $alg The algorithm
*
* @return bool
*
* @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
*/
private static function verify(
string $msg,
string $signature,
#[\SensitiveParameter] $keyMaterial,
string $alg
): bool {
if (empty(static::$supported_algs[$alg])) {
throw new DomainException('Algorithm not supported');
}
list($function, $algorithm) = static::$supported_algs[$alg];
switch ($function) {
case 'openssl':
if (!$key = openssl_pkey_get_public($keyMaterial)) {
throw new DomainException('OpenSSL unable to validate key');
}
if (str_starts_with($alg, 'RS')) {
self::validateRsaKeyLength($key);
} elseif (str_starts_with($alg, 'ES')) {
self::validateEcKeyLength($key, $alg);
}
$success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm);
if ($success === 1) {
return true;
}
if ($success === 0) {
return false;
}
// returns 1 on success, 0 on failure, -1 on error.
throw new DomainException(
'OpenSSL error: ' . \openssl_error_string()
);
case 'sodium_crypto':
try {
$key = self::validateEdDSAKey($keyMaterial);
if (\strlen($signature) === 0) {
throw new DomainException('Signature cannot be empty string');
}
return sodium_crypto_sign_verify_detached($signature, $msg, $key);
} catch (Exception $e) {
throw new DomainException($e->getMessage(), 0, $e);
}
case 'hash_hmac':
default:
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
self::validateHmacKeyLength($keyMaterial, $algorithm);
$hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
return self::constantTimeEquals($hash, $signature);
}
}
/**
* Decode a JSON string into a PHP object.
*
* @param string $input JSON string
*
* @return mixed The decoded JSON string
*
* @throws DomainException Provided string was invalid JSON
*/
public static function jsonDecode(string $input)
{
$obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
if ($errno = \json_last_error()) {
self::handleJsonError($errno);
} elseif ($obj === null && $input !== 'null') {
throw new DomainException('Null result with non-null input');
}
return $obj;
}
/**
* Encode a PHP array into a JSON string.
*
* @param array<mixed> $input A PHP array
*
* @return string JSON representation of the PHP array
*
* @throws DomainException Provided object could not be encoded to valid JSON
*/
public static function jsonEncode(array $input): string
{
$json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
if ($errno = \json_last_error()) {
self::handleJsonError($errno);
} elseif ($json === 'null') {
throw new DomainException('Null result with non-null input');
}
if ($json === false) {
throw new DomainException('Provided object could not be encoded to valid JSON');
}
return $json;
}
/**
* Decode a string with URL-safe Base64.
*
* @param string $input A Base64 encoded string
*
* @return string A decoded string
*
* @throws InvalidArgumentException invalid base64 characters
*/
public static function urlsafeB64Decode(string $input): string
{
return \base64_decode(self::convertBase64UrlToBase64($input));
}
/**
* Convert a string in the base64url (URL-safe Base64) encoding to standard base64.
*
* @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding)
*
* @return string A Base64 encoded string with standard characters (+/) and padding (=), when
* needed.
*
* @see https://www.rfc-editor.org/rfc/rfc4648
*/
public static function convertBase64UrlToBase64(string $input): string
{
$remainder = \strlen($input) % 4;
if ($remainder) {
$padlen = 4 - $remainder;
$input .= \str_repeat('=', $padlen);
}
return \strtr($input, '-_', '+/');
}
/**
* Encode a string with URL-safe Base64.
*
* @param string $input The string you want encoded
*
* @return string The base64 encode of what you passed in
*/
public static function urlsafeB64Encode(string $input): string
{
return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
}
/**
* Determine if an algorithm has been provided for each Key
*
* @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray
* @param string|null $kid
*
* @throws UnexpectedValueException
*
* @return Key
*/
private static function getKey(
#[\SensitiveParameter] $keyOrKeyArray,
?string $kid
): Key {
if ($keyOrKeyArray instanceof Key) {
return $keyOrKeyArray;
}
if (empty($kid) && $kid !== '0') {
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
}
if ($keyOrKeyArray instanceof CachedKeySet) {
// Skip "isset" check, as this will automatically refresh if not set
return $keyOrKeyArray[$kid];
}
if (!isset($keyOrKeyArray[$kid])) {
throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
}
return $keyOrKeyArray[$kid];
}
/**
* @param string $left The string of known length to compare against
* @param string $right The user-supplied string
* @return bool
*/
public static function constantTimeEquals(string $left, string $right): bool
{
if (\function_exists('hash_equals')) {
return \hash_equals($left, $right);
}
$len = \min(self::safeStrlen($left), self::safeStrlen($right));
$status = 0;
for ($i = 0; $i < $len; $i++) {
$status |= (\ord($left[$i]) ^ \ord($right[$i]));
}
$status |= (self::safeStrlen($left) ^ self::safeStrlen($right));
return ($status === 0);
}
/**
* Helper method to create a JSON error.
*
* @param int $errno An error number from json_last_error()
*
* @throws DomainException
*
* @return void
*/
private static function handleJsonError(int $errno): void
{
$messages = [
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
];
throw new DomainException(
isset($messages[$errno])
? $messages[$errno]
: 'Unknown JSON error: ' . $errno
);
}
/**
* Get the number of bytes in cryptographic strings.
*
* @param string $str
*
* @return int
*/
private static function safeStrlen(string $str): int
{
if (\function_exists('mb_strlen')) {
return \mb_strlen($str, '8bit');
}
return \strlen($str);
}
/**
* Convert an ECDSA signature to an ASN.1 DER sequence
*
* @param string $sig The ECDSA signature to convert
* @return string The encoded DER object
*/
private static function signatureToDER(string $sig): string
{
// Separate the signature into r-value and s-value
$length = max(1, (int) (\strlen($sig) / 2));
list($r, $s) = \str_split($sig, $length);
// Trim leading zeros
$r = \ltrim($r, "\x00");
$s = \ltrim($s, "\x00");
// Convert r-value and s-value from unsigned big-endian integers to
// signed two's complement
if (\ord($r[0]) > 0x7f) {
$r = "\x00" . $r;
}
if (\ord($s[0]) > 0x7f) {
$s = "\x00" . $s;
}
return self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(self::ASN1_INTEGER, $r) .
self::encodeDER(self::ASN1_INTEGER, $s)
);
}
/**
* Encodes a value into a DER object.
*
* @param int $type DER tag
* @param string $value the value to encode
*
* @return string the encoded object
*/
private static function encodeDER(int $type, string $value): string
{
$tag_header = 0;
if ($type === self::ASN1_SEQUENCE) {
$tag_header |= 0x20;
}
// Type
$der = \chr($tag_header | $type);
// Length
$der .= \chr(\strlen($value));
return $der . $value;
}
/**
* Encodes signature from a DER object.
*
* @param string $der binary signature in DER format
* @param int $keySize the number of bits in the key
*
* @return string the signature
*/
private static function signatureFromDER(string $der, int $keySize): string
{
// OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
list($offset, $_) = self::readDER($der);
list($offset, $r) = self::readDER($der, $offset);
list($offset, $s) = self::readDER($der, $offset);
// Convert r-value and s-value from signed two's compliment to unsigned
// big-endian integers
$r = \ltrim($r, "\x00");
$s = \ltrim($s, "\x00");
// Pad out r and s so that they are $keySize bits long
$r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
$s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
return $r . $s;
}
/**
* Reads binary DER-encoded data and decodes into a single object
*
* @param string $der the binary data in DER format
* @param int $offset the offset of the data stream containing the object
* to decode
*
* @return array{int, string|null} the new offset and the decoded object
*/
private static function readDER(string $der, int $offset = 0): array
{
$pos = $offset;
$size = \strlen($der);
$constructed = (\ord($der[$pos]) >> 5) & 0x01;
$type = \ord($der[$pos++]) & 0x1f;
// Length
$len = \ord($der[$pos++]);
if ($len & 0x80) {
$n = $len & 0x1f;
$len = 0;
while ($n-- && $pos < $size) {
$len = ($len << 8) | \ord($der[$pos++]);
}
}
// Value
if ($type === self::ASN1_BIT_STRING) {
$pos++; // Skip the first contents octet (padding indicator)
$data = \substr($der, $pos, $len - 1);
$pos += $len - 1;
} elseif (!$constructed) {
$data = \substr($der, $pos, $len);
$pos += $len;
} else {
$data = null;
}
return [$pos, $data];
}
/**
* Validate HMAC key length
*
* @param string $key HMAC key material
* @param string $algorithm The algorithm
*
* @throws DomainException Provided key is too short
*/
private static function validateHmacKeyLength(string $key, string $algorithm): void
{
$keyLength = \strlen($key) * 8;
$minKeyLength = (int) \str_replace('SHA', '', $algorithm);
if ($keyLength < $minKeyLength) {
throw new DomainException('Provided key is too short');
}
}
/**
* Validate RSA key length
*
* @param OpenSSLAsymmetricKey $key RSA key material
* @throws DomainException Provided key is too short
*/
private static function validateRsaKeyLength(#[\SensitiveParameter] OpenSSLAsymmetricKey $key): void
{
if (!$keyDetails = openssl_pkey_get_details($key)) {
throw new DomainException('Unable to validate key');
}
if ($keyDetails['bits'] < self::RSA_KEY_MIN_LENGTH) {
throw new DomainException('Provided key is too short');
}
}
/**
* Validate RSA key length
*
* @param OpenSSLAsymmetricKey $key RSA key material
* @param string $algorithm The algorithm
* @throws DomainException Provided key is too short
*/
private static function validateEcKeyLength(
#[\SensitiveParameter] OpenSSLAsymmetricKey $key,
string $algorithm
): void {
if (!$keyDetails = openssl_pkey_get_details($key)) {
throw new DomainException('Unable to validate key');
}
$minKeyLength = (int) \str_replace('ES', '', $algorithm);
if ($keyDetails['bits'] < $minKeyLength) {
throw new DomainException('Provided key is too short');
}
}
/**
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
* @return non-empty-string
*/
private static function validateEdDSAKey(#[\SensitiveParameter] $keyMaterial): string
{
if (!\function_exists('sodium_crypto_sign_verify_detached')) {
throw new DomainException('libsodium is not available');
}
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $keyMaterial));
$key = self::urlsafeB64Decode((string) end($lines));
if (\strlen($key) === 0) {
throw new DomainException('Key cannot be empty string');
}
return $key;
}
}
@@ -0,0 +1,20 @@
<?php
namespace Firebase\JWT;
interface JWTExceptionWithPayloadInterface
{
/**
* Get the payload that caused this exception.
*
* @return object
*/
public function getPayload(): object;
/**
* Get the payload that caused this exception.
*
* @param object $payload
* @return void
*/
public function setPayload(object $payload): void;
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace Firebase\JWT;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use TypeError;
class Key
{
/**
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
* @param string $algorithm
*/
public function __construct(
#[\SensitiveParameter] private $keyMaterial,
private string $algorithm
) {
if (
!\is_string($keyMaterial)
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
&& !$keyMaterial instanceof OpenSSLCertificate
) {
throw new TypeError('Key material must be a string, OpenSSLCertificate, or OpenSSLAsymmetricKey');
}
if (empty($keyMaterial)) {
throw new InvalidArgumentException('Key material must not be empty');
}
if (empty($algorithm)) {
throw new InvalidArgumentException('Algorithm must not be empty');
}
}
/**
* Return the algorithm valid for this key
*
* @return string
*/
public function getAlgorithm(): string
{
return $this->algorithm;
}
/**
* @return string|OpenSSLAsymmetricKey|OpenSSLCertificate
*/
public function getKeyMaterial()
{
return $this->keyMaterial;
}
}
@@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class SignatureInvalidException extends \UnexpectedValueException
{
}
+203
View File
@@ -0,0 +1,203 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+19
View File
@@ -0,0 +1,19 @@
Google PHP API Client Services
==============================
[Reference Documentation](https://googleapis.github.io/google-api-php-client-services)
**NOTE**: please check to see if the package you'd like to install is available in our
list of [Google cloud packages](https://cloud.google.com/php/docs/reference) first, as
these are the recommended libraries.
## Requirements
[Google API PHP Client](https://github.com/googleapis/google-api-php-client/releases)
## Usage
This library is automatically updated daily with new API changes, and tagged weekly.
It is installed as part of the
[Google API PHP Client](https://github.com/googleapis/google-api-php-client/releases)
library via Composer, which will pull down the most recent tag.

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