diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..147498b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +version: 2 +updates: + - package-ecosystem: composer + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + groups: + composer-development-dependencies: + dependency-type: development + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + groups: + github-actions: + patterns: + - "*" + update-types: + - minor + - patch diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 599c8b4..eface39 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,11 @@ ## Maintainer Checklist +- [ ] The scope, user impact, and compatibility implications are described. +- [ ] Security-sensitive inputs, outputs, capabilities, nonces, SQL, and filesystem paths were reviewed where applicable. +- [ ] Regression coverage was added for fixes and edge cases. - [ ] Version fields remain aligned when this is a release change. -- [ ] `composer lint` passes. -- [ ] `composer test` passes. +- [ ] `composer validate --strict`, `composer audit --locked`, and `composer ci` pass. +- [ ] JavaScript syntax/tests and WordPress Plugin Check pass. +- [ ] Documentation, changelog, localized release summaries, and security notes are synchronized. - [ ] WordPress.org package-only files remain separate from repository-only docs. +- [ ] The runtime archive was inspected for forbidden files and secrets. diff --git a/.github/social-preview.png b/.github/social-preview.png new file mode 100644 index 0000000..fe94fd7 Binary files /dev/null and b/.github/social-preview.png differ diff --git a/AGENTS.md b/AGENTS.md index 7373b3a..990cd4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,4 +76,4 @@ For release-sensitive changes, run WordPress Plugin Check against a clean filter Runtime packages must exclude repository metadata, tests, docs, Composer files, CI configuration, `.wordpress-org/`, `.env*`, `auth.json`, caches, coverage, `node_modules`, `vendor`, and prebuilt ZIP files. WordPress.org listing assets are synchronized separately. -Use concise imperative commit messages. This public source mirror does not accept external pull requests; maintainer changes may land directly on `main` only after the complete release gate passes and the remote branch is still aligned. Never force-push or bypass failed checks. Keep CI green and call out any check that could not be run locally. +Use concise imperative commit messages. This public source mirror does not accept external pull requests; maintainer changes use internal pull requests so protected-branch checks and CodeQL complete before merge. Never force-push, bypass failed checks, or disable protection to land a release. Keep CI green and call out any check that could not be run locally. diff --git a/CITATION.cff b/CITATION.cff index d750b65..4f99374 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,8 +1,17 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." title: "Mikesoft TeamVault" +abstract: "Private document management for WordPress teams, with protected storage and granular access control." authors: - family-names: Gasperini given-names: Michael url: "https://github.com/TheStreamCode/mikesoft-teamvault" -license: GPL-2.0 +repository-code: "https://github.com/TheStreamCode/mikesoft-teamvault" +version: "3.2.6" +date-released: "2026-08-08" +keywords: + - WordPress + - document management + - access control + - private storage +license: GPL-2.0-or-later diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ae52b7..52a133f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,7 @@ This repository is a public source mirror for the Mikesoft TeamVault plugin. - External pull requests are not accepted. - GitHub Issues are not the support channel for the plugin. - End-user support and bug reports should go through the [official WordPress.org support forum](https://wordpress.org/support/plugin/mikesoft-teamvault/). +- Maintainer changes use internal pull requests so required CI and security checks run before protected `main` is updated. ## What This Repository Is For diff --git a/README.de.md b/README.de.md index 6c3f12f..a531b2b 100644 --- a/README.de.md +++ b/README.de.md @@ -11,7 +11,7 @@ Privater Dokumenten-Arbeitsbereich für WordPress-Teams, Agenturen und Betriebsabläufe, die eine kontrollierte Dateifreigabe außerhalb der Mediathek benötigen. -Aktuelle Plugin-Version: `3.2.5`. +Aktuelle Plugin-Version: `3.2.6`. Direkt über WordPress.org verfügbar und für aktuelle WordPress-Versionen gepflegt. @@ -61,6 +61,8 @@ Governance-Funktionen (alle kostenlos, seit 2.6): ## Neueste Version +Version `3.2.6` stärkt die Upload-Eingrenzung und die Zuverlässigkeit von ZIP-Exporten, korrigiert Randfälle bei REST-Governance-Einstellungen und der Multisite-Aktivierung und hält Werkzeugleistenaktionen in gängigen WordPress-Desktop-Layouts zugänglich. Außerdem verbessert sie die Präsentation und Governance des Repositorys. Konfigurationsänderungen sind nicht erforderlich. + Version `3.2.5` schließt die in 3.2.4 begonnene Ausgabekodierung ab: Das Detailfenster maskiert die gespeicherte Dateiendung nun vor der Darstellung. Der Wert ist durch die Upload- und Reindex-Validierung bereits auf `[a-z0-9]` beschränkt; es handelt sich also um Verteidigung in der Tiefe und nicht um die Behebung einer ausnutzbaren Schwachstelle. Konfigurationsänderungen sind nicht erforderlich. Version `3.2.4` härtet die Auslieferung privater Dateien, indem No-Cache-Header beibehalten und Speicherpfade aus Diagnoseprotokollen ferngehalten werden. Browserziele sind auf HTTP(S)-URLs gleichen Ursprungs beschränkt, die Spracheinstellung verwendet eine ausdrückliche Positivliste und wiederholte Berechtigungsprüfungen nutzen während einer Anfrage zwischengespeicherte Ordner-, Regel- und Gruppenabfragen. CI, Paketfilter, Sicherheitshinweise sowie Entwicklungs- und Veröffentlichungsdokumentation wurden ebenfalls verbessert. Es sind keine Konfigurationsänderungen erforderlich. diff --git a/README.es.md b/README.es.md index 357c876..dc84da6 100644 --- a/README.es.md +++ b/README.es.md @@ -11,7 +11,7 @@ Espacio de trabajo de documentos privados para equipos, agencias y operaciones de WordPress que necesitan compartir archivos de forma controlada fuera de la Biblioteca de medios. -Versión actual del plugin: `3.2.5`. +Versión actual del plugin: `3.2.6`. Disponible directamente en WordPress.org y mantenido para las versiones actuales de WordPress. @@ -61,6 +61,8 @@ Capacidades de gobernanza (todas gratuitas, desde la versión 2.6): ## Última versión +La versión `3.2.6` refuerza la contención de las subidas y la fiabilidad de las exportaciones ZIP, corrige casos límite en los ajustes de gobernanza REST y la activación multisitio, y mantiene accesibles las acciones de la barra de herramientas en los diseños de escritorio habituales de WordPress. También mejora la presentación y la gobernanza del repositorio. No se requieren cambios de configuración. + La versión `3.2.5` completa el trabajo de codificación de salida iniciado en la 3.2.4: el panel de detalles del archivo ahora escapa la extensión almacenada antes de mostrarla. El valor ya está limitado a `[a-z0-9]` por la validación de subida y reindexación, por lo que se trata de defensa en profundidad y no de la corrección de una vulnerabilidad explotable. No se requieren cambios de configuración. La versión `3.2.4` refuerza la entrega de archivos privados al conservar las cabeceras no-cache y evitar que las rutas de almacenamiento aparezcan en los registros de diagnóstico. Los destinos del navegador se limitan a URL HTTP(S) del mismo origen, el ajuste de idioma utiliza una lista permitida explícita y las comprobaciones repetidas de permisos reutilizan durante la petición las consultas de carpetas, reglas y grupos. También se han reforzado la CI, los filtros de empaquetado, las indicaciones de seguridad y la documentación de desarrollo y publicación. No se requieren cambios de configuración. diff --git a/README.fr.md b/README.fr.md index 0025ef3..ee0e873 100644 --- a/README.fr.md +++ b/README.fr.md @@ -11,7 +11,7 @@ Espace de travail documentaire privé pour les équipes, agences et services d'exploitation WordPress qui ont besoin d'un partage de fichiers contrôlé en dehors de la Médiathèque. -Version actuelle du plugin : `3.2.5`. +Version actuelle du plugin : `3.2.6`. Disponible directement sur WordPress.org et maintenu pour les versions actuelles de WordPress. @@ -61,6 +61,8 @@ Fonctionnalités de gouvernance (toutes gratuites, depuis la 2.6) : ## Dernière version +La version `3.2.6` renforce le confinement des téléversements et la fiabilité des exports ZIP, corrige des cas limites dans les réglages de gouvernance REST et l'activation multisite, et maintient les actions de la barre d'outils accessibles dans les dispositions de bureau WordPress courantes. Elle améliore également la présentation et la gouvernance du dépôt. Aucune modification de configuration n'est nécessaire. + La version `3.2.5` achève le travail d'encodage des sorties entamé en 3.2.4 : le panneau de détails du fichier échappe désormais l'extension enregistrée avant de l'afficher. Cette valeur est déjà limitée à `[a-z0-9]` par la validation de téléversement et de réindexation ; il s'agit donc d'une défense en profondeur et non de la correction d'une faille exploitable. Aucune modification de configuration n'est nécessaire. La version `3.2.4` renforce la distribution des fichiers privés en conservant les en-têtes no-cache et en empêchant l'apparition des chemins de stockage dans les journaux de diagnostic. Les destinations du navigateur sont limitées aux URL HTTP(S) de même origine, le réglage de la langue utilise une liste d'autorisation explicite et les contrôles d'autorisation répétés réutilisent, pendant la requête, les recherches de dossiers, de règles et de groupes. La CI, les filtres de paquetage, les consignes de sécurité et la documentation de développement et de publication ont également été renforcés. Aucune modification de configuration n'est nécessaire. diff --git a/README.it.md b/README.it.md index f2ed989..09b379d 100644 --- a/README.it.md +++ b/README.it.md @@ -11,7 +11,7 @@ Spazio di lavoro documentale privato per team, agenzie e reparti operativi WordPress che hanno bisogno di condividere file in modo controllato al di fuori della Libreria Media. -Versione attuale del plugin: `3.2.5`. +Versione attuale del plugin: `3.2.6`. Disponibile direttamente su WordPress.org e mantenuto per le versioni correnti di WordPress. @@ -61,6 +61,8 @@ Funzionalità di governance (tutte gratuite, dalla versione 2.6): ## Ultima versione +La versione `3.2.6` rafforza il contenimento degli upload e l'affidabilità degli export ZIP, corregge casi limite nelle impostazioni di governance REST e nell'attivazione multisito, e mantiene accessibili le azioni della barra strumenti nei comuni layout desktop di WordPress. Migliora inoltre la presentazione e la governance del repository. Non sono richieste modifiche alla configurazione. + La versione `3.2.5` completa il lavoro di codifica dell'output iniziato con la 3.2.4: il pannello dei dettagli file applica ora l'escaping all'estensione memorizzata prima di visualizzarla. Il valore è già limitato a `[a-z0-9]` dalla validazione di upload e reindicizzazione, quindi si tratta di difesa in profondità e non della correzione di una vulnerabilità sfruttabile. Non sono richieste modifiche alla configurazione. La versione `3.2.4` rafforza la distribuzione dei file privati mantenendo gli header no-cache ed evitando che i percorsi dello storage compaiano nei log diagnostici. Le destinazioni del browser sono limitate a URL HTTP(S) same-origin, l'impostazione della lingua usa una allowlist esplicita e i controlli ripetuti dei permessi riutilizzano, per la durata della richiesta, le query su cartelle, regole e gruppi. Sono stati inoltre migliorati CI, filtri di packaging, indicazioni di sicurezza e documentazione di sviluppo e rilascio. Non sono richieste modifiche alla configurazione. diff --git a/README.md b/README.md index 1747133..040d490 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,47 @@ -# Mikesoft TeamVault +

+ TeamVault +

-[![CI](https://github.com/TheStreamCode/mikesoft-teamvault/actions/workflows/ci.yml/badge.svg)](https://github.com/TheStreamCode/mikesoft-teamvault/actions/workflows/ci.yml) -[![WordPress Plugin Version](https://img.shields.io/wordpress/plugin/v/mikesoft-teamvault?label=WordPress.org)](https://wordpress.org/plugins/mikesoft-teamvault/) -[![WordPress Tested](https://img.shields.io/wordpress/plugin/tested/mikesoft-teamvault?label=Tested%20up%20to)](https://wordpress.org/plugins/mikesoft-teamvault/) -[![PHP](https://img.shields.io/badge/PHP-8.0%2B-777BB4?logo=php&logoColor=white)](https://www.php.net/) -[![License](https://img.shields.io/badge/License-GPL%20v2%2B-blue.svg)](LICENSE) -[![Sponsor](https://img.shields.io/badge/Sponsor-%E2%9D%A4-db61a2?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/TheStreamCode) +

Mikesoft TeamVault

-**English** · [Italiano](README.it.md) · [Français](README.fr.md) · [Español](README.es.md) · [Deutsch](README.de.md) +

Private document management for WordPress teams, with protected storage and granular access control.

-Private document workspace for WordPress teams, agencies, and operations that need controlled file sharing outside the Media Library. +

+ Install from WordPress.org · + Documentation · + Security · + Support +

-Current plugin version: `3.2.5`. +

+ CI + WordPress Plugin Version + WordPress Tested + PHP 8.0+ + GPL v2 or later +

-Available directly from WordPress.org and maintained against current WordPress releases. +

English · Italiano · Français · Español · Deutsch

-If TeamVault is useful to you, consider [sponsoring the project on GitHub](https://github.com/sponsors/TheStreamCode) — it is developed and maintained for free, and sponsorships help keep it going. +> [!NOTE] +> This is the public source mirror for TeamVault. Installations and updates are distributed through [WordPress.org](https://wordpress.org/plugins/mikesoft-teamvault/); user support is handled in the [official support forum](https://wordpress.org/support/plugin/mikesoft-teamvault/). -## Overview +Current plugin version: `3.2.6`. -Mikesoft TeamVault adds a private document workspace inside the WordPress admin. -It is designed for teams that need to organize, preview, export, and share sensitive files without exposing them through normal Media Library URLs. +![TeamVault file manager — folder tree, file cards with type-aware icons and image thumbnails, and the details panel with inline preview](.wordpress-org/assets/screenshot-1.jpg) -Files are stored in protected storage and delivered through authenticated WordPress handlers instead of public media URLs. +## Why TeamVault -![TeamVault file manager — folder tree, file cards with type-aware icons and image thumbnails, and the details panel with inline preview](.wordpress-org/assets/screenshot-1.jpg) +TeamVault gives internal teams, agencies, and operations a dedicated private workspace inside WordPress instead of mixing sensitive documents with public Media Library assets. + +- **Protected delivery:** files are served through authenticated WordPress handlers instead of normal public media URLs. +- **Granular governance:** groups and inheritable per-folder rules control view, upload, download, delete, and management actions. +- **Operational visibility:** quotas, access reports, activity logs, CSV export, and email notifications support accountable workflows. +- **Practical file management:** drag-and-drop upload, search, previews, folder operations, and ZIP export live in one focused interface. + +Typical uses include internal company documents, agency-to-client delivery, partner exchanges, and back-office archives. + +## Product Tour | Per-folder permissions | Search across the vault | Storage quotas | | :---: | :---: | :---: | @@ -32,58 +49,25 @@ Files are stored in protected storage and delivered through authenticated WordPr | **Groups** | **Activity log** | **Settings** | | [![User groups](.wordpress-org/assets/screenshot-5.jpg)](.wordpress-org/assets/screenshot-5.jpg) | [![Audit trail](.wordpress-org/assets/screenshot-6.jpg)](.wordpress-org/assets/screenshot-6.jpg) | [![Plugin settings](.wordpress-org/assets/screenshot-7.jpg)](.wordpress-org/assets/screenshot-7.jpg) | -Typical use cases include: - -- internal company documents -- agency-to-client document delivery from WordPress admin -- partner or vendor file exchanges -- back-office archives that should stay out of the public Media Library - -Core capabilities include: +## Capabilities - private storage outside the normal Media Library workflow -- shared access for authorized internal users -- folder creation, rename, move, and delete operations -- drag-and-drop uploads with file validation -- inline preview for supported file types, including PDFs -- ZIP export for folders or the full document library -- activity logging for operational traceability -- maintenance tools for orphan cleanup and storage reindex - -Governance capabilities (all free, since 2.6): - -- TeamVault groups to organize users into departments or teams, independent from WordPress roles -- per-folder permissions with granular actions (view, upload, download, delete, manage) for users and groups, with inheritance and explicit child overrides -- preview-only access that allows viewing without download or ZIP export +- capability, optional whitelist, user, group, and per-folder access controls +- folder creation, rename, move, delete, inheritance, and explicit child overrides +- validated drag-and-drop uploads and inline previews, including PDFs +- preview-only access that blocks downloads and ZIP exports - per-user and per-group storage quotas enforced before upload -- access reports (who viewed or downloaded what) with filters and a CSV export of the activity log -- email notifications for upload, download, delete, and access-denied events - -## Latest Release +- access reports, activity logging, CSV export, and event notifications +- ZIP export plus orphan cleanup and storage reindex maintenance tools +- English, Italian, French, Spanish, and German admin interfaces -Version `3.2.5` completes the output-encoding pass started in 3.2.4: the file details panel now escapes the stored file extension before rendering it. The value is already restricted to `[a-z0-9]` by upload and reindex validation, so this is defense in depth rather than a fixed exploit. No configuration changes are required. +All governance capabilities are included in the free plugin. -Version `3.2.4` hardens private file delivery by preserving no-cache headers and keeping storage paths out of diagnostic logs. Browser destinations are restricted to same-origin HTTP(S), the interface-language setting uses an explicit allowlist, and repeated permission checks reuse request-scoped folder, rule, and group lookups. CI, packaging filters, security guidance, and development/release documentation have also been strengthened. No configuration changes are required. - -Version `3.2.3` strengthens upload and inline-preview validation, makes permission and group updates transactional, and keeps file, folder, quota, and export operations consistent when storage or database writes fail. Explicitly shared child folders remain discoverable when a parent is hidden, audit CSV exports are safer to open in spreadsheet applications, new installations again default to the Automatic interface language, and the Plugins screen now identifies the author simply as Mikesoft. - -Version `3.2.2` refreshes the **file-type icons** throughout the file manager. PDF, Word, Excel, PowerPoint, CSV, text, archive, audio, video, and image files now show clear, recognizable colored badges with the format label — in the file grid, the list view, and the details preview — replacing the previous monochrome glyphs. - -Version `3.2.0` improves the file manager and streamlines the settings. Folders that carry their own permission rules now show a **lock badge** so restricted areas are recognizable at a glance, the **empty-folder view** offers a clear drop zone with quick upload / new-folder actions, and the interface received accessibility improvements (higher-contrast text, labeled icon buttons, screen-reader announcements). The **white-label branding option was removed** to keep TeamVault focused on secure document management; the plugin now always uses its standard identity and any previously saved brand settings are cleaned up on update. - -Version `3.1.1` makes the interface language **follow the WordPress language automatically**. The new default "Automatic" mode matches the WordPress site/admin locale — Italian, French, Spanish, or German when supported, English otherwise — so the plugin speaks the same language as the rest of the dashboard with no configuration. A specific language can still be forced in the settings. - -Version `3.1.0` adds a fully translated plugin admin interface: the interface language selector now offers **Italian, French, Spanish, and German** in addition to English, covering every screen, label, warning, and error message. This README is also available in those languages via the links at the top. - -Version `3.0.0` is a security and reliability milestone. Search results are now filtered through the per-folder permission engine, so restricted users can no longer discover file names or metadata from folders they cannot view. The generated storage `.htaccess` denies direct access on Apache 2.4 in addition to Apache 2.2 and IIS, and storage quotas are enforced with a database lock so concurrent uploads cannot jointly exceed a limit. Downloads and inline previews gain HTTP Range support (`Accept-Ranges` / `206 Partial Content`) for resumable transfers and range-seeking PDF viewers on large files. The folder permissions dialog now warns when rules exist but the root has none, the admin menu icon matches native WordPress styling, and the admin JavaScript was split into focused modules with no change in behavior. - -Version `2.6` introduced the free document **governance suite**: TeamVault groups, per-folder permissions with inheritance and granular actions (view, upload, download, delete, manage), preview-only access, per-user and per-group storage quotas, access reports with CSV export, and email notifications. Existing installs are unaffected because folders with no rules keep the prior behavior. +## Latest Release -Why teams adopt TeamVault: +Version `3.2.6` hardens upload containment and ZIP export reliability, corrects REST governance and multisite activation edge cases, and keeps toolbar actions accessible on common WordPress desktop layouts. It also refreshes repository presentation and release governance. No configuration changes are required. -- it creates a dedicated private document area instead of overloading the Media Library -- it adds capability-based access control with an optional whitelist layer, plus per-folder permissions and groups for finer governance -- it keeps export, maintenance, and recovery workflows focused on operational files +See the [full changelog](changelog.txt) and [GitHub releases](https://github.com/TheStreamCode/mikesoft-teamvault/releases) for complete history. ## Requirements @@ -235,6 +219,7 @@ This repository is the public source mirror for the plugin. - `.wordpress-org/assets/icon-256x256.png` is the primary full-color icon for the WordPress.org listing. - `.wordpress-org/assets/icon.svg` is the scalable companion asset for the WordPress.org listing. - `.wordpress-org/assets/screenshot-1.jpg` … `screenshot-7.jpg` are the WordPress.org listing screenshots, also used in this README. +- `.github/social-preview.png` is the dedicated 1280×640 GitHub social preview; upload this file in the repository's social preview settings after brand changes. - `assets/logo-teamvault.svg` is the in-plugin admin logo used inside the TeamVault interface. These assets serve different surfaces and should stay aligned to the same brand without forcing the runtime plugin UI to match WordPress.org packaging constraints. diff --git a/SECURITY.md b/SECURITY.md index fb4ace1..3762bf4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,6 +12,7 @@ Do not disclose security issues in public support forums or public issue tracker Report vulnerabilities privately to: +- [GitHub Private Vulnerability Reporting](https://github.com/TheStreamCode/mikesoft-teamvault/security/advisories/new) (preferred) - `teamvault@mikesoft.it` Include the following details when possible: @@ -25,8 +26,10 @@ Include the following details when possible: ## Response Expectations -- Reports are reviewed privately. -- Confirmed issues are prioritized for a maintenance release. +- We aim to acknowledge complete reports within 3 business days. +- We aim to provide an initial assessment within 7 business days. +- Confirmed issues are prioritized for a coordinated maintenance release; remediation timing depends on severity and complexity. +- Reporters receive status updates when the assessment or expected release window changes. - Public disclosure should wait until a fix is available to users. ## Security Scope diff --git a/assets/css/admin.css b/assets/css/admin.css index 1de9fcf..db61dea 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -1224,7 +1224,9 @@ box-shadow: inset 0 0 0 1px var(--pdm-color-primary); } -@media (max-width: 1200px) { +/* The WordPress desktop admin menu consumes 160px of the viewport. Moving the details + panel off-canvas at 1360px preserves the app's intended 1200px content threshold. */ +@media (max-width: 1360px) { .pdm-details { position: fixed; right: 0; diff --git a/assets/js/admin-app-core.js b/assets/js/admin-app-core.js index 7fbeaa7..971aa1e 100644 --- a/assets/js/admin-app-core.js +++ b/assets/js/admin-app-core.js @@ -57,7 +57,9 @@ }, isDetailsDrawerViewport() { - return window.innerWidth <= 1200; + // WordPress keeps a 160px admin menu beside the plugin. Switch the details + // panel to a drawer before that reduced content viewport can cover toolbar actions. + return window.innerWidth <= 1360; }, cacheElements() { diff --git a/changelog.txt b/changelog.txt index 331ee98..e98b146 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,6 +2,15 @@ Mikesoft TeamVault Changelog Entries are ordered newest first. Release dates are included where recorded. +## 3.2.6 - 2026-08-08 +- Security: upload destinations now pass through the shared storage-boundary and intermediate-symlink verifier before any destination stream is opened. +- Reliability: ZIP exports check directory, file, and finalization results, clean up on any throwable failure, and bound recursive traversal when corrupt folder metadata contains a cycle. +- Governance: REST boolean strings are normalized with WordPress semantics, and group updates reject names that become empty after sanitization. +- Multisite: new-site initialization runs only when TeamVault is active network-wide, preventing site-local activation from provisioning unrelated sites. +- Admin UI: the details panel becomes an off-canvas drawer at 1360px so the WordPress desktop admin menu cannot make it cover Upload or other toolbar actions. +- Repository: the GitHub README now leads with the product, installation, security, and visual proof; repository metadata, dependency automation, citation data, and vulnerability-reporting guidance were refreshed. +- Tests: added regression coverage for verified upload paths, governance normalization, multisite activation scope, corrupt export cycles, and the responsive details-panel threshold. + ## 3.2.5 - 2026-08-02 - Security: the file details panel escapes the stored file extension before HTML interpolation. The value is already constrained to `[a-z0-9]` by upload and reindex validation, so this is defense in depth rather than a fixed exploit; it closes the last unescaped server-derived value in the admin JavaScript after the 3.2.4 output-encoding pass. - Release metadata: aligned the plugin header, stable tag, localized repository documentation, WordPress.org changelog and upgrade notice with the real release date. diff --git a/docs/maintainer/release.md b/docs/maintainer/release.md index 7e0365a..656dadf 100644 --- a/docs/maintainer/release.md +++ b/docs/maintainer/release.md @@ -35,7 +35,7 @@ The deploy script already validates plugin version and stable tag alignment. 9. Smoke test the file browser REST endpoint with plain permalinks when REST URL handling changes. 10. Confirm `.wordpress-org/assets/` contains the expected public assets. 11. Build the release ZIP and verify that repository-only files and sensitive local files are absent. -12. Commit the release, push `main`, and publish the matching GitHub tag and release with the ZIP attached. +12. Commit the release on a maintainer branch, open an internal pull request, wait for required CI and CodeQL checks, merge into protected `main`, and publish the matching GitHub tag and release with the ZIP attached. 13. Run the WordPress.org deployment script from the workspace root. 14. Verify the public GitHub release and the WordPress.org `trunk` and version tag. @@ -58,7 +58,7 @@ For Italian listing copy, translate the plugin readme strings in the plugin's De From the workspace root: ```powershell -.\deployment\deploy-to-wordpress.ps1 -Version 3.2.5 -Username thestreamcode +.\deployment\deploy-to-wordpress.ps1 -Version 3.2.6 -Username thestreamcode ``` The script never accepts or forwards an SVN password. Authenticate through SVN's interactive prompt or its operating-system credential store so credentials are not exposed in native process arguments. diff --git a/docs/maintainer/security-review.md b/docs/maintainer/security-review.md index a01a54f..b88d783 100644 --- a/docs/maintainer/security-review.md +++ b/docs/maintainer/security-review.md @@ -1,4 +1,4 @@ -# Security Review — 2026-08-02 +# Security Review — 2026-08-08 ## Scope and Method @@ -11,7 +11,7 @@ No critical issue was identified. The findings below were fixed in the reviewed ### SEC-001 — Missing output encoding for the avatar initial - Severity: Low -- Status: Fixed +- Status: Fixed in 3.2.4 - Location: `assets/js/admin-app.js` (user-search result rendering) - Rule: `web-frontend-xss-dom-001` - Evidence: the first character of the WordPress display name was interpolated into HTML without contextual escaping. @@ -22,7 +22,7 @@ No critical issue was identified. The findings below were fixed in the reviewed ### SEC-002 — Unvalidated REST-provided browser destinations - Severity: Low -- Status: Fixed +- Status: Fixed in 3.2.4 - Location: `assets/js/admin-app.js`, `assets/js/admin-app-core.js`, and `assets/js/admin-app-governance.js` - Rule: `web-frontend-untrusted-url-001` - Evidence: preview, download, and audit-export URLs from localized or REST data were assigned to `href`, `src`, `window.location`, or `window.open` without an explicit protocol and origin allowlist. Some preview URLs were also inserted into HTML attributes without escaping. @@ -33,7 +33,7 @@ No critical issue was identified. The findings below were fixed in the reviewed ### SEC-003 — Cache semantics for private file responses - Severity: Medium -- Status: Fixed +- Status: Fixed in 3.2.4 - Location: `includes/class-mstv-download.php`, `includes/class-mstv-preview.php`, and `includes/class-mstv-export.php` - Evidence: the handlers called WordPress `nocache_headers()` and then replaced part of that policy with `must-revalidate` and `Pragma: public` headers. - Impact: browsers or intermediaries could retain authenticated private content longer than intended, depending on their interpretation of the conflicting headers. @@ -43,7 +43,7 @@ No critical issue was identified. The findings below were fixed in the reviewed ### SEC-004 — Absolute private paths in diagnostic logs - Severity: Low -- Status: Fixed +- Status: Fixed in 3.2.4 - Location: `includes/class-mstv-download.php` and `includes/class-mstv-preview.php` - Evidence: read and stream failures included the absolute storage path in PHP error logs. - Impact: anyone with log access could learn host directory layout and private storage locations, increasing the value of another local or configuration weakness. @@ -53,7 +53,7 @@ No critical issue was identified. The findings below were fixed in the reviewed ### SEC-005 — Settings language accepted a broader value set than the UI - Severity: Low -- Status: Fixed +- Status: Fixed in 3.2.4 - Location: `includes/class-mstv-admin.php` (`handle_save_settings`) - Evidence: the direct admin-post handler used generic text sanitization for the interface language instead of the existing supported-language allowlist. - Impact: crafted requests could persist unsupported configuration and create inconsistent fallback behavior. @@ -71,6 +71,66 @@ No critical issue was identified. The findings below were fixed in the reviewed - Resolution: the value is coerced to a string and passed through the existing escaper before interpolation. A regression test asserts the escaped form is present and the raw form is absent. - False-positive notes: this is the same class as SEC-001 and completes the 3.2.4 output-encoding pass; after this change no server-derived value in the admin JavaScript reaches an HTML sink unescaped. +## Release Review — 3.2.6 + +The full repository was reviewed again on 2026-08-08, including lifecycle and multisite behavior, REST authorization and validation, governance settings, upload/storage boundaries, ZIP export, browser rendering and URL sinks, SQL construction, uninstall cleanup, CI, packaging, and deployment tooling. The existing public API namespace, response shapes, database schema, option names, and runtime visual assets remain unchanged. The resolved findings below ship in version 3.2.6. + +### SEC-007 — Upload writes did not use the complete symlink-aware path verifier + +- Severity: Medium +- Status: Fixed in 3.2.6 +- Location: `includes/class-mstv-storage.php` (`store_uploaded_file`) +- Rule: `filesystem-path-boundary-001` +- Evidence: the upload destination used a lexical storage-root check before opening the destination, while other filesystem writes used `MSTV_Filesystem::get_verified_path()`, which also rejects symlinks in intermediate path components. +- Impact: if a local process or storage operator had already placed an intermediate symlink inside the TeamVault storage tree, an authorized upload could follow it and write the generated file outside the configured storage root. +- Resolution: upload destinations now pass through the shared boundary and symlink verifier before any destination stream is opened. A regression test verifies that a rejected destination is never resolved or written through the weaker path. +- False-positive notes: no remote method for creating such a symlink was found. Exploitation requires an already modified storage filesystem, but the prior behavior weakened an explicit containment boundary and could amplify a local or shared-storage compromise. + +### SEC-008 — REST boolean strings could invert governance settings + +- Severity: Low +- Status: Fixed in 3.2.6 +- Location: `includes/class-mstv-rest-governance.php` (`update_notifications`, `update_quotas`) +- Evidence: direct PHP boolean casts treat the string `"false"` as true, including the administrator-recipient flag. +- Impact: a nonstandard but valid REST client could unintentionally enable notifications, administrator recipients, or quota enforcement while sending a false boolean string. +- Resolution: the affected fields now use WordPress boolean normalization. Regression tests cover string-form false values without changing the JSON response contract. + +### SEC-009 — Group updates accepted a name that became empty after sanitization + +- Severity: Low +- Status: Fixed in 3.2.6 +- Location: `includes/class-mstv-rest-governance.php` (`update_group`) +- Evidence: the update path checked the raw name for emptiness but did not repeat the check after `sanitize_text_field()`, unlike group creation. +- Impact: a crafted administrator request could persist an empty display name and a fallback slug, degrading governance data integrity and usability. +- Resolution: the sanitized name is now validated before the transaction starts, with the existing validation error shape and regression coverage. + +### SEC-010 — Site-local activation could initialize unrelated new multisite sites + +- Severity: Low +- Status: Fixed in 3.2.6 +- Location: `includes/class-mstv-bootstrap.php` (`initialize_site`) +- Evidence: the `wp_initialize_site` hook initialized TeamVault tables and defaults whenever the plugin happened to be loaded, even when it was active only for one existing site rather than network-wide. +- Impact: creating another site in the network could receive TeamVault schema and options outside the plugin's intended activation scope. +- Resolution: new-site initialization now runs only when TeamVault is network-active. A regression test covers both inactive and network-active states. + +### SEC-011 — ZIP creation could continue after archive write failures or cyclic metadata + +- Severity: Low +- Status: Fixed in 3.2.6 +- Location: `includes/class-mstv-export.php` +- Evidence: return values from ZIP entry writes and finalization were not checked, and recursive traversal relied entirely on the database parent invariant. +- Impact: storage or ZIP errors could produce an incomplete download reported as successful; manually corrupted cyclic folder metadata could cause unbounded recursive traversal during export. +- Resolution: entry writes and finalization now fail closed, `Throwable` is handled by the existing cleanup path, and visited folder identifiers bound recursive traversal. The cycle guard has regression coverage where ZipArchive is available. + +### SEC-012 — Details panel covered toolbar actions in the WordPress desktop layout + +- Severity: Low +- Status: Fixed in 3.2.6 +- Location: `assets/css/admin.css`, `assets/js/admin-app-core.js` +- Evidence: at a 1280px browser viewport, WordPress's 160px desktop admin menu reduced the plugin canvas below the intended 1200px breakpoint. The inline details panel covered the Upload action and intercepted pointer input. +- Impact: administrators on common laptop-sized desktop viewports could see Export but could not activate Upload with a pointer, despite the control remaining in the DOM. +- Resolution: the details drawer threshold now includes the WordPress desktop admin-menu width, preserving the original 1200px app-content target. Static regression coverage and an isolated browser smoke test verify that the action remains visible and clickable. + ## Residual Risks and Recommendations ### SR-001 — Audit-log retention is not automated diff --git a/includes/class-mstv-bootstrap.php b/includes/class-mstv-bootstrap.php index fdb4f41..ab5e023 100644 --- a/includes/class-mstv-bootstrap.php +++ b/includes/class-mstv-bootstrap.php @@ -166,13 +166,23 @@ public function log_export_completed($folderId, $zipPath, $filesCount): void public function initialize_site(\WP_Site $newSite): void { - if (empty($newSite->blog_id)) { + if (empty($newSite->blog_id) || !$this->is_network_active()) { return; } MSTV_Activator::initialize_site((int) $newSite->blog_id); } + private function is_network_active(): bool + { + if (!function_exists('is_plugin_active_for_network')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + return function_exists('is_plugin_active_for_network') + && is_plugin_active_for_network(MSTV_PLUGIN_BASENAME); + } + /** * Wire the REST API controllers with all required dependencies. * diff --git a/includes/class-mstv-export.php b/includes/class-mstv-export.php index e0a7320..290e517 100644 --- a/includes/class-mstv-export.php +++ b/includes/class-mstv-export.php @@ -14,6 +14,7 @@ class MSTV_Export private string $currentZipPath = ''; private int $currentFileCount = 0; private array $reservedArchivePaths = []; + private array $visitedFolderIds = []; public function __construct( MSTV_Storage $storage, @@ -68,6 +69,7 @@ public function export_selection(array $folderIds, ?string $zipName = null): voi $this->currentZipPath = $zipPath; $this->currentFileCount = 0; $this->reservedArchivePaths = []; + $this->visitedFolderIds = []; register_shutdown_function([$this, 'cleanup_zip']); if (class_exists('MSTV_Hooks')) { @@ -91,12 +93,16 @@ public function export_selection(array $folderIds, ?string $zipName = null): voi foreach ($selectedFolders as $folder) { $folderPath = $this->build_unique_zip_path($folder->name, $usedPaths); - $zip->addEmptyDir($folderPath); + if (!$zip->addEmptyDir($folderPath)) { + throw new \RuntimeException('Unable to add folder to export'); + } $this->add_folder_to_zip($zip, (int) $folder->id, $folderPath); } - $zip->close(); - } catch (\Exception $e) { + if (!$zip->close()) { + throw new \RuntimeException('Unable to finalize export'); + } + } catch (\Throwable $e) { $zip->close(); wp_delete_file($zipPath); wp_die( @@ -158,6 +164,7 @@ public function export_folder(?int $folderId, ?string $zipName = null): void $this->currentZipPath = $zipPath; $this->currentFileCount = 0; $this->reservedArchivePaths = []; + $this->visitedFolderIds = []; register_shutdown_function([$this, 'cleanup_zip']); if (class_exists('MSTV_Hooks')) { @@ -178,8 +185,10 @@ public function export_folder(?int $folderId, ?string $zipName = null): void try { $this->add_folder_to_zip($zip, $folderId, ''); - $zip->close(); - } catch (\Exception $e) { + if (!$zip->close()) { + throw new \RuntimeException('Unable to finalize export'); + } + } catch (\Throwable $e) { $zip->close(); wp_delete_file($zipPath); wp_die( @@ -205,6 +214,14 @@ public function cleanup_zip(): void private function add_folder_to_zip(ZipArchive $zip, ?int $folderId, string $basePath): void { + if ($folderId !== null) { + if (isset($this->visitedFolderIds[$folderId])) { + return; + } + + $this->visitedFolderIds[$folderId] = true; + } + $canDownloadCurrent = !$this->permissions || $this->permissions->current_user_can( $folderId !== null && $folderId > 0 ? $folderId : null, MSTV_Permissions::ACTION_DOWNLOAD @@ -212,18 +229,26 @@ private function add_folder_to_zip(ZipArchive $zip, ?int $folderId, string $base $folders = $this->folderRepo->find_by_parent($folderId); foreach ($folders as $folder) { + $childFolderId = (int) $folder->id; + + if (isset($this->visitedFolderIds[$childFolderId])) { + continue; + } + $canDownloadFolder = !$this->permissions || $this->permissions->current_user_can( - (int) $folder->id, + $childFolderId, MSTV_Permissions::ACTION_DOWNLOAD ); $folderPath = $basePath; if ($canDownloadFolder) { $folderPath = $this->build_unique_folder_archive_path($basePath, $folder->name); - $zip->addEmptyDir($folderPath); + if (!$zip->addEmptyDir($folderPath)) { + throw new \RuntimeException('Unable to add folder to export'); + } } - $this->add_folder_to_zip($zip, $folder->id, $folderPath); + $this->add_folder_to_zip($zip, $childFolderId, $folderPath); } if (!$canDownloadCurrent) { @@ -241,7 +266,9 @@ private function add_folder_to_zip(ZipArchive $zip, ?int $folderId, string $base if ($filesPath !== false && file_exists($filesPath) && is_readable($filesPath)) { $zipPath = $this->build_unique_file_archive_path($basePath, $files); - $zip->addFile($filesPath, $zipPath); + if (!$zip->addFile($filesPath, $zipPath)) { + throw new \RuntimeException('Unable to add file to export'); + } $this->currentFileCount++; } } diff --git a/includes/class-mstv-rest-governance.php b/includes/class-mstv-rest-governance.php index a46a094..9019bf1 100644 --- a/includes/class-mstv-rest-governance.php +++ b/includes/class-mstv-rest-governance.php @@ -195,8 +195,14 @@ public function update_group(\WP_REST_Request $request): \WP_REST_Response|\WP_E $name = $request->get_param('name'); if (is_string($name) && $name !== '') { - $data['name'] = sanitize_text_field($name); - $data['slug'] = $this->unique_slug(sanitize_title($name), $id); + $sanitizedName = sanitize_text_field($name); + + if ($sanitizedName === '') { + return new \WP_Error('validation_error', __('The group name cannot be empty.', 'mikesoft-teamvault'), ['status' => 400]); + } + + $data['name'] = $sanitizedName; + $data['slug'] = $this->unique_slug(sanitize_title($sanitizedName), $id); } $description = $request->get_param('description'); @@ -305,7 +311,7 @@ public function get_notifications(\WP_REST_Request $request): \WP_REST_Response public function update_notifications(\WP_REST_Request $request): \WP_REST_Response { - update_option('mstv_notify_enabled', (bool) $request->get_param('enabled')); + update_option('mstv_notify_enabled', wp_validate_boolean($request->get_param('enabled'))); $allowedEvents = ['upload', 'download', 'delete', 'access_denied']; $events = array_values(array_intersect($allowedEvents, (array) $request->get_param('events'))); @@ -315,7 +321,7 @@ public function update_notifications(\WP_REST_Request $request): \WP_REST_Respon $recipients = is_array($recipients) ? $recipients : []; update_option('mstv_notify_recipients', [ - 'admins' => !empty($recipients['admins']), + 'admins' => wp_validate_boolean($recipients['admins'] ?? false), 'users' => array_values(array_unique(array_filter(array_map('absint', (array) ($recipients['users'] ?? []))))), 'groups' => array_values(array_unique(array_filter(array_map('absint', (array) ($recipients['groups'] ?? []))))), ]); @@ -421,7 +427,7 @@ public function get_quotas(\WP_REST_Request $request): \WP_REST_Response public function update_quotas(\WP_REST_Request $request): \WP_REST_Response { - update_option('mstv_quotas_enabled', (bool) $request->get_param('enabled')); + update_option('mstv_quotas_enabled', wp_validate_boolean($request->get_param('enabled'))); $rawItems = $request->get_param('items'); $map = []; diff --git a/includes/class-mstv-storage.php b/includes/class-mstv-storage.php index d6dd06d..7cbe056 100644 --- a/includes/class-mstv-storage.php +++ b/includes/class-mstv-storage.php @@ -163,9 +163,9 @@ public function store_uploaded_file( $folderPath = $this->get_folder_path($folderId, $folderRepo); $relativePath = $this->build_file_path($folderPath, $storedName); - $fullPath = $this->filesystem->resolve($relativePath); + $fullPath = $this->filesystem->get_verified_path($relativePath, true); - if (!$this->filesystem->is_path_within_base($fullPath)) { + if ($fullPath === false) { return [ 'success' => false, 'error' => __('Invalid destination path.', 'mikesoft-teamvault'), diff --git a/mikesoft-teamvault.php b/mikesoft-teamvault.php index 0443ac0..1f28629 100644 --- a/mikesoft-teamvault.php +++ b/mikesoft-teamvault.php @@ -3,7 +3,7 @@ * Plugin Name: Mikesoft TeamVault * Plugin URI: https://github.com/TheStreamCode/mikesoft-teamvault * Description: Private shared document management separated from the WordPress Media Library with secure access control, preview and drag-and-drop. - * Version: 3.2.5 + * Version: 3.2.6 * Author: Mikesoft * Author URI: https://mikesoft.it * License: GPL v2 or later @@ -16,7 +16,7 @@ defined('ABSPATH') || exit; -define('MSTV_VERSION', '3.2.5'); +define('MSTV_VERSION', '3.2.6'); define('MSTV_PLUGIN_FILE', __FILE__); define('MSTV_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('MSTV_PLUGIN_URL', plugin_dir_url(__FILE__)); diff --git a/readme.txt b/readme.txt index fc0d26d..62e0f51 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: documents, secure, collaboration, privacy, file-manager Requires at least: 6.0 Tested up to: 7.0 Requires PHP: 8.0 -Stable tag: 3.2.5 +Stable tag: 3.2.6 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -159,6 +159,13 @@ For direct contact, email teamvault@mikesoft.it. == Changelog == += 3.2.6 = +* Security: upload destinations now use the complete symlink-aware storage verifier before any file is written. +* Reliability: ZIP exports fail closed when an archive entry or finalization fails and stop safely when corrupt folder metadata contains a cycle. +* Fixed: REST boolean strings are normalized correctly, sanitized-empty group names are rejected, and new multisite sites are initialized only for network-wide activation. +* Fixed: the details panel switches to a drawer before the WordPress admin menu can make it cover toolbar actions on common desktop viewports. +* Improved: repository presentation, release metadata, dependency automation, and security-reporting guidance were refreshed. + = 3.2.5 = * Security: the file details panel now escapes the stored file extension before rendering it, completing the output-encoding pass started in 3.2.4. @@ -293,6 +300,10 @@ For the full release history, see `changelog.txt` in the plugin package. == Upgrade Notice == += 3.2.6 = + +Hardens upload containment and ZIP reliability, corrects governance and multisite edge cases, and keeps toolbar actions accessible on common desktop viewports. No configuration changes are required. + = 3.2.5 = Defense-in-depth output encoding for the file details panel. No configuration changes are required. diff --git a/tests/PDMActivatorTest.php b/tests/PDMActivatorTest.php index 847b992..788e233 100644 --- a/tests/PDMActivatorTest.php +++ b/tests/PDMActivatorTest.php @@ -11,6 +11,7 @@ protected function setUp(): void parent::setUp(); $GLOBALS['pdm_test_roles'] = []; + $GLOBALS['pdm_test_network_active_plugins'] = []; } public function test_register_capabilities_grants_document_access_only_to_administrators_by_default(): void @@ -80,4 +81,16 @@ public function update($table, $data, $where): int ], ], $wpdb->updates); } + + public function test_new_site_initialization_requires_network_activation(): void + { + $bootstrap = MSTV_Bootstrap::instance(); + $method = new ReflectionMethod(MSTV_Bootstrap::class, 'is_network_active'); + + self::assertFalse($method->invoke($bootstrap)); + + $GLOBALS['pdm_test_network_active_plugins'][MSTV_PLUGIN_BASENAME] = true; + + self::assertTrue($method->invoke($bootstrap)); + } } diff --git a/tests/PDMAdminAppTest.php b/tests/PDMAdminAppTest.php index 1448643..0244863 100644 --- a/tests/PDMAdminAppTest.php +++ b/tests/PDMAdminAppTest.php @@ -113,9 +113,9 @@ public function testResponsiveViewportHelpersUseSeparateSidebarAndDetailsBreakpo 'The details drawer must use its own wider breakpoint helper.' ); self::assertStringContainsString( - 'return window.innerWidth <= 1200;', + 'return window.innerWidth <= 1360;', $source, - 'The details drawer must open at 1200px and below.' + 'The details drawer must account for the WordPress desktop admin menu.' ); self::assertStringNotContainsString( 'isMobileViewport()', @@ -124,7 +124,7 @@ public function testResponsiveViewportHelpersUseSeparateSidebarAndDetailsBreakpo ); } - public function testDetailsDrawerCssStartsAtTwelveHundredPixelsAndRespectsAdminBarOffsets(): void + public function testDetailsDrawerCssAccountsForTheAdminMenuAndRespectsAdminBarOffsets(): void { $source = (string) file_get_contents(dirname(__DIR__) . '/assets/css/admin.css'); @@ -139,9 +139,9 @@ public function testDetailsDrawerCssStartsAtTwelveHundredPixelsAndRespectsAdminB 'Fixed admin drawers must use the taller mobile WordPress admin bar offset.' ); self::assertStringContainsString( - '@media (max-width: 1200px)', + '@media (max-width: 1360px)', $source, - 'The details panel responsive threshold must remain at 1200px.' + 'The details panel threshold must preserve 1200px of app space beside the WordPress admin menu.' ); self::assertStringContainsString( 'top: var(--pdm-admin-bar-offset);', @@ -156,12 +156,12 @@ public function testDetailsDrawerCssStartsAtTwelveHundredPixelsAndRespectsAdminB self::assertStringContainsString( 'transform: translateX(100%);', $source, - 'The details panel must move off-canvas instead of shrinking at 1200px.' + 'The details panel must move off-canvas instead of covering toolbar actions.' ); self::assertStringNotContainsString( 'width: 280px;', $source, - 'The 1200px details behavior must not leave a cramped inline details panel.' + 'The responsive details behavior must not leave a cramped inline details panel.' ); } } diff --git a/tests/PDMExportTest.php b/tests/PDMExportTest.php index 0f8f8df..7c4cd26 100644 --- a/tests/PDMExportTest.php +++ b/tests/PDMExportTest.php @@ -85,6 +85,48 @@ public function test_restricted_folder_names_are_omitted_while_shared_descendant self::assertContains('Shared/', $entries); } + public function test_corrupt_folder_cycles_do_not_recurse_indefinitely(): void + { + if (!class_exists('ZipArchive')) { + self::markTestSkipped('ZipArchive is not available in this environment.'); + } + + $folderRepo = $this->getMockBuilder(MSTV_Repository_Folders::class)->disableOriginalConstructor()->getMock(); + $folderRepo->expects(self::exactly(2)) + ->method('find_by_parent') + ->willReturnCallback(static function ($parentId): array { + if ((int) $parentId === 10) { + return [(object) ['id' => 20, 'name' => 'Child']]; + } + + return [(object) ['id' => 10, 'name' => 'Parent']]; + }); + + $filesRepo = $this->getMockBuilder(MSTV_Repository_Files::class)->disableOriginalConstructor()->getMock(); + $filesRepo->method('find_by_folder')->willReturn([]); + + $export = new MSTV_Export( + $this->getMockBuilder(MSTV_Storage::class)->disableOriginalConstructor()->getMock(), + $filesRepo, + $folderRepo, + $this->createMock(MSTV_Auth::class) + ); + $path = wp_tempnam('teamvault-export-cycle-test.zip'); + $zip = new ZipArchive(); + self::assertTrue($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE)); + + $method = new ReflectionMethod(MSTV_Export::class, 'add_folder_to_zip'); + $method->setAccessible(true); + $method->invoke($export, $zip, 10, 'Parent/'); + self::assertTrue($zip->close()); + + self::assertTrue($zip->open($path)); + self::assertNotFalse($zip->locateName('Parent/Child/')); + self::assertFalse($zip->locateName('Parent/Child/Parent/')); + $zip->close(); + wp_delete_file($path); + } + private function buildExport(): MSTV_Export { return new MSTV_Export( diff --git a/tests/PDMGovernanceAtomicityTest.php b/tests/PDMGovernanceAtomicityTest.php index 2c18930..adff1f5 100644 --- a/tests/PDMGovernanceAtomicityTest.php +++ b/tests/PDMGovernanceAtomicityTest.php @@ -6,6 +6,11 @@ final class PDMGovernanceAtomicityTest extends TestCase { + protected function setUp(): void + { + $GLOBALS['pdm_test_options'] = []; + } + public function test_group_update_rolls_back_metadata_when_member_replacement_fails(): void { $wpdb = new class { @@ -50,4 +55,67 @@ public function query(string $query): int self::assertContains('ROLLBACK', $wpdb->queries); self::assertNotContains('COMMIT', $wpdb->queries); } + + public function test_group_update_rejects_a_name_that_is_empty_after_sanitization(): void + { + $groups = $this->getMockBuilder(MSTV_Repository_Groups::class) + ->disableOriginalConstructor() + ->getMock(); + $groups->method('find')->willReturn((object) ['id' => 5]); + $groups->expects(self::never())->method('update'); + + $result = $this->buildController($groups)->update_group(new WP_REST_Request([ + 'id' => 5, + 'name' => '', + ])); + + self::assertInstanceOf(WP_Error::class, $result); + self::assertSame('validation_error', $result->get_error_code()); + } + + public function test_notification_boolean_strings_are_normalized_before_storage(): void + { + $controller = $this->buildController(); + + $controller->update_notifications(new WP_REST_Request([ + 'enabled' => 'false', + 'events' => [], + 'recipients' => ['admins' => 'false'], + ])); + + self::assertFalse(get_option('mstv_notify_enabled')); + self::assertFalse(get_option('mstv_notify_recipients')['admins']); + } + + public function test_quota_boolean_strings_are_normalized_before_storage(): void + { + $groups = $this->getMockBuilder(MSTV_Repository_Groups::class)->disableOriginalConstructor()->getMock(); + $quota = new MSTV_Quota( + new MSTV_Settings(), + $this->getMockBuilder(MSTV_Repository_Files::class)->disableOriginalConstructor()->getMock(), + $groups + ); + $controller = $this->buildController($groups, $quota); + + $controller->update_quotas(new WP_REST_Request([ + 'enabled' => 'false', + 'items' => [], + ])); + + self::assertFalse(get_option('mstv_quotas_enabled')); + } + + private function buildController(?MSTV_Repository_Groups $groups = null, ?MSTV_Quota $quota = null): MSTV_REST_Governance_Controller + { + $groups ??= $this->getMockBuilder(MSTV_Repository_Groups::class)->disableOriginalConstructor()->getMock(); + + return new MSTV_REST_Governance_Controller( + $this->createMock(MSTV_Auth::class), + $groups, + $this->getMockBuilder(MSTV_Repository_Permissions::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(MSTV_Repository_Folders::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(MSTV_Permissions::class)->disableOriginalConstructor()->getMock(), + $quota + ); + } } diff --git a/tests/PDMReleaseMetadataTest.php b/tests/PDMReleaseMetadataTest.php index b94c6f4..b3bc39f 100644 --- a/tests/PDMReleaseMetadataTest.php +++ b/tests/PDMReleaseMetadataTest.php @@ -6,8 +6,8 @@ final class PDMReleaseMetadataTest extends TestCase { - private const RELEASE_VERSION = '3.2.5'; - private const RELEASE_DATE = '2026-08-02'; + private const RELEASE_VERSION = '3.2.6'; + private const RELEASE_DATE = '2026-08-08'; private const TESTED_UP_TO = '7.0'; private const CONTACT_EMAIL = 'teamvault@mikesoft.it'; private const PLUGIN_AUTHOR = 'Mikesoft'; @@ -59,4 +59,27 @@ public function test_public_contact_metadata_uses_teamvault_mailbox(): void self::assertStringContainsString(self::CONTACT_EMAIL, $githubReadme); self::assertStringContainsString(self::CONTACT_EMAIL, $securityPolicy); } + + public function test_repository_presentation_metadata_is_release_ready(): void + { + $root = dirname(__DIR__); + $citation = (string) file_get_contents($root . '/CITATION.cff'); + $dependabot = (string) file_get_contents($root . '/.github/dependabot.yml'); + $securityPolicy = (string) file_get_contents($root . '/SECURITY.md'); + $socialPreview = $root . '/.github/social-preview.png'; + + self::assertStringContainsString('version: "' . self::RELEASE_VERSION . '"', $citation); + self::assertStringContainsString('date-released: "' . self::RELEASE_DATE . '"', $citation); + self::assertStringContainsString('license: GPL-2.0-or-later', $citation); + self::assertStringContainsString('package-ecosystem: composer', $dependabot); + self::assertStringContainsString('package-ecosystem: github-actions', $dependabot); + self::assertStringContainsString('/security/advisories/new', $securityPolicy); + self::assertFileExists($socialPreview); + + $dimensions = getimagesize($socialPreview); + + self::assertIsArray($dimensions); + self::assertSame(1280, $dimensions[0]); + self::assertSame(640, $dimensions[1]); + } } diff --git a/tests/PDMStorageTest.php b/tests/PDMStorageTest.php index 84df115..59d2d80 100644 --- a/tests/PDMStorageTest.php +++ b/tests/PDMStorageTest.php @@ -169,6 +169,34 @@ public function test_reindex_reports_metadata_insert_failure(): void self::assertSame(0, $result['files_created']); } + public function test_upload_rejects_a_destination_that_fails_verified_path_checks(): void + { + $filesystem = $this->createMock(MSTV_Filesystem::class); + $filesystem->method('get_base_path')->willReturn($this->storagePath); + $filesystem->method('is_writable')->willReturn(true); + $filesystem->expects(self::once()) + ->method('get_verified_path') + ->with(self::callback(static fn (string $path): bool => str_starts_with($path, 'linked/')), true) + ->willReturn(false); + $filesystem->expects(self::never())->method('resolve'); + + $folderRepo = $this->getMockBuilder(MSTV_Repository_Folders::class)->disableOriginalConstructor()->getMock(); + $folderRepo->method('find')->with(9)->willReturn((object) ['relative_path' => 'linked']); + + $storage = new MSTV_Storage(new MSTV_Settings()); + $filesystemProperty = new ReflectionProperty(MSTV_Storage::class, 'filesystem'); + $filesystemProperty->setAccessible(true); + $filesystemProperty->setValue($storage, $filesystem); + + $result = $storage->store_uploaded_file([ + 'name' => 'contract.pdf', + 'tmp_name' => $this->storagePath . '/contract.pdf', + ], 9, $folderRepo); + + self::assertFalse($result['success']); + self::assertSame('Invalid destination path.', $result['error']); + } + private function deleteDirectory(string $path): void { if (!is_dir($path)) { diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b8c2011..013d7f3 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,6 +19,7 @@ $GLOBALS['pdm_test_user_meta'] = []; $GLOBALS['pdm_test_transients'] = []; $GLOBALS['pdm_test_roles'] = []; +$GLOBALS['pdm_test_network_active_plugins'] = []; function __($text, $domain = null) { @@ -123,6 +124,20 @@ function is_multisite() return false; } +function is_plugin_active_for_network($plugin) +{ + return !empty($GLOBALS['pdm_test_network_active_plugins'][(string) $plugin]); +} + +function wp_validate_boolean($value) +{ + if (is_string($value) && strtolower($value) === 'false') { + $value = false; + } + + return (bool) $value; +} + function get_sites(array $args = []) { return []; @@ -461,6 +476,16 @@ public function get_headers(): array } } +class WP_Site +{ + public int $blog_id; + + public function __construct(int $blogId) + { + $this->blog_id = $blogId; + } +} + class FakePDMUser { public int $ID; @@ -547,3 +572,9 @@ public function capabilities(): array require_once __DIR__ . '/../includes/class-mstv-activator.php'; require_once __DIR__ . '/../includes/class-mstv-admin.php'; require_once __DIR__ . '/../includes/class-mstv-assets.php'; + +if (!defined('MSTV_PLUGIN_BASENAME')) { + define('MSTV_PLUGIN_BASENAME', 'mikesoft-teamvault/mikesoft-teamvault.php'); +} + +require_once __DIR__ . '/../includes/class-mstv-bootstrap.php';