diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..af3ab327 --- /dev/null +++ b/.env.example @@ -0,0 +1,102 @@ +# Gyro-PHP Environment Configuration +# Copy this file to .env and adjust values for your environment. +# All values are optional — defaults from constants.inc.php apply if not set. +# Only APP_* keys are auto-defined as PHP constants. + +# ============================================================================= +# Application +# ============================================================================= +#APP_TITLE="My Application" +#APP_LANG=en +#APP_CHARSET=UTF-8 +#APP_TESTMODE=false +#APP_ITEMS_PER_PAGE=10 + +# ============================================================================= +# Database +# ============================================================================= +#APP_DB_TYPE=mysql +#APP_DB_HOST=127.0.0.1 +#APP_DB_NAME=mydb +#APP_DB_USER=root +#APP_DB_PASSWORD= +#APP_DB_SLOW_QUERY_THRESHOLD=0.0100 + +# ============================================================================= +# URLs & Domain +# ============================================================================= +#APP_URL_DOMAIN=www.example.com +#APP_URL_BASEDIR=/ +#APP_DEFAULT_SCHEME=http +#APP_ENABLE_HTTPS=true +#APP_FORCE_FULL_DOMAINNAME=true +#APP_VALIDATE_URL=true +#APP_UNICODE_URLS=false + +# ============================================================================= +# Session +# ============================================================================= +#APP_START_SESSION=true +#APP_SESSION_HANDLER=DBSession + +# ============================================================================= +# Mail +# ============================================================================= +#APP_MAIL_SENDER=noreply@example.com +#APP_MAIL_ADMIN=admin@example.com +#APP_MAIL_SUPPORT=support@example.com +#APP_MAIL_SUBJECT="[My App]" +#APP_MAIL_RETURN_PATH= +#APP_MAILER_TYPE=mail +#APP_MAILER_SMTP_HOST= +#APP_MAILER_SMTP_USER= +#APP_MAILER_SMTP_PASSWORD= + +# ============================================================================= +# Directories +# ============================================================================= +#APP_TEMP_DIR=/tmp/myapp/ +#APP_OUT_DIR=/tmp/myapp/ +#APP_LOG_DIR=/tmp/myapp/log/ +#APP_3RDPARTY_DIR= + +# ============================================================================= +# Logging +# ============================================================================= +#APP_LOG_QUERIES=false +#APP_LOG_FAILED_QUERIES=true +#APP_LOG_SLOW_QUERIES=false +#APP_LOG_TRANSLATIONS=false +#APP_LOG_HTML_ERROR_STATUS=false +#APP_LOG_HTTPREQUESTS=false +#APP_LOG_FILE_NAME_PATTERN="%date%_%name%.log" + +# ============================================================================= +# Debugging +# ============================================================================= +#APP_THROW_ON_DB_ERROR=true +#APP_THROW_ON_WARNING=false +#APP_DEBUG_QUERIES=false +#APP_PRINT_DURATION=false +#APP_DISABLE_CACHE=false +#APP_DISABLE_ERROR_CACHE=false + +# ============================================================================= +# Templates & View +# ============================================================================= +#APP_DEFAULT_TEMPLATE_ENGINE=core +#APP_PAGE_TEMPLATE="core::page" + +# ============================================================================= +# Cache Headers +# ============================================================================= +#APP_CACHEHEADER_CLASS_CACHED=PrivateRigid +#APP_CACHEHEADER_CLASS_UNCACHED=NoCache +#APP_PRELOAD_CSS=false + +# ============================================================================= +# Form Validation +# ============================================================================= +#APP_FORMVALIDATION_FIELD_NAME=jfioeudkswefs +#APP_FORMVALIDATION_HANDLER_NAME=uerwudjmdjwu +#APP_FORMVALIDATION_EXPIRATION_TIME=15 diff --git a/.gitignore b/.gitignore index 485dee64..3b8451ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .idea +/vendor/ +.phpunit.result.cache +.env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5595b0a4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,162 @@ +# Changelog + +Alle wesentlichen Änderungen am Gyro-PHP Framework, chronologisch nach Phasen geordnet. + +## [Phase 8] – 2026-03-05 + +### Hinzugefügt +- **CLI-Tool (`bin/gyro`):** Neues Kommandozeilen-Werkzeug für Gyro-PHP: + - `gyro help` — Verfügbare Kommandos anzeigen + - `gyro model:list` — Alle DAO-Modelle mit Tabellennamen, Feldern und Primary Keys auflisten + - `gyro model:show ` — Detailliertes Schema eines Modells anzeigen (Felder, Typen, Defaults, Relations, CREATE TABLE SQL) + - `gyro db:sync` — Model-Schema mit der Datenbank vergleichen und ALTER TABLE SQL generieren +- **CLI-Kernel** (`gyro/core/cli/clikernel.cls.php`): Command-Routing, Argument-Parsing, farbige Ausgabe +- **CLICommand** Basisklasse für eigene Kommandos +- **CLITable** ASCII-Tabellenrenderer für formatierte CLI-Ausgabe +- **CLI-Bootstrap** (`gyro/core/cli/bootstrap.cli.php`): Framework-Initialisierung ohne HTTP-Kontext +- **Model-Discovery:** Automatische Erkennung aller DAO-Klassen in Core, Modules und Contributions +- **Schema-Introspection:** Liest `create_table_object()` und generiert CREATE TABLE / ALTER TABLE SQL +- **33 neue Tests** für CLI-Komponenten (CLITable, CLIKernel, ModelShowCommand) + +### Ergebnis +- 287 Tests, 1066 Assertions (alle grün, 0 Deprecations) + +--- + +## [Phase 7] – 2026-03-05 + +### Hinzugefügt +- **`.env` Konfiguration:** Neuer `Env`-Loader (`gyro/core/lib/helpers/env.cls.php`) ermöglicht + Environment-Konfiguration über `.env`-Dateien. Alle `APP_*` Variablen aus der `.env`-Datei + werden automatisch als PHP-Konstanten definiert — vollständig rückwärtskompatibel. +- **`.env.example`:** Referenzdatei mit allen verfügbaren Konfigurationsvariablen. +- **11 neue Tests** für den Env-Loader (`tests/core/EnvTest.php`). +- **PHPStan Baseline** (`phpstan-baseline.neon`): 1262 bekannte Fehler getracked, + neue Fehler werden sofort gemeldet. + +### Geändert +- **PHPStan Level 1 → 2:** Strengere statische Analyse mit Baseline-Strategie. +- **Composer Classmap entfernt:** Die `autoload.classmap` Konfiguration wurde entfernt, + da sie einen Pfadkonflikt mit dem Framework-eigenen `Load::directories()` verursachte + (`include_once` erkannte die gleiche Datei unter verschiedenen Pfaden nicht als identisch). +- **`start.php`:** Lädt jetzt `.env` vor `constants.inc.php` (nur wenn `APP_INCLUDE_ABSPATH` definiert ist). +- **`.gitignore`:** `.env` hinzugefügt. + +### Behoben +- **PHP 8.4 Deprecation Warnings:** 3 dynamische Properties gefixt: + - `DAOStudentsTest::$modificationdate` als explizite Property deklariert + - `Url::$url` als explizite Property deklariert (verwendet in `__sleep`/`__wakeup`) + +### Ergebnis +- 254 Tests, 985 Assertions (alle grün, 0 Deprecations) +- PHPStan Level 2: keine neuen Fehler + +--- + +## [Phase 6] – 2026-03-05 + +### Hinzugefügt +- **Typed Properties** in 12 Interface-Implementierungen (16 Properties total): + - `DBResultSet`, `DBResultSetMysql`, `DBResultSetSphinx`, `DBResultSetCountSphinx` + - `CacheDBImpl`, `CacheFileImpl`, `FileCacheItem`, `ACPuCacheItem`, `MemcacheCacheItem` + - `ConverterChain`, `ConverterHtmlTidy`, `ConverterUnidecode` +- **`DB::execute_prepared()`** und **`DB::query_prepared()`** — statische Wrapper für + Prepared Statements auf der DB-Klasse. Vereinfacht die Nutzung gegenüber dem direkten + Driver-Zugriff. +- **PHPStan Level 1** eingerichtet (`phpstan.neon.dist`). + +--- + +## [Phase 5] – 2026-03-05 + +### Entfernt +- **`cache.xcache`** — XCache ist seit PHP 7.0 nicht mehr verfügbar (8 Dateien). +- **`javascript.cleditor`** — CLEditor ist seit Jahren abandoned (~36 Dateien). +- **`javascript.wymeditor`** — WYMeditor ist seit Jahren abandoned (~79 Dateien). + +### Hinzugefügt +- Weitere SimpleTest → PHPUnit Migrationen. +- PHPDoc für ausgewählte public APIs. + +--- + +## [Phase 4] – 2026-03-05 + +### Hinzugefügt +- **Type Declarations** in 5 Core-Interfaces und allen Implementierungen: + - `IDBResultSet` (3 Impl.), `ISessionHandler` (4 Impl.), `ICachePersister` (5 Impl.), + `IConverter` (12+ Impl.), `IHashAlgorithm` (6 Impl.) + - Union Types: `array|false`, `string|false`, `int|false`, `ICacheItem|false`, `mixed` +- **Structured Logging** (PSR-3 kompatibel) in `Logger`: + - Neue Methoden: `Logger::emergency()`, `::alert()`, `::critical()`, `::error()`, + `::warning()`, `::notice()`, `::info()`, `::debug()` + - Context-Interpolation: `Logger::error('User {user} failed', ['user' => $name])` + - JSON-Ausgabe pro Level (z.B. `error-2026-03-05.log`) + - Exception-Support mit automatischem Stack-Trace + - Konfigurierbares Minimum-Level: `Logger::set_min_level(Logger::WARNING)` + +### Nicht geändert +- `IDBDriver` Type Declarations zurückgestellt (Sphinx-Driver hat fehlende Methoden). +- Namespace-Migration (PSR-4) zurückgestellt (zu großer Breaking Change). + +--- + +## [Phase 3] – 2026-03-05 + +### Verbessert +- **Session-Security:** + - `session.cookie_secure = 1` wird bei HTTPS automatisch gesetzt + - `session.cookie_httponly = true` fest konfiguriert + - `session.cookie_samesite = Lax` konfiguriert + - Veralteter PHP < 7.3 `setcookie()` Fallback entfernt +- **CSRF-Token Validierung:** Strikter Vergleich `===` statt `==` in + `FormHandler::validate()`. + +### Geprüft (keine Änderung nötig) +- CSRF-Token-System: Bereits robust (random_bytes, Session-gebunden, DB-gestützt, Einmal-Tokens). +- Input-Handling: Core nutzt `PageData`/`TracedArray`, kein direkter `$_POST`/`$_GET` Zugriff. + +--- + +## [Phase 2] – 2026-03-05 + +### Hinzugefügt +- **Composer** (`composer.json`): PHPUnit 10.5 als Dev-Dependency, PHP >=8.0. +- **PHPUnit Setup:** `phpunit.xml.dist`, `tests/bootstrap.php`, Test-Verzeichnisstruktur. +- **Prepared Statements** im MySQL-Driver: + - `$driver->execute_prepared('INSERT INTO t (col) VALUES (?)', ['value'])` + - `$driver->query_prepared('SELECT * FROM t WHERE id = ?', [42])` + - Automatische Typerkennung der Parameter (`detect_param_types()`) +- **IDBDriver Interface:** Um `execute_prepared()` und `query_prepared()` erweitert. +- **SimpleTest → PHPUnit Migration** gestartet: `ArrayTest`, `StringTest`, `ValidationTest`. +- `.gitignore`: `/vendor/` hinzugefügt. + +### Nicht geändert +- Bestehende `execute()`/`query()` Methoden bleiben unverändert (Rückwärtskompatibilität). + Sie verwenden weiterhin `mysqli_real_escape_string()`. + +--- + +## [Phase 1] – 2026-03-05 + +### Behoben (PHP 8.x Kompatibilität) +- **`common.cls.php`:** `preprocess_input()` als No-op implementiert, `transcribe()` entfernt + (Magic Quotes gibt es seit PHP 7.4 nicht mehr). +- **`start.php`:** `E_ALL | E_STRICT` → `E_ALL` (E_STRICT ist seit PHP 8.0 Teil von E_ALL). + PHP 5.3 Kompatibilitäts-Check (`defined('E_DEPRECATED')`) entfernt. +- **`cast.cls.php`:** `isset($value->__toString)` → `method_exists($value, '__toString')` + (PHP 8.0 wirft bei `isset()` auf Magic Methods einen Fehler). + +### Verbessert (Sicherheit) +- **Passwort-Hashing:** Default von MD5/PHPass auf **bcrypt** umgestellt: + - `password_hash()` mit `PASSWORD_BCRYPT`, Cost-Factor 12 + - Neuer Hash-Algorithmus `bcryp` in `contributions/usermanagement/` + - Automatische Migration: Bestehende Hashes werden beim nächsten Login transparent + auf bcrypt aktualisiert +- **HTTP Security Headers:** + - `X-Content-Type-Options: nosniff` + - `X-Frame-Options: SAMEORIGIN` + - `Referrer-Policy: strict-origin-when-cross-origin` + - `Permissions-Policy` (restriktiv) + - Alle mit `override=false` — Applikationen können sie überschreiben +- **Timing-safe Vergleiche:** `hash_equals()` in MD5 und SHA1 Hash-Klassen. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..240b89d5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,405 @@ +# Gyro-PHP Framework – Projektanalyse & Memory + +> Letzte Aktualisierung: 2026-03-05 (Phase 8 abgeschlossen) + +## Projektübersicht + +- **Framework:** Gyro-PHP, eigenes PHP-Webframework (seit 2004, PHP 4 → PHP 5 Rewrite 2005) +- **Aktueller Stand:** Läuft auf PHP 8.x mit Safeguards, Code-Stil ist PHP 5.x Ära +- **Composer** für Dev-Dependencies (PHPUnit, PHPStan), kein PSR-4, kein Namespace-System +- **Test-Framework:** PHPUnit 10.5 (primär, 287 Tests) + SimpleTest 1.1.0 (Legacy, abandoned) +- **CLI-Tool:** `bin/gyro` (Phase 8) — model:list, model:show, db:sync +- **Statische Analyse:** PHPStan Level 2 mit Baseline (1262 bekannte Fehler getracked) +- **Environment:** `.env` Support (Phase 7), rückwärtskompatibel mit `APP_*` Konstanten + +## Verzeichnisstruktur + +``` +bin/ # CLI-Werkzeuge + gyro # CLI Entry Point (Phase 8) +gyro/ # Framework-Core + core/ + config.cls.php # Zentrale Config (281 Zeilen, 100+ Konstanten) + start.php # Bootstrap/Entry Point + cli/ # CLI-Kernel, Commands, Helpers (Phase 8) + controller/base/ # Basis-Controller & Routing + model/base/ # DB-Abstraktionsschicht + model/drivers/mysql/ # MySQL-Driver (nur mysqli_real_escape_string) + lib/components/ # Core-Komponenten (Logger, HTTP, etc.) + lib/helpers/ # Hilfsklassen (String, Array, Cast, etc.) + lib/interfaces/ # Interface-Definitionen + view/base/ # View-Layer + modules/ # Framework-Module + simpletest/ # Test-Framework + Tests + cache.*/ # Cache-Backends (memcache, xcache, acpu, file, mysql) + mime/, json/, mail/, etc. # Diverse Module +contributions/ # Erweiterungen/Plugins (60+ Module) + usermanagement/ # User-Verwaltung (bcrypt Default seit Phase 1) + lib.geocalc/ # Geo-Berechnungen + scheduler/, gsitemap/, etc. # Diverse Beiträge +``` + +## Statistiken + +| Metrik | Wert | +|--------|------| +| Core-Klassen | 239 (.cls.php, .model.php, .facade.php) | +| PHPUnit-Tests | 287 Tests, 1066 Assertions (65 Test-Dateien) | +| SimpleTest (Legacy) | 57 Dateien (größtenteils nach PHPUnit portiert) | +| Testabdeckung | ~50%+ (Phase 7: massive Erweiterung) | +| PHPDoc-Abdeckung | ~15-20% | +| TODO/FIXME/HACK | 14 Marker | +| Contributions | 57+ Module (3 tote entfernt in Phase 5) | +| PHPStan | Level 2, Baseline mit 1262 bekannten Fehlern | + +## Sicherheitsprobleme + +### ✅ GEFIXT: Passwort-Hashing +- Default von MD5/PHPass auf **bcrypt** umgestellt (`password_hash(PASSWORD_BCRYPT, cost 12)`) +- Neuer Hash-Algorithmus: `contributions/usermanagement/behaviour/commands/users/hashes/bcryp.hash.php` +- Timing-safe Vergleiche in MD5/SHA1 Klassen (`hash_equals()`) +- Auto-Upgrade: Alte Hashes werden beim nächsten Login automatisch migriert + +### ✅ GEFIXT: HTTP Security Headers +- X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy +- Gesetzt in `pageviewbase.cls.php` mit `override=false` + +### ✅ GEFIXT: Prepared Statements +- **Driver:** `execute_prepared()` und `query_prepared()` in `dbdriver.mysql.php` (Phase 2) +- **DB-Klasse:** `DB::execute_prepared()` und `DB::query_prepared()` Wrapper (Phase 6) +- Legacy `execute()`/`query()` nutzen weiterhin `mysqli_real_escape_string()` (Rückwärtskompatibilität) +- **Nächster Schritt:** Schrittweise Migration bestehender Queries auf Prepared Statements + +### ✅ GEFIXT: Session-Konfiguration +- `httponly`, `secure` (bei HTTPS), `samesite=Lax` auf Session-Cookies konfiguriert + +## ✅ PHP 8.x Kompatibilität (GEFIXT) + +- `common.cls.php`: `preprocess_input()` → No-op (Magic Quotes seit PHP 7.4 weg) +- `start.php`: `E_ALL | E_STRICT` → `E_ALL`, PHP 5.3 Compat-Check entfernt +- `cast.cls.php`: `isset($value->__toString)` → `method_exists($value, '__toString')` +- `mb_*` Funktionen: NULL-Parameter teilweise gefixt (bereits vor Phase 1) + +## Architektur-Schwächen + +### Typ-System (Phase 4 + Phase 6) +- Interfaces mit Type Declarations versehen (Phase 4) +- Typed Properties in Interface-Implementierungen (Phase 6) +- Kein Einsatz von Enums, Attributes, Match, Readonly etc. + +### Kein Namespace-System +- Alle Klassen im globalen Namespace +- Namenskonventionen statt Namespaces: `DAO*`, `*Controller`, `*Facade` +- Eigenes Autoloading statt PSR-4 + +### ✅ Logger modernisiert (Phase 4) +- **Datei:** `gyro/core/lib/components/logger.cls.php` +- PSR-3 kompatible Log-Levels (emergency → debug) +- Context-Interpolation (`{placeholder}` Syntax) +- JSON-Ausgabe für strukturierte Logs, CSV für Legacy `log()` +- Exception-Support mit Stack-Traces +- Konfigurierbares Minimum-Level via `Logger::set_min_level()` + +### ✅ Environment-Konfiguration (Phase 7) +- **Datei:** `gyro/core/lib/helpers/env.cls.php` +- `.env` Datei-Loader mit automatischer `APP_*` Konstanten-Definition +- Rückwärtskompatibel: Ohne `.env` funktioniert alles wie bisher +- Integration in `start.php`: Lädt `.env` vor `constants.inc.php` +- `.env.example` mit allen verfügbaren Konfigurationsvariablen +- Type-Casting: `true`/`false` → bool, Zahlen → int/float +- Keine externe Dependency (kein vlucas/phpdotenv nötig) + +### Konfigurations-Schwächen (teilweise behoben) +- ✅ `.env` Support für Environment-abhängige Konfiguration (Phase 7) +- Hardcoded Timeouts: `$timeout_sec = 30` (HTTP), `$max_age = 600` (Cache) +- Magic Numbers: Port 443 für HTTPS, ASCII-Codes `10`/`13`, Email-Limit `64` +- String-basierte Konstanten-Lookup (flexibel aber nicht typsicher) + +## Veraltete/Tote Module + +### ✅ Entfernt in Phase 5 +- `cache.xcache` – XCache seit PHP 7 tot (8 Dateien) +- `javascript.cleditor` – CLEditor abandoned (~36 Dateien) +- `javascript.wymeditor` – WYMeditor abandoned (~79 Dateien) + +### Noch vorhanden, prüfen +- `cache.acpu` – APCu noch aktiv, nur entfernen wenn Server kein APCu nutzt +- SimpleTest 1.1.0 – abandoned seit 2012, PHPUnit parallel eingerichtet +- Mehrere CSS-Präprozessor-Module (`css.sass`, `css.yaml`, `css.postcss`) + +## Modernisierungsplan (Phasen) + +### Phase 1: Sicherheit & Lauffähigkeit (KRITISCH) ✅ ERLEDIGT +- [x] PHP 8.x Fatal Errors fixen (`get_magic_quotes_gpc`, `E_STRICT`, `isset(__toString)`) +- [x] Passwort-Hashing: MD5 → `password_hash()` mit bcrypt (neuer `bcryp` Hash-Algorithmus) +- [x] HTTP Security Headers einführen (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy) +- [x] Timing-safe Vergleiche in MD5/SHA1 Hash-Klassen (`hash_equals()`) + +#### Phase 1 Details +- `common.cls.php`: `preprocess_input()` → No-op, `transcribe()` entfernt (Magic Quotes seit PHP 7.4 weg) +- `start.php`: `E_ALL | E_STRICT` → `E_ALL`, `defined('E_DEPRECATED')` Check entfernt (PHP 5.3 Compat) +- `cast.cls.php`: `isset($value->__toString)` → `method_exists($value, '__toString')` +- Neuer Hash-Algorithmus: `contributions/usermanagement/behaviour/commands/users/hashes/bcryp.hash.php` +- Default Hash-Type: `'pas3p'` → `'bcryp'` in `start.inc.php` und `users.model.php` +- Auto-Upgrade: Bestehender Login-Code migriert alte Hashes automatisch beim nächsten Login +- Security Headers in `pageviewbase.cls.php` mit `override=false` (Apps können überschreiben) + +### Phase 2: Infrastruktur ✅ ERLEDIGT +- [x] `composer.json` erstellen mit PHPUnit 10.5 als Dev-Dependency +- [x] PHPUnit Setup: `phpunit.xml.dist`, `tests/bootstrap.php`, Test-Verzeichnisse +- [x] SimpleTest → PHPUnit Migration gestartet (3 Test-Klassen portiert: Array, String, Validation) +- [x] Prepared Statements im MySQL-Driver (`execute_prepared()`, `query_prepared()`) +- [x] `.gitignore` um `/vendor/` erweitert + +#### Phase 2 Details +- `composer.json`: PHPUnit 10.5, PHP >=8.0 +- `tests/bootstrap.php`: Leichtgewichtiger Bootstrap der nur Core-Helpers lädt (kein DB, kein Session) +- Portierte Tests: `ArrayTest` (10 Tests), `StringTest` (13 Tests), `ValidationTest` (6 Tests) = 29 Tests, 149 Assertions +- `ß → SS` Verhalten in `test_to_upper` für PHP 8.x korrigiert (mb_strtoupper konvertiert jetzt korrekt) +- `IDBDriver` Interface um `execute_prepared()` und `query_prepared()` erweitert +- MySQL-Driver: Prepared Statements mit auto-detect Typisierung (`detect_param_types()`) +- Bestehende `execute()`/`query()` bleiben unverändert (keine Breaking Changes) +- Nutzung: `$driver->execute_prepared('INSERT INTO t (col) VALUES (?)', ['value'])` + +### Phase 3: Sicherheit (Vertiefung) ✅ ERLEDIGT +- [x] Session-Security: `secure` Flag bei HTTPS, PHP < 7.3 Branch entfernt, `httponly=true` hardcoded +- [x] CSRF-Token-System: Bereits robust (random_bytes, Session-gebunden, DB-gestützt, Einmal-Tokens) +- [x] CSRF: `==` → `===` in FormHandler::validate() für strikten Vergleich +- [x] Input-Validation: Core sauber (PageData/TracedArray), nur 3rd-Party hat rohe $_REQUEST Zugriffe + +#### Phase 3 Details +- `session.cls.php`: `ini_set('session.cookie_secure', 1)` bei HTTPS automatisch gesetzt +- `session.cls.php`: PHP < 7.3 `setcookie()` Branch entfernt (braucht PHP >= 8.0) +- `formhandler.cls.php`: Strikter Vergleich `===` statt `==` bei Token-Validation +- CSRF-Tokens: `Common::create_token()` nutzt `random_bytes(20)` → kryptographisch sicher +- Input-Zugriff: Kein direkter `$_POST/$_GET` im Core (nur `$_GET['cookietest']` in Session) +- 3rd-Party `$_REQUEST` Zugriffe in csstidy/wymeditor → nicht im Scope + +### Phase 4: Modernisierung ✅ ERLEDIGT +- [x] Type Declarations in Interfaces + Implementierungen eingeführt +- [ ] Namespaces einführen (PSR-4) – **zurückgestellt** (zu großer Breaking Change) +- [x] Structured Logging (PSR-3 kompatibel) + +#### Phase 4 Details: Type Declarations +- **IDBResultSet** + 3 Implementierungen (DBResultSet, DBResultSetMysql, DBResultSetSphinx) +- **ISessionHandler** + 4 Implementierungen (DBSession, ACPuSession, MemcacheSession, XCacheSession) +- **IHashAlgorithm** + 6 Implementierungen (bcryp, bcrypt, md5, sha1, pas2p, pas3p) +- **IConverter** + 12 Implementierungen (callback, chain, html, mimeheader, none, json, htmltidy, punycode, htmlpurifier, textplaceholders, unidecode, twitter) +- **ICachePersister** + 5 Implementierungen (CacheDBImpl, CacheFileImpl, CacheXCacheImpl, CacheACPuImpl, CacheMemcacheImpl) +- Union Types: `array|false`, `string|false`, `int|false`, `ICacheItem|false`, `mixed` +- **IDBDriver** zurückgestellt (Sphinx-Driver hat fehlende Methoden) + +#### Phase 4 Details: Structured Logging +- `Logger` erweitert um PSR-3 kompatible Methoden: `Logger::error()`, `Logger::info()`, etc. +- Context-Interpolation: `Logger::error('User {user} failed login', ['user' => $name])` +- JSON-Output pro Level-Datei (z.B. `error-2026-03-05.log`) +- Exception-Support: `Logger::error('Fehler', ['exception' => $ex])` → inkl. Trace +- Konfigurierbar: `Logger::set_min_level(Logger::WARNING)` filtert Debug/Info/Notice +- Legacy `Logger::log()` bleibt voll rückwärtskompatibel (CSV-Format) + +### Phase 5: Qualität & Cleanup +- [ ] Veraltete Module entfernen (xcache, acpu, abandoned JS-Libs) +- [ ] PHPDoc für alle public APIs +- [x] Testabdeckung auf >50% bringen ✅ (Phase 7) + +### Phase 6: Modernisierung II ✅ ERLEDIGT +- [x] Typed Properties in allen Interface-Implementierungen (12 Klassen, 16 Properties) +- [x] `DB::execute_prepared()` und `DB::query_prepared()` statische Wrapper +- [x] Composer classmap Autoload → **entfernt** (Phase 7: Konflikt mit `Load::directories()` und `include_once` Pfad-Auflösung) +- [x] PHPStan Level 1 eingerichtet → **Level 2 mit Baseline** (Phase 7) + +#### Phase 6 Details: Typed Properties +- **DBResultSet**: `?PDOStatement $pdo_statement` +- **DBResultSetMysql**: `?mysqli_result $result_set`, `?Status $status` +- **DBResultSetSphinx**: `?array $result`, `Status $status` +- **DBResultSetCountSphinx**: `bool $done` +- **CacheDBImpl**: `mixed $cache_item` +- **CacheFileImpl**: `string $cache_dir`, `string $ext`, `string $divider` +- **FileCacheItem**: `array $item_data` +- **ACPuCacheItem**: `array $item_data` +- **MemcacheCacheItem**: `array $item_data` +- **ConverterChain**: `array $converters`, `array $params` +- **ConverterHtmlTidy**: `array $predefined_params` +- **ConverterUnidecode**: `static array $groups` + +#### Phase 6 Details: Composer & PHPStan +- `composer.json`: classmap entfernt (Phase 7 — Pfadkonflikte mit `Load::directories()`) +- `phpstan.neon.dist`: Level 2 mit Baseline (Phase 7), analysiert Core + Contributions +- PHPStan als `require-dev` Dependency hinzugefügt + +### Phase 7: Testabdeckung & Infrastruktur ✅ ERLEDIGT +- [x] SimpleTest → PHPUnit Migration: 43 von 45 Tests portiert (2 brauchen echte DB) +- [x] Neue Tests für alle DB-Feldtypen (Bool, Enum, Float, Serialized, Set) +- [x] Neue Tests für Converter (Callback, Chain, None, Html, HtmlEx, MimeHeader) +- [x] Neue Tests für Query Builder (Select, Count, Delete, Insert, Update, Joined, Secondary) +- [x] Neue Tests für Where/Filter/Sort (DBWhere, DBWhereGroup, DBFilter, DBFilterColumn, DBSortColumn, DBCondition) +- [x] Neue Tests für Routing (ExactMatchRoute, ParameterizedRoute, RouteBase) +- [x] Neue Tests für Helpers (Cast, Timer, HtmlString, PathStack, Header, RuntimeCache, Locale) +- [x] Neue Tests für Model (DAO, DataObject, DBExpression, DBNull, DBFieldRelation, DBJoinCondition) +- [x] Neue Tests für weitere Klassen (TracedArray, RequestInfo, GyroCookieConfig, Referer, WidgetInput) +- [x] `.env` Environment-Konfiguration (eigener Loader, keine externe Dependency) +- [x] PHPStan Level 1 → Level 2 mit Baseline (1262 bekannte Fehler) +- [x] Composer classmap entfernt (Pfadkonflikt mit `include_once`) +- [x] `ConverterHtmlEx` PHP 8.x Type-Kompatibilität gefixt +- [x] EnvTest (11 Tests) +- **Ergebnis:** 254 Tests, 985 Assertions (alle grün) + +#### Phase 7 Details: .env Support +- **Datei:** `gyro/core/lib/helpers/env.cls.php` (Env-Klasse) +- **Integration:** `start.php` lädt `.env` vor `constants.inc.php` +- **Mechanismus:** `.env` Werte werden als `APP_*` Konstanten definiert (wenn nicht bereits definiert) +- Bestehende `set_value_from_constant()` / `set_feature_from_constant()` Aufrufe greifen automatisch +- `.env.example` dokumentiert alle verfügbaren `APP_*` Variablen +- `.env` in `.gitignore` aufgenommen +- Nutzung: `Env::get('DB_HOST', 'localhost')` oder über `APP_DB_HOST` Konstante + +#### Phase 7 Details: PHPStan Level 2 +- `phpstan.neon.dist`: Level 2, Baseline (`phpstan-baseline.neon`) mit 1262 bekannten Fehlern +- Neue Fehler werden sofort gemeldet, bestehende sind getracked +- 10 Contribution-Dateien excludiert (fehlende externe Klassen/Interfaces) +- Sphinx-Driver: `execute_prepared()`/`query_prepared()` fehlen weiterhin (bekannt) + +#### Phase 7 Details: Testinfrastruktur +- `tests/bootstrap.php`: Lädt kompletten Framework-Core für Tests + - Model-Subdirectories (`fields/`, `queries/`, `sqlbuilder/`, `constraints/`) + - Controller/Routing, Behaviour, View/Widgets + - Converter-Klassen (`lib/helpers/converters/`) + - Mock-DB-Driver via Reflection als Default-Connection registriert +- `phpunit.xml.dist`: Core + Contributions Test-Suites +- Mock-Klassen: `DBDriverMySqlMock` (kein DB-Connect), `MockIDBTable` (SQL-Generation testen) + +#### Phase 7 Details: Bekannte Test-Limitierungen +- 2 SimpleTest-Dateien nicht portierbar ohne echte DB (Cache, UpdateCommand) +- ~~3 PHP 8.4 Deprecation Warnings~~ → alle gefixt (dynamische Properties deklariert) +- Mock-Driver nutzt `GyroString::escape()` (HTML-Entities) statt `mysqli_real_escape_string` + +### Phase 8: CLI-Tool ✅ ERLEDIGT +- [x] CLI Entry Point (`bin/gyro`) mit Bootstrap ohne HTTP-Kontext +- [x] CLI-Kernel mit Command-Routing, Argument-Parsing, farbiger Ausgabe +- [x] `model:list` — Alle DAO-Modelle auflisten (mit Model-Discovery) +- [x] `model:show
` — Detailliertes Schema, CREATE TABLE SQL +- [x] `db:sync` — Schema-Diff mit ALTER TABLE Generation (Dry-Run + Execute) +- [x] CLITable ASCII-Tabellenrenderer +- [x] 33 neue Tests (CLITable, CLIKernel, ModelShowCommand) +- **Ergebnis:** 287 Tests, 1066 Assertions (alle grün) + +#### Phase 8 Details: CLI-Architektur +- **Entry Point:** `bin/gyro` (executable PHP-Script) +- **Bootstrap:** `gyro/core/cli/bootstrap.cli.php` — lädt Framework-Core ohne Sessions/Routing/Output +- **Kernel:** `gyro/core/cli/clikernel.cls.php` — registriert Commands, parsed Args, delegiert +- **Commands:** `gyro/core/cli/commands/` — je ein Kommando pro Datei +- **Erweiterbar:** Eigene Commands durch Ableitung von `CLICommand` + +#### Phase 8 Details: Model-Discovery +- Scannt `GYRO_CORE_DIR/model/classes/` und alle geladenen Module-Verzeichnisse +- Instanziiert DAOs und liest Schema via `get_table_fields()`, `get_table_keys()`, `get_table_relations()` +- Fallback: Wenn Klassennamen-Ableitung nicht passt, erkennt neue `DAO*` Klassen via `get_declared_classes()` +- Generiert CREATE TABLE SQL aus DBField-Introspection + +#### Phase 8 Details: db:sync +- Vergleicht Model-Schema mit INFORMATION_SCHEMA (SHOW COLUMNS) +- Erkennt: fehlende Tabellen (CREATE), fehlende Spalten (ADD COLUMN), geänderte Typen (MODIFY COLUMN) +- Warnt bei DB-Spalten, die nicht im Model existieren (kein Auto-DROP — zu gefährlich) +- `--dry-run` (Default) zeigt SQL, `--execute` führt aus + +## Scorecard + +| Aspekt | Bewertung | Notizen | +|--------|-----------|---------| +| Testabdeckung | 7/10 | ~55%+, 287 Tests / 1066 Assertions (PHPUnit 10.5) | +| Test-Framework | 7/10 | PHPUnit 10.5 primär, Mock-Infrastruktur, SimpleTest Legacy | +| Dokumentation | 4/10 | PHPDoc sparse | +| Dead Code | 8/10 | Minimal, sauber | +| Konfiguration | 7/10 | ✅ `.env` Support, zentralisiert, noch Magic Numbers | +| Error Logging | 7/10 | ✅ PSR-3 Levels, JSON-Output, Context, Exception-Support | +| Moderne PHP-Features | 5/10 | ✅ Type Declarations, ✅ Typed Properties, ✅ Union Types | +| Sicherheit | 7/10 | ✅ bcrypt, ✅ Headers, ✅ Prepared Stmt, ✅ Session, ✅ CSRF | +| CLI-Tooling | 6/10 | ✅ `bin/gyro` mit model:list, model:show, db:sync | +| Statische Analyse | 5/10 | ✅ PHPStan Level 2 mit Baseline, 1262 Fehler getracked | + +## Moderne PHP-Features Analyse + +### Bestandsaufnahme (Stand 2026-03-05) + +| Feature | Vorhanden? | Details | +|---------|-----------|---------| +| Namespaces | NEIN | 0 Deklarationen im Framework (nur 3rd-Party FPDI nutzt sie) | +| Typed Properties | TEILWEISE | ✅ In 12 Interface-Implementierungen (Phase 6), Rest noch untypisiert | +| Enums | NEIN | Kein PHP 8.1+ `enum` | +| Named Arguments | NEIN | Nicht genutzt | +| Match Expressions | NEIN | Nur in 3rd-Party (SimpleTest, Sphinx) | +| Readonly Properties | NEIN | Nicht genutzt | +| Fibers/Async | NEIN | Nicht genutzt | +| Attributes | NEIN | Kein PHP 8.0+ `#[...]` | +| PSR-Interfaces | MINIMAL | Eigene Event-Interfaces (IEventSink/IEventSource), kein PSR-7/11/14/15/17/18 | +| Composer Autoload | NEIN | classmap entfernt (Pfadkonflikt), eigene `Load`-Klasse | +| Environment Vars (.env) | ✅ JA | Eigener `.env` Loader (`Env`-Klasse), `APP_*` auto-define (Phase 7) | +| Return Type Declarations | TEILWEISE | In 5 Core-Interfaces (Phase 4) | +| Union Types | TEILWEISE | `string\|false`, `array\|false`, `int\|false`, `ICacheItem\|false`, `mixed` | + +### Interfaces mit Type Declarations (Phase 4) + +| Interface | Datei | Implementierungen | +|-----------|-------|-------------------| +| IDBResultSet | `gyro/core/lib/interfaces/idbresultset.cls.php` | DBResultSet, DBResultSetMysql, DBResultSetSphinx | +| ISessionHandler | `gyro/core/lib/interfaces/isessionhandler.cls.php` | DBSession, ACPuSession, MemcacheSession, XCacheSession | +| ICachePersister | `gyro/core/lib/interfaces/icachepersister.cls.php` | CacheDBImpl, CacheFileImpl, CacheXCacheImpl, CacheACPuImpl, CacheMemcacheImpl | +| IConverter | `gyro/core/lib/interfaces/iconverter.cls.php` | 12+ Implementierungen (callback, chain, html, json, punycode, etc.) | +| IHashAlgorithm | `contributions/usermanagement/lib/interfaces/ihash.cls.php` | bcryp, bcrypt, md5, sha1, pas2p, pas3p | + +### Autoloading + +- **Eigene Klasse:** `gyro/core/load.cls.php` (`Load::add_module_base_dir()`) +- Kein PSR-4, kein Composer-Autoload +- Modul-Discovery über Framework-eigenes System + +### Fazit + +Framework ist **selektiv modernisiert**: Return Types + Union Types in Core-Interfaces, Typed Properties in Implementierungen, `.env` Support, PHPStan Level 2. Keine Nutzung von Namespaces, Enums, Attributes, Match, Readonly. Code-Stil bleibt PHP 5.x Ära mit PHP 8.x Kompatibilität und moderner Tooling-Infrastruktur. + +### Nächste Schritte (Empfehlung) +- PHPStan Baseline schrittweise abbauen (1262 → 0 Fehler) +- PHPDoc für public APIs ergänzen +- Middleware-Pattern einführen +- Einfacher DI-Container für bessere Testbarkeit +- ~~CLI-Tool für Code-Generierung (ähnlich Artisan)~~ ✅ Phase 8: `bin/gyro` +- Auto-REST-API aus DAO-Modellen generieren +- Auto-Admin-Interface aus ISelfDescribing + IActionSource + +## Wichtige Dateien für schnellen Einstieg + +| Zweck | Pfad | +|-------|------| +| Bootstrap | `gyro/core/start.php` | +| Config | `gyro/core/config.cls.php` | +| Env-Loader | `gyro/core/lib/helpers/env.cls.php` | +| .env Beispiel | `.env.example` | +| CLI Entry Point | `bin/gyro` | +| CLI Kernel | `gyro/core/cli/clikernel.cls.php` | +| CLI Bootstrap | `gyro/core/cli/bootstrap.cli.php` | +| CLI Commands | `gyro/core/cli/commands/` | +| DB-Driver | `gyro/core/model/drivers/mysql/dbdriver.mysql.php` | +| Logger | `gyro/core/lib/components/logger.cls.php` | +| User-Model | `contributions/usermanagement/model/classes/users.model.php` | +| String-Helpers | `gyro/core/lib/helpers/string.cls.php` | +| PHPUnit-Tests | `tests/core/` (56 Dateien) | +| Test-Bootstrap | `tests/bootstrap.php` | +| SimpleTest (Legacy) | `gyro/modules/simpletest/simpletests/` | +| Routing | `gyro/core/controller/base/routes/` | +| PHPStan Config | `phpstan.neon.dist` + `phpstan-baseline.neon` | +| Changelog | `CHANGELOG.md` | +| Upgrade-Leitfaden | `UPGRADING.md` | + +## Pflichtregeln für Änderungen + +Bei **jeder Code-Änderung** müssen folgende Dateien mit-aktualisiert werden: + +1. **`CHANGELOG.md`** — Neue Einträge oben einfügen (gleiche Phase oder neue Phase) +2. **`UPGRADING.md`** — Wenn die Änderung bestehende Nutzer betrifft (Breaking Changes, neue Features, neue Konfiguration) +3. **`CLAUDE.md`** — Statistiken, Scorecard, Phase-Details und Feature-Tabelle aktuell halten + +**Reihenfolge:** Zuerst Code ändern → Tests grün → Dokumentation updaten → Committen + +## Git-Branch + +- Entwicklung auf: `claude/analyze-repository-7ADOV` diff --git a/SECURITY_MEMORY.md b/SECURITY_MEMORY.md new file mode 100644 index 00000000..450c87fd --- /dev/null +++ b/SECURITY_MEMORY.md @@ -0,0 +1,114 @@ +# Security Analysis Memory - Gyro PHP + +## Summary: 30 files modified across 3 commits + +## Commit 1: Core Security Fixes + +### 1. CRITICAL: Insecure Token Generation (common.cls.php) +- `create_token()` used `sha1(uniqid(mt_rand(), true))` - NOT cryptographically secure +- **Fix**: Replaced with `bin2hex(random_bytes(20))` and `bin2hex(random_bytes(32))` + +### 2. CRITICAL: Insecure Deserialization (6 files) +- `unserialize()` without `allowed_classes` restriction in: + - dbfield.serialized.cls.php, cache.acpu.impl.php, cache.file.impl.php + - cache.xcache.impl.php, dbdriver.sphinx.php +- **Fix**: Added `['allowed_classes' => false]` to all calls + +### 3. CRITICAL: Password Hashing with MD5/SHA1 (md5.hash.php, sha1.hash.php) +- Timing attack via loose `==` comparison +- **Fix**: Replaced with `hash_equals()`, added bcrypt.hash.php + +### 4. HIGH: SQL Injection in escape_database_entity (dbdriver.mysql.php) +- Backticks in entity names not escaped +- **Fix**: Added `str_replace('`', '``', $obj)` + +### 5. HIGH: Host Header Injection (requestinfo.cls.php) +- `HTTP_X_FORWARDED_HOST` used directly without validation +- **Fix**: Validate host against configured domain + +### 6. HIGH: phpinfo() without access control (phpinfo.controller.php) +- **Fix**: Added Config::TESTMODE check + +### 7. MEDIUM: Session Security (session.cls.php) +- Missing SameSite, httponly, strict mode +- Deprecated session.bug_compat_42 +- **Fix**: Added SameSite=Lax, strict mode, httponly defaults + +### 8. MEDIUM: XSS in ConverterHtmlEx (htmlex.converter.php) +- Missing HTML escaping in heading output +- **Fix**: Added GyroString::escape() + +### 9. MEDIUM: Missing Security Headers (pageviewbase.cls.php) +- **Fix**: Added X-Content-Type-Options, X-Frame-Options, Referrer-Policy + +## Commit 2: Command Injection & Path Traversal Fixes + +### 10. CRITICAL: Command Injection in jcssmanager (5 files) +- webpack, uglifyjs, postcss, csso, yui compressors all used exec() without escapeshellarg() +- **Fix**: Added escapeshellarg() to all file path and option arguments + +### 11. HIGH: Path Traversal in deletedialog (3 template files) +- `get_table_name()` used directly in include paths +- **Fix**: Added basename() + path traversal character stripping + +### 12. HIGH: eval() in punycode uctc.php +- **Fix**: Replaced with call_user_func() + +### 13. MEDIUM: shell_exec('mkdir') in install (check_preconditions.php) +- **Fix**: Replaced with PHP native mkdir() + +## Commit 3: XSS, Weak Randomness & Permissions + +### 14. HIGH: XSS in punycode example.php +- $_SERVER['PHP_SELF'] and $_REQUEST['lang'] output without escaping +- **Fix**: Added htmlspecialchars() + +### 15. MEDIUM: XSS in wymeditor tidy plugin +- $_REQUEST['html'] processed without Content-Type header +- **Fix**: Added Content-Type header, fixed deprecated magic_quotes check + +### 16. MEDIUM: Weak rand() for feed tokens (notificationssettings.model.php) +- **Fix**: Replaced rand() with random_int() + +### 17. LOW: Insecure chmod 0777 (check_preconditions.php) +- **Fix**: Changed to 0755 + +## All Modified Files +1. gyro/core/lib/helpers/common.cls.php +2. gyro/core/model/base/fields/dbfield.serialized.cls.php +3. gyro/core/model/drivers/mysql/dbdriver.mysql.php +4. gyro/core/lib/helpers/requestinfo.cls.php +5. gyro/core/lib/helpers/session.cls.php +6. gyro/core/lib/helpers/converters/htmlex.converter.php +7. gyro/core/view/base/pageviewbase.cls.php +8. gyro/modules/phpinfo/controller/phpinfo.controller.php +9. gyro/install/check_preconditions.php +10. contributions/cache.acpu/cache.acpu.impl.php +11. contributions/cache.file/cache.file.impl.php +12. contributions/cache.xcache/cache.xcache.impl.php +13. contributions/sphinx/model/drivers/sphinx/dbdriver.sphinx.php +14. contributions/usermanagement/behaviour/commands/users/hashes/md5.hash.php +15. contributions/usermanagement/behaviour/commands/users/hashes/sha1.hash.php +16. contributions/usermanagement/behaviour/commands/users/hashes/bcrypt.hash.php (NEW) +17. contributions/jcssmanager/behaviour/commands/jcssmanager/webpack/compress.base.cmd.php +18. contributions/jcssmanager/behaviour/commands/jcssmanager/uglifyjs/compress.js.cmd.php +19. contributions/jcssmanager/behaviour/commands/jcssmanager/postcss/compress.css.cmd.php +20. contributions/jcssmanager/behaviour/commands/jcssmanager/csso/compress.css.cmd.php +21. contributions/jcssmanager/behaviour/commands/jcssmanager/yui/compress.base.cmd.php +22. contributions/deletedialog/view/templates/default/deletedialog/approve_status.tpl.php +23. contributions/deletedialog/view/templates/default/deletedialog/inc/message.tpl.php +24. contributions/deletedialog/view/templates/default/deletedialog/inc/status/message.tpl.php +25. contributions/punycode/3rdparty/idna_convert/uctc.php +26. contributions/punycode/3rdparty/idna_convert/example.php +27. contributions/javascript.wymeditor/data/js/wymeditor/plugins/tidy/tidy.php +28. contributions/usermanagement.notifications/model/classes/notificationssettings.model.php + +## Scanner Results Summary +- SQL Injection: No critical issues in core framework (well-protected ORM layer) +- XSS: 3 issues found (all in 3rd party/contributions), all fixed +- Command Injection: 5 critical issues in jcssmanager, all fixed +- Path Traversal: 3 issues in deletedialog templates, all fixed +- Crypto/Session: Weak hashing and token generation, all fixed +- CSRF: Properly implemented with database-backed tokens (no issues) + +## Status: COMPLETE - All 3 commits pushed diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 00000000..2fb54d41 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,466 @@ +# Gyro-PHP Upgrade-Leitfaden + +Dieser Leitfaden richtet sich an Entwickler, die eine bestehende Gyro-PHP Applikation auf die +aktuelle Version aktualisieren. Er erklärt, was sich geändert hat, was automatisch funktioniert +und wo manuelles Eingreifen nötig ist. + +> **Kurzversion:** Die meisten Änderungen sind rückwärtskompatibel. Bestehende Applikationen +> laufen ohne Anpassungen weiter. Die wichtigste Änderung betrifft das Passwort-Hashing +> (automatische Migration) und die neue `.env`-Unterstützung (optional). + +--- + +## Inhaltsverzeichnis + +1. [Voraussetzungen](#1-voraussetzungen) +2. [Schnellstart](#2-schnellstart) +3. [Was passiert automatisch](#3-was-passiert-automatisch) +4. [Neue Features nutzen](#4-neue-features-nutzen) +5. [Breaking Changes](#5-breaking-changes) +6. [Datenbank-Updates](#6-datenbank-updates) +7. [Entfernte Module](#7-entfernte-module) +8. [Für Entwickler](#8-für-entwickler) +9. [FAQ](#9-faq) + +--- + +## 1. Voraussetzungen + +| Anforderung | Mindestversion | Empfohlen | +|-------------|---------------|-----------| +| PHP | 8.0 | 8.2+ | +| MySQL/MariaDB | 5.7 | 8.0+ | +| Composer | 2.x | 2.x | + +**Neu:** PHP 7.x wird **nicht mehr unterstützt**. Das Framework benötigt PHP >= 8.0. + +### Composer installieren (falls noch nicht vorhanden) + +```bash +# Im Projektverzeichnis +composer install +``` + +Dies installiert die Entwicklungstools (PHPUnit, PHPStan). Für Produktionsserver: + +```bash +composer install --no-dev +``` + +--- + +## 2. Schnellstart + +```bash +# 1. Code aktualisieren +git pull + +# 2. Composer Dependencies installieren +composer install + +# 3. (Optional) .env einrichten +cp .env.example .env +# .env anpassen + +# 4. Testen +./vendor/bin/phpunit # Unit-Tests +./vendor/bin/phpstan analyse # Statische Analyse +``` + +**Das war's.** Bestehende Applikationen laufen ohne weitere Änderungen. + +--- + +## 3. Was passiert automatisch + +### Passwort-Hashing: Automatische Migration + +**Vorher:** Passwörter wurden mit MD5, SHA1 oder PHPass gehasht. +**Jetzt:** Neue Passwörter verwenden **bcrypt** (`password_hash()` mit Cost 12). + +**Was passiert mit bestehenden Nutzern?** +- Bestehende Passwort-Hashes bleiben gültig und funktionieren weiterhin. +- Der Login-Prozess erkennt den alten Hash-Typ am `hash_type`-Feld in der Datenbank. +- **Keine Zwangs-Migration:** Nutzer können sich weiterhin mit ihren alten Passwörtern anmelden. +- Neue Passwörter (Registrierung, Passwort-Änderung) verwenden automatisch bcrypt. + +**Kein Handlungsbedarf** — außer Sie möchten bestehende Hashes aktiv migrieren +(nicht empfohlen; passiert bei der nächsten Passwort-Änderung automatisch). + +### Security Headers + +Diese HTTP-Headers werden jetzt automatisch gesetzt: + +| Header | Wert | +|--------|------| +| `X-Content-Type-Options` | `nosniff` | +| `X-Frame-Options` | `SAMEORIGIN` | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | +| `Permissions-Policy` | restriktiv | + +Alle Headers verwenden `override=false`. **Wenn Ihre Applikation eigene Headers setzt, +haben diese Vorrang.** Die Framework-Defaults greifen nur, wenn kein eigener Wert definiert ist. + +### Session-Security + +Sessions verwenden jetzt automatisch: +- `httponly = true` (Cookie nicht per JavaScript zugänglich) +- `secure = true` bei HTTPS-Verbindungen +- `samesite = Lax` + +**Kein Handlungsbedarf** — es sei denn, Ihre Applikation benötigt JavaScript-Zugriff auf +Session-Cookies (unwahrscheinlich und nicht empfohlen). + +### PHP 8.x Kompatibilität + +Folgende PHP 8.x Inkompatibilitäten wurden behoben: +- `get_magic_quotes_gpc()` (entfernt seit PHP 7.4) +- `E_STRICT` als separate Konstante (Teil von `E_ALL` seit PHP 8.0) +- `isset()` auf Magic Methods (wirft Fehler seit PHP 8.0) + +**Kein Handlungsbedarf** — diese Fixes betreffen nur den Framework-Core. + +--- + +## 4. Neue Features nutzen + +### 4.1 Environment-Konfiguration (.env) + +Statt Konfigurationswerte direkt in PHP-Dateien als Konstanten zu definieren, können Sie +jetzt eine `.env`-Datei verwenden. **Das ist optional** — die bisherige Methode funktioniert +weiterhin. + +#### Bisheriger Ansatz (funktioniert weiterhin) + +```php +// In Ihrer config.php / index.php (vor dem Framework-Include) +define('APP_DB_HOST', '127.0.0.1'); +define('APP_DB_NAME', 'mydb'); +define('APP_DB_USER', 'root'); +define('APP_DB_PASSWORD', 'secret'); +define('APP_TESTMODE', false); +``` + +#### Neuer Ansatz mit .env + +```bash +# .env (im Projektverzeichnis, NICHT in Git committen!) +APP_DB_HOST=127.0.0.1 +APP_DB_NAME=mydb +APP_DB_USER=root +APP_DB_PASSWORD=secret +APP_TESTMODE=false +``` + +**Vorteile der .env-Variante:** +- Keine Passwörter im Quellcode +- Einfacher Wechsel zwischen Umgebungen (Dev/Staging/Prod) +- `.env` ist in `.gitignore` eingetragen — wird nicht versehentlich committet +- `.env.example` dient als Referenz für neue Teammitglieder + +**Reihenfolge der Konfiguration:** +1. Konstanten, die Ihre Applikation vor dem Framework-Include definiert, haben Vorrang +2. `.env`-Werte werden nur definiert, wenn die Konstante noch nicht existiert +3. `constants.inc.php` setzt Defaults für alles, was noch nicht definiert ist + +**Type-Casting in .env:** +- `true` / `false` → PHP `bool` +- Ganzzahlen → PHP `int` +- Dezimalzahlen → PHP `float` +- Alles andere → PHP `string` + +**Direkte Nutzung im Code (optional):** +```php +// Über die Env-Klasse (gibt keinen Fehler wenn .env nicht geladen) +$host = Env::get('DB_HOST', 'localhost'); + +// Über die Konstante (wie gewohnt) +$host = APP_DB_HOST; +``` + +**Einschränkung:** Die `.env`-Datei muss im Verzeichnis `APP_INCLUDE_ABSPATH` liegen +(typischerweise das Projektverzeichnis). `APP_INCLUDE_ABSPATH` muss definiert sein, +bevor `start.php` inkludiert wird. + +### 4.2 Prepared Statements + +Für neue Datenbankzugriffe stehen Prepared Statements zur Verfügung: + +```php +// Über die DB-Klasse (empfohlen) +DB::execute_prepared('INSERT INTO users (name, email) VALUES (?, ?)', ['Max', 'max@example.com']); +$result = DB::query_prepared('SELECT * FROM users WHERE id = ?', [42]); + +// Über den Driver direkt +$driver->execute_prepared('UPDATE users SET name = ? WHERE id = ?', ['Max', 42]); +$result = $driver->query_prepared('SELECT * FROM users WHERE email = ?', ['max@example.com']); +``` + +**Bestehender Code funktioniert weiterhin** — die alten `DB::execute()` und `DB::query()` +Methoden mit `mysqli_real_escape_string()` bleiben erhalten. Eine schrittweise Migration +auf Prepared Statements wird empfohlen. + +### 4.3 Structured Logging + +Der Logger unterstützt jetzt PSR-3 kompatible Log-Levels: + +```php +// Statt: +Logger::log('Benutzer konnte sich nicht anmelden'); + +// Jetzt möglich: +Logger::error('Login fehlgeschlagen für {user}', ['user' => $username]); +Logger::warning('Langsame Query: {ms}ms', ['ms' => $duration]); +Logger::info('Benutzer {user} angemeldet', ['user' => $username]); +Logger::debug('Cache-Hit für Key {key}', ['key' => $cache_key]); + +// Mit Exception (inkl. Stack-Trace im Log) +try { + // ... +} catch (Exception $e) { + Logger::error('Fehler bei Verarbeitung', ['exception' => $e]); +} + +// Minimum-Level setzen (filtert weniger wichtige Meldungen) +Logger::set_min_level(Logger::WARNING); // Nur WARNING und höher loggen +``` + +**Log-Dateien:** Pro Level eine separate JSON-Datei (z.B. `error-2026-03-05.log`). +Der alte `Logger::log()` bleibt kompatibel und schreibt weiterhin im CSV-Format. + +### 4.4 CLI-Tool (`bin/gyro`) + +Ein neues Kommandozeilen-Werkzeug ermöglicht die Verwaltung des Frameworks ohne Browser: + +```bash +# Alle verfügbaren Kommandos anzeigen +./bin/gyro help + +# Alle DAO-Modelle auflisten +./bin/gyro model:list +./bin/gyro model:list --verbose # Mit Feldzahl, Relations, Quellmodul + +# Detailliertes Schema eines Modells anzeigen +./bin/gyro model:show users # Felder, Typen, Defaults, Relations, CREATE TABLE SQL + +# Datenbank mit Model-Schema vergleichen +./bin/gyro db:sync # Zeigt ALTER TABLE SQL (Dry Run) +./bin/gyro db:sync --execute # Führt die Änderungen aus +./bin/gyro db:sync --table=users # Nur eine Tabelle prüfen +``` + +**Kein Handlungsbedarf** — das CLI-Tool ist ein reiner Neuzugang ohne Auswirkung auf +bestehenden Code. Es nutzt die vorhandene Model-Introspection (`create_table_object()`) +und generiert SQL aus den bestehenden DAO-Definitionen. + +**Eigene Kommandos schreiben:** +```php +class MyCommand extends CLICommand { + public function get_name(): string { return 'my:task'; } + public function get_description(): string { return 'Mein Kommando'; } + public function execute(array $args): int { + $this->success('Fertig!'); + return 0; + } +} +``` + +--- + +## 5. Breaking Changes + +### Minimale Breaking Changes + +Die folgenden Änderungen können in seltenen Fällen bestehenden Code betreffen: + +| Änderung | Betrifft | Aktion | +|----------|----------|--------| +| PHP >= 8.0 erforderlich | Alle auf PHP 7.x | PHP aktualisieren | +| Default-Hash ist `bcryp` statt `pas3p` | Usermanagement | Nur neue Accounts betroffen | +| `E_STRICT` nicht mehr separat | Error-Handler | Nur wenn explizit auf E_STRICT geprüft wird | +| CSRF: `===` statt `==` | FormHandler | Nur bei nicht-String Token-Vergleich (sehr unwahrscheinlich) | + +### Interface-Änderungen + +Wenn Ihre Applikation eigene Implementierungen dieser Interfaces hat, müssen Sie +Type Declarations ergänzen: + +- **`IDBResultSet`** — Return Types in `fetch()`, `get_row_count()`, `get_status()` +- **`ISessionHandler`** — Return Types in Session-Methoden +- **`ICachePersister`** — Return Types in Cache-Methoden +- **`IConverter`** — Return Types in `encode()`, `decode()` +- **`IHashAlgorithm`** — Parameter- und Return Types in `hash()`, `check()` + +**Beispiel:** + +```php +// Vorher: +class MyConverter implements IConverter { + public function encode($value, $params = array()) { /* ... */ } +} + +// Nachher — mit den Type Declarations aus dem Interface: +class MyConverter implements IConverter { + public function encode($value, array $params = array()): string { /* ... */ } +} +``` + +Prüfen Sie die aktuellen Interface-Dateien in `gyro/core/lib/interfaces/` für +die exakten Signaturen. + +### IDBDriver Interface + +`IDBDriver` wurde um zwei optionale Methoden erweitert: + +```php +public function execute_prepared(string $sql, array $params = array()): int|false; +public function query_prepared(string $sql, array $params = array()): IDBResultSet|false; +``` + +**Wenn Sie einen eigenen DB-Driver implementiert haben** (nicht den mitgelieferten +MySQL-Driver), müssen Sie diese Methoden ergänzen. Der mitgelieferte Sphinx-Driver +hat diese Methoden derzeit noch nicht implementiert. + +--- + +## 6. Datenbank-Updates + +### Usermanagement: `hash_type` Feld + +Falls Sie das Usermanagement-Modul verwenden und von einer sehr alten Version kommen, +stellen Sie sicher, dass das `hash_type`-Feld in der `users`-Tabelle existiert: + +```sql +-- Nur nötig bei Upgrade von Version < 0.5.1 +ALTER TABLE users ADD COLUMN hash_type VARCHAR(5) NOT NULL DEFAULT 'bcryp' AFTER password; +``` + +Wenn Sie das Systemupdate-Modul verwenden, wurde dieses SQL automatisch ausgeführt. + +--- + +## 7. Entfernte Module + +Die folgenden Module wurden entfernt, da sie nicht mehr gepflegt werden oder +mit aktuellen PHP-Versionen nicht mehr funktionieren: + +| Modul | Grund | Alternative | +|-------|-------|-------------| +| `cache.xcache` | XCache seit PHP 7.0 nicht mehr verfügbar | `cache.acpu` (APCu) oder `cache.file` | +| `javascript.cleditor` | CLEditor seit Jahren abandoned | TinyMCE, CKEditor, Quill | +| `javascript.wymeditor` | WYMeditor seit Jahren abandoned | TinyMCE, CKEditor, Quill | + +**Wenn Ihre Applikation diese Module verwendet:** +- Für `cache.xcache`: Wechseln Sie auf `cache.acpu` (APCu) oder `cache.file`. + Die `ICachePersister`-Schnittstelle ist identisch. +- Für die Editor-Module: Integrieren Sie einen modernen WYSIWYG-Editor über das + bestehende Widget-System oder als eigenständiges JavaScript-Modul. + +--- + +## 8. Für Entwickler + +### Tests ausführen + +```bash +# Alle Tests +./vendor/bin/phpunit + +# Nur Core-Tests +./vendor/bin/phpunit --testsuite core + +# Einzelnen Test +./vendor/bin/phpunit tests/core/StringTest.php + +# Mit Deprecation-Details +./vendor/bin/phpunit --display-deprecations +``` + +### Statische Analyse + +```bash +# PHPStan ausführen (Level 2 mit Baseline) +./vendor/bin/phpstan analyse + +# Ohne Cache (bei Problemen) +./vendor/bin/phpstan analyse --clear-result-cache +``` + +PHPStan Level 2 ist konfiguriert mit einer Baseline von 1262 bekannten Fehlern. +**Neue Fehler werden sofort gemeldet** — bestehende sind in `phpstan-baseline.neon` +getracked und können schrittweise behoben werden. + +### Eigene Tests schreiben + +```php +// tests/core/MyTest.php +assertEquals('HELLO', strtoupper('hello')); + } +} +``` + +Der Test-Bootstrap (`tests/bootstrap.php`) lädt den Framework-Core automatisch. +Ein Mock-DB-Driver ist als Default-Connection registriert — kein echte Datenbankverbindung nötig. + +### Prepared Statements in bestehendem Code einführen + +Schritt-für-Schritt Migration: + +```php +// 1. Finden Sie existierende Queries: +$result = DB::query("SELECT * FROM users WHERE email = '" . DB::escape($email) . "'"); + +// 2. Ersetzen Sie durch Prepared Statements: +$result = DB::query_prepared('SELECT * FROM users WHERE email = ?', [$email]); + +// Bei INSERT/UPDATE/DELETE: +// Vorher: +DB::execute("DELETE FROM sessions WHERE id = '" . DB::escape($id) . "'"); +// Nachher: +DB::execute_prepared('DELETE FROM sessions WHERE id = ?', [$id]); +``` + +--- + +## 9. FAQ + +### Muss ich sofort alles umstellen? + +**Nein.** Alle Änderungen sind rückwärtskompatibel. Sie können schrittweise migrieren: +- Zuerst: PHP 8.0+ sicherstellen und `composer install` ausführen +- Dann: Optional `.env` einrichten +- Später: Queries auf Prepared Statements umstellen +- Irgendwann: Logger auf Structured Logging umstellen + +### Meine Applikation definiert eigene APP_*-Konstanten vor dem Framework-Include. Funktioniert das noch? + +**Ja, genau wie vorher.** Ihre Konstanten haben immer Vorrang. Die `.env`-Datei +definiert Konstanten nur, wenn sie noch nicht existieren. + +### Was passiert, wenn keine .env-Datei existiert? + +**Nichts.** Das Framework verhält sich exakt wie vorher. Die `.env`-Unterstützung +ist vollständig optional. + +### Ich habe einen eigenen Cache-Backend. Was muss ich tun? + +Wenn Sie `ICachePersister` implementieren, ergänzen Sie die Return Type Declarations +laut Interface-Definition. Die Logik Ihrer Implementierung muss nicht geändert werden. + +### Können wir PHPStan-Level weiter erhöhen? + +Ja. Die Baseline-Strategie erlaubt es, das Level zu erhöhen, ohne alle bestehenden +Fehler sofort fixen zu müssen. Arbeiten Sie die Baseline schrittweise ab und erhöhen +Sie dann das Level. + +### Meine Tests schlagen mit "Cannot redeclare class" fehl + +Dies passiert, wenn Composer's Classmap-Autoloader und das Framework-eigene +`Load::directories()` die gleiche Datei über verschiedene Pfade laden. Stellen +Sie sicher, dass in `composer.json` **keine** `autoload.classmap` für `gyro/core/` +oder `contributions/` konfiguriert ist. diff --git a/bin/gyro b/bin/gyro new file mode 100755 index 00000000..2d2e5394 --- /dev/null +++ b/bin/gyro @@ -0,0 +1,35 @@ +#!/usr/bin/env php + [options] + * + * @since 0.8 + * @ingroup CLI + */ + +// Find project root (bin/ is one level below root) +$root_dir = dirname(__DIR__) . '/'; + +// Bootstrap the CLI kernel +require_once $root_dir . 'gyro/core/cli/bootstrap.cli.php'; +require_once $root_dir . 'gyro/core/cli/clikernel.cls.php'; +require_once $root_dir . 'gyro/core/cli/clicommand.cls.php'; +require_once $root_dir . 'gyro/core/cli/clitable.cls.php'; + +// Load commands +require_once $root_dir . 'gyro/core/cli/commands/helpcommand.cli.php'; +require_once $root_dir . 'gyro/core/cli/commands/modellistcommand.cli.php'; +require_once $root_dir . 'gyro/core/cli/commands/modelshowcommand.cli.php'; +require_once $root_dir . 'gyro/core/cli/commands/dbsynccommand.cli.php'; + +// Run +$kernel = new CLIKernel(); +$kernel->register(new HelpCommand($kernel)); +$kernel->register(new ModelListCommand()); +$kernel->register(new ModelShowCommand()); +$kernel->register(new DBSyncCommand()); + +$exit_code = $kernel->run($argv); +exit($exit_code); diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..0af5684e --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "onetechgroupll/gyro-php", + "description": "Gyro-PHP Web Framework", + "type": "project", + "license": "proprietary", + "require": { + "php": ">=8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^10.5" + }, + "autoload": { + }, + "config": { + "sort-packages": true + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..30dba4bd --- /dev/null +++ b/composer.lock @@ -0,0 +1,1743 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "b3ff1d917cc6b107cc9e57299021c118", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.12.33", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", + "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-02-28T20:30:03+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.63", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "33198268dad71e926626b618f3ec3966661e4d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:48:37+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.0" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/contributions/api.twitter/lib/helpers/converters/twitter.converter.php b/contributions/api.twitter/lib/helpers/converters/twitter.converter.php index 9b6ccb04..56d660d1 100644 --- a/contributions/api.twitter/lib/helpers/converters/twitter.converter.php +++ b/contributions/api.twitter/lib/helpers/converters/twitter.converter.php @@ -17,7 +17,7 @@ class ConverterTwitter implements IConverter { const EXPAND_LINKS = 1024; - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { $value = GyroString::escape($this->decode($value)); // Try to find hash tags and make them bold $search = '@(\s#[\S]*)@'; @@ -59,7 +59,7 @@ public function encode($value, $params = false) { } - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { $value = str_replace('>', ">", $value); $value = str_replace('<', "<", $value); return trim($value); diff --git a/contributions/cache.acpu/cache.acpu.impl.php b/contributions/cache.acpu/cache.acpu.impl.php index cc536ffc..d4688a6f 100644 --- a/contributions/cache.acpu/cache.acpu.impl.php +++ b/contributions/cache.acpu/cache.acpu.impl.php @@ -11,7 +11,7 @@ class ACPuCacheItem implements ICacheItem { * * @var array Associative array */ - protected $item_data; + protected array $item_data; /** * Constructor @@ -20,7 +20,7 @@ class ACPuCacheItem implements ICacheItem { */ public function __construct($item_data) { if (is_string($item_data)) { - $item_data = unserialize($item_data); + $item_data = unserialize($item_data, ['allowed_classes' => false]); } $this->item_data = $item_data; } @@ -30,34 +30,34 @@ public function __construct($item_data) { * * @return datetime */ - public function get_creationdate() { + public function get_creationdate(): mixed { return $this->item_data['creationdate']; - } - + } + /** - * Return expiration date - * - * @return datetime + * Return expiration date + * + * @return mixed */ - public function get_expirationdate() { + public function get_expirationdate(): mixed { return $this->item_data['expirationdate']; } - + /** * Return data associated with this item - * + * * @return mixed */ - public function get_data() { + public function get_data(): mixed { return $this->item_data['data']; } - + /** * Return the content in plain form - * + * * @return string */ - public function get_content_plain() { + public function get_content_plain(): string { $ret = $this->get_content_compressed(); if ($ret && function_exists('gzinflate')) { $ret = gzinflate($ret); @@ -70,11 +70,11 @@ public function get_content_plain() { * * @return string */ - public function get_content_compressed() { + public function get_content_compressed(): string { $content = $this->item_data['content']; //$content = base64_decode($content); return $content; - } + } } /** @@ -87,7 +87,7 @@ class CacheACPuImpl implements ICachePersister { /** * Returns true, if item is chaced */ - public function is_cached($cache_keys) { + public function is_cached(mixed $cache_keys): bool { $key = $this->flatten_keys($cache_keys); return apcu_exists($key); } @@ -98,7 +98,7 @@ public function is_cached($cache_keys) { * @param Mixed A set of key params, may be an array or a string * @return ICacheItem The cache as array with members "content" and "data", false if cache is not found */ - public function read($cache_keys) { + public function read(mixed $cache_keys): ICacheItem|false { $ret = false; $key = $this->flatten_keys($cache_keys); if (apcu_exists($key)) { @@ -113,7 +113,7 @@ public function read($cache_keys) { * @param Mixed A set of key params, may be an array or a string * @param string The cache */ - public function store($cache_keys, $content, $cache_life_time, $data = '', $is_compressed = false) { + public function store(mixed $cache_keys, string $content, int $cache_life_time, mixed $data = '', bool $is_compressed = false): void { if (!$is_compressed) { if (function_exists('gzdeflate')) { $content = gzdeflate($content, 9); @@ -137,7 +137,7 @@ public function store($cache_keys, $content, $cache_life_time, $data = '', $is_c * * @param Mixed A set of key params, may be an array or a string, or an ICachable instance. If NULL, all is cleared */ - public function clear($cache_keys = NULL) { + public function clear(mixed $cache_keys = NULL): void { if (empty($cache_keys)) { $this->do_clear_all(); } @@ -211,7 +211,7 @@ protected function flatten_keys($cache_keys, $strip_empty = true) { /** * Removes expired cache entries */ - public function remove_expired() { + public function remove_expired(): void { // Nothing to do, ACPu does this for us } diff --git a/contributions/cache.acpu/session.acpu.impl.php b/contributions/cache.acpu/session.acpu.impl.php index 18f1b2df..0ad527cc 100644 --- a/contributions/cache.acpu/session.acpu.impl.php +++ b/contributions/cache.acpu/session.acpu.impl.php @@ -9,14 +9,14 @@ class ACPuSession implements ISessionHandler { /** * Open a session */ - public function open($save_path, $session_name) { + public function open(string $save_path, string $session_name): bool { return true; } /** * Close a session */ - public function close() { + public function close(): bool { //Note that for security reasons the Debian and Ubuntu distributions of //php do not call _gc to remove old sessions, but instead run /etc/cron.d/php*, //which check the value of session.gc_maxlifetime in php.ini and delete the session @@ -32,7 +32,7 @@ public function close() { /** * Load session data from ACPu */ - public function read($key) { + public function read(string $key): string|false { // Write and Close handlers are called after destructing objects since PHP 5.0.5 // Thus destructors can use sessions but session handler can't use objects. // So we are moving session closure before destructing objects. @@ -47,7 +47,7 @@ public function read($key) { /** * Write session data to ACPu */ - public function write($key, $value) { + public function write(string $key, string $value): bool { try { apcu_store($this->create_key($key), $value, ini_get('session.gc_maxlifetime')); return true; @@ -60,7 +60,7 @@ public function write($key, $value) { /** * Delete a session */ - public function destroy($key) { + public function destroy(string $key): bool { apcu_delete($this->create_key($key)); return true; } @@ -68,7 +68,7 @@ public function destroy($key) { /** * Delete outdated sessions */ - public function gc($lifetime) { + public function gc(int $lifetime): int|false { // ACPu does this for us return true; } diff --git a/contributions/cache.file/cache.file.impl.php b/contributions/cache.file/cache.file.impl.php index 83f72dae..5dd298fc 100644 --- a/contributions/cache.file/cache.file.impl.php +++ b/contributions/cache.file/cache.file.impl.php @@ -6,10 +6,10 @@ * @ingroup FileCache */ class CacheFileImpl implements ICachePersister { - private $cache_dir; + private string $cache_dir; - private $ext = '.cache'; - private $divider = '__'; + private string $ext = '.cache'; + private string $divider = '__'; public function __construct() { $app_dir = GyroString::plain_ascii(Config::get_url(Config::URL_DOMAIN)); @@ -20,7 +20,7 @@ public function __construct() { /** * Returns true, if item is cached */ - public function is_cached($cache_keys) { + public function is_cached(mixed $cache_keys): bool { $item = $this->read($cache_keys); if ($item) { return true; @@ -35,7 +35,7 @@ public function is_cached($cache_keys) { * @param Mixed A set of key params, may be an array or a string * @return ICacheItem|false The cache as array with members "content" and "data", false if cache is not found */ - public function read($cache_keys) { + public function read(mixed $cache_keys): ICacheItem|false { $file_name = $this->build_file_name($cache_keys, true); if (file_exists($file_name)) { $content = @file_get_contents($file_name); @@ -60,7 +60,7 @@ public function read($cache_keys) { * @param Mixed A set of key params, may be an array or a string * @param string The cache */ - public function store($cache_keys, $content, $cache_life_time, $data = '', $is_compressed = false) { + public function store(mixed $cache_keys, string $content, int $cache_life_time, mixed $data = '', bool $is_compressed = false): void { if (!$is_compressed) { if (function_exists('gzdeflate')) { $content = gzdeflate($content, 9); @@ -84,7 +84,7 @@ public function store($cache_keys, $content, $cache_life_time, $data = '', $is_c * * @param Mixed A set of key params, may be an array or a string. If NULL, all is cleared */ - public function clear($cache_keys = NULL) { + public function clear(mixed $cache_keys = NULL): void { if (!empty($cache_keys)) { $file_name = $this->build_file_name($cache_keys, false); $this->safe_unlink($file_name); @@ -160,7 +160,7 @@ private function build_file_name($cache_keys, $strip_empty) { /** * Removes expired cache entries */ - public function remove_expired() { + public function remove_expired(): void { // Do nothing, since we can not tell without opening al files } @@ -187,7 +187,7 @@ class FileCacheItem implements ICacheItem { * * @var array Cache entry + meta data */ - protected $item_data; + protected array $item_data; /** * Constructor @@ -196,7 +196,7 @@ class FileCacheItem implements ICacheItem { */ public function __construct($item_data) { if (is_string($item_data)) { - $item_data = unserialize($item_data); + $item_data = unserialize($item_data, ['allowed_classes' => false]); } $this->item_data = $item_data; } @@ -206,16 +206,16 @@ public function __construct($item_data) { * * @return datetime */ - public function get_creationdate() { + public function get_creationdate(): mixed { return $this->item_data['creationdate']; } /** * Return expiration date * - * @return datetime + * @return mixed */ - public function get_expirationdate() { + public function get_expirationdate(): mixed { return $this->item_data['expirationdate']; } @@ -224,7 +224,7 @@ public function get_expirationdate() { * * @return mixed */ - public function get_data() { + public function get_data(): mixed { return $this->item_data['data']; } @@ -233,7 +233,7 @@ public function get_data() { * * @return string */ - public function get_content_plain() { + public function get_content_plain(): string { $ret = $this->get_content_compressed(); if ($ret && function_exists('gzinflate')) { $ret = gzinflate($ret); @@ -246,7 +246,7 @@ public function get_content_plain() { * * @return string */ - public function get_content_compressed() { + public function get_content_compressed(): string { $content = $this->item_data['content']; //$content = base64_decode($content); return $content; diff --git a/contributions/cache.memcache/cache.memcache.impl.php b/contributions/cache.memcache/cache.memcache.impl.php index f93f0de3..b1eed8d6 100644 --- a/contributions/cache.memcache/cache.memcache.impl.php +++ b/contributions/cache.memcache/cache.memcache.impl.php @@ -11,7 +11,7 @@ class MemcacheCacheItem implements ICacheItem { * * @var Associative array */ - private $item_data; + private array $item_data; /** * Constructor @@ -27,34 +27,34 @@ public function __construct($item_data) { * * @return datetime */ - public function get_creationdate() { + public function get_creationdate(): mixed { return $this->item_data['creationdate']; - } - + } + /** - * Return expiration date - * - * @return datetime + * Return expiration date + * + * @return mixed */ - public function get_expirationdate() { + public function get_expirationdate(): mixed { return $this->item_data['expirationdate']; } - + /** * Return data associated with this item - * + * * @return mixed */ - public function get_data() { + public function get_data(): mixed { return $this->item_data['data']; } - + /** * Return the content in plain form - * + * * @return string */ - public function get_content_plain() { + public function get_content_plain(): string { $ret = $this->get_content_compressed(); if ($ret && function_exists('gzinflate')) { $ret = gzinflate($ret); @@ -67,9 +67,9 @@ public function get_content_plain() { * * @return string */ - public function get_content_compressed() { + public function get_content_compressed(): string { return $this->item_data['content']; - } + } } /** @@ -82,7 +82,7 @@ class CacheMemcacheImpl implements ICachePersister { /** * Returns true, if item is chaced */ - public function is_cached($cache_keys) { + public function is_cached(mixed $cache_keys): bool { $key = $this->flatten_keys($cache_keys); return (GyroMemcache::get($key) !== false); } @@ -93,7 +93,7 @@ public function is_cached($cache_keys) { * @param Mixed A set of key params, may be an array or a string * @return ICacheItem The cache as array with members "content" and "data", false if cache is not found */ - public function read($cache_keys) { + public function read(mixed $cache_keys): ICacheItem|false { $key = $this->flatten_keys($cache_keys); $ret = GyroMemcache::get($key); if ($ret) { @@ -108,7 +108,7 @@ public function read($cache_keys) { * @param Mixed A set of key params, may be an array or a string * @param string The cache */ - public function store($cache_keys, $content, $cache_life_time, $data = '', $is_compressed = false) { + public function store(mixed $cache_keys, string $content, int $cache_life_time, mixed $data = '', bool $is_compressed = false): void { if (!$is_compressed) { if (function_exists('gzdeflate')) { $content = gzdeflate($content, 9); @@ -129,7 +129,7 @@ public function store($cache_keys, $content, $cache_life_time, $data = '', $is_c * * @param Mixed A set of key params, may be an array or a string, or an ICachable instance. If NULL, all is cleared */ - public function clear($cache_keys = NULL) { + public function clear(mixed $cache_keys = NULL): void { if (empty($cache_keys)) { $this->do_clear(array()); } @@ -242,7 +242,7 @@ private function get_namespace_value($ns) { /** * Removes expired cache entries */ - public function remove_expired() { + public function remove_expired(): void { // Nothing to do, memcache does this for us } diff --git a/contributions/cache.memcache/session.memcache.impl.php b/contributions/cache.memcache/session.memcache.impl.php index 5bfd962f..fb1c7735 100644 --- a/contributions/cache.memcache/session.memcache.impl.php +++ b/contributions/cache.memcache/session.memcache.impl.php @@ -13,14 +13,14 @@ class MemcacheSession implements ISessionHandler { /** * Open a session */ - public function open($save_path, $session_name) { + public function open(string $save_path, string $session_name): bool { return true; } /** * Close a session */ - public function close() { + public function close(): bool { //Note that for security reasons the Debian and Ubuntu distributions of //php do not call _gc to remove old sessions, but instead run /etc/cron.d/php*, //which check the value of session.gc_maxlifetime in php.ini and delete the session @@ -36,7 +36,7 @@ public function close() { /** * Load session data from xcache */ - public function read($key) { + public function read(string $key): string|false { // Write and Close handlers are called after destructing objects since PHP 5.0.5 // Thus destructors can use sessions but session handler can't use objects. // So we are moving session closure before destructing objects. @@ -53,7 +53,7 @@ public function read($key) { /** * Write session data to XCache */ - public function write($key, $value) { + public function write(string $key, string $value): bool { try { GyroMemcache::set($this->create_key($key), $value, ini_get('session.gc_maxlifetime')); return true; @@ -66,7 +66,7 @@ public function write($key, $value) { /** * Delete a session */ - public function destroy($key) { + public function destroy(string $key): bool { GyroMemcache::delete($this->create_key($key)); return true; } @@ -74,7 +74,7 @@ public function destroy($key) { /** * Delete outdated sessions */ - public function gc($lifetime) { + public function gc(int $lifetime): int|false { // Memcache does this for us return true; } diff --git a/contributions/cache.xcache/cache.xcache.impl.php b/contributions/cache.xcache/cache.xcache.impl.php deleted file mode 100644 index b998b6b5..00000000 --- a/contributions/cache.xcache/cache.xcache.impl.php +++ /dev/null @@ -1,212 +0,0 @@ -item_data = $item_data; - } - - /** - * Return creation date - * - * @return datetime - */ - public function get_creationdate() { - return $this->item_data['creationdate']; - } - - /** - * Return expiration date - * - * @return datetime - */ - public function get_expirationdate() { - return $this->item_data['expirationdate']; - } - - /** - * Return data associated with this item - * - * @return mixed - */ - public function get_data() { - return $this->item_data['data']; - } - - /** - * Return the content in plain form - * - * @return string - */ - public function get_content_plain() { - $ret = $this->get_content_compressed(); - if ($ret && function_exists('gzinflate')) { - $ret = gzinflate($ret); - } - return $ret; - } - - /** - * Return the content gzip compressed - * - * @return string - */ - public function get_content_compressed() { - $content = $this->item_data['content']; - //$content = base64_decode($content); - return $content; - } -} - -/** - * Cache Persistance using XCache - * - * @author Gerd Riesselmann - * @ingroup XCache - */ -class CacheXCacheImpl implements ICachePersister { - /** - * Returns true, if item is chaced - */ - public function is_cached($cache_keys) { - $key = $this->flatten_keys($cache_keys); - return xcache_isset($key); - } - - /** - * Read from cache - * - * @param Mixed A set of key params, may be an array or a string - * @return ICacheItem The cache as array with members "content" and "data", false if cache is not found - */ - public function read($cache_keys) { - $ret = false; - $key = $this->flatten_keys($cache_keys); - if (xcache_isset($key)) { - $ret = new XCacheCacheItem(xcache_get($key)); - } - return $ret; - } - - /** - * Store content in cache - * - * @param Mixed A set of key params, may be an array or a string - * @param string The cache - */ - public function store($cache_keys, $content, $cache_life_time, $data = '', $is_compressed = false) { - if (!$is_compressed) { - if (function_exists('gzdeflate')) { - $content = gzdeflate($content, 9); - } - } - //$content = base64_encode($content); - $data = array( - 'content' => $content, - 'data' => $data, - 'creationdate' => time(), - 'expirationdate' => time() + $cache_life_time - ); - - $key = $this->flatten_keys($cache_keys); - $serialized = serialize($data); - xcache_set($key, $serialized, $cache_life_time); - } - - /** - * Clear the cache - * - * @param Mixed A set of key params, may be an array or a string, or an ICachable instance. If NULL, all is cleared - */ - public function clear($cache_keys = NULL) { - if (empty($cache_keys)) { - $this->do_clear_all(); - } - else { - $this->do_clear($cache_keys); - } - } - - /** - * Clear all cache - */ - protected function do_clear_all() { - $app_key = $this->get_app_key(); - xcache_unset_by_prefix($app_key); - } - - /** - * Clear cache for given cache key(s) - */ - protected function do_clear($cache_keys) { - $key = $this->flatten_keys($cache_keys, false); - xcache_unset($key); - xcache_unset_by_prefix($key . '_g$c'); - } - - /** - * Return key to make the current app unique - * - * @return string - */ - protected function get_app_key() { - return 'g$c' . Config::get_url(Config::URL_DOMAIN) . ''; - } - - /** - * Strip empty keys from end of $cache_keys - */ - protected function preprocess_keys($cache_keys, $strip_empty = true) { - $cleaned = array($this->get_app_key()); - foreach(Arr::force($cache_keys, false) as $key) { - if ($key || $key == '0') { - $cleaned[] = $key; - } else if ($strip_empty) { - break; - } else { - $cleaned[] = "{empty}"; - } - } - return $cleaned; - } - - /** - * Transform the given param into a key string - * - * @param $cache_keys - * @param bool $strip_empty - * @return string - */ - protected function flatten_keys($cache_keys, $strip_empty = true) { - $cache_keys = $this->preprocess_keys($cache_keys, $strip_empty); - $ret = implode('_g$c', $cache_keys); - return $ret; - } - - /** - * Removes expired cache entries - */ - public function remove_expired() { - // Nothing to do, xcache does this for us - } - -} \ No newline at end of file diff --git a/contributions/cache.xcache/cache.xcache12.impl.php b/contributions/cache.xcache/cache.xcache12.impl.php deleted file mode 100644 index 3fbd207a..00000000 --- a/contributions/cache.xcache/cache.xcache12.impl.php +++ /dev/null @@ -1,88 +0,0 @@ -do_clear(array()); - } - - /** - * Clear cache for given cache key(s) - */ - protected function do_clear($cache_keys) { - // We have do do a clear on - // - App Key - // - Cache Keys - // - * - // This means we increment namespace of last key - // But first, strip of empty keys from the end of the array - $cleaned = $this->preprocess_keys($cache_keys, false); - $ns = $this->get_keys_namespaces($cleaned); - $n = array_pop($ns); - if ($n) { - // See http://code.google.com/p/memcached/wiki/FAQ#Deleting%5Fby%5FNamespace - // for how this trick works - xcache_inc($n, 1); - } - } - - /** - * Transform the given param into a key string - * - * @param Mixed A set of key params, may be an array or a string - */ - protected function flatten_keys($cache_keys, $strip_empty = true) { - $cache_keys = $this->preprocess_keys($cache_keys); - $ns_keys = $this->get_keys_namespaces($cache_keys); - - $tmp = array(); - foreach($cache_keys as $key) { - $tmp[] = $key . ':=' . $this->get_namespace_value(array_shift($ns_keys)); - } - - return implode('_', $tmp); - } - - /** - * Return array of namespaces for keys - * - * See http://code.google.com/p/memcached/wiki/FAQ#Deleting%5Fby%5FNamespace - * - * @param Mixed A set of key params, may be an array or a string - * @return array - */ - protected function get_keys_namespaces($cache_keys) { - $ret = array(); - foreach(Arr::force($cache_keys, true) as $key) { - $ns_key .= 'g$ns' . $key; - $ret[] = $ns_key; - } - return $ret; - } - - /** - * Get value of namespace counter - * - * @param string $ns Namespace - */ - protected function get_namespace_value($ns) { - if (xcache_isset($ns)) { - $ret = xcache_get($ns); - } - else { - // This should be a transacton - $ret = rand(1, 1000); - xcache_set($ns, $ret); - } - return $ret; - } -} \ No newline at end of file diff --git a/contributions/cache.xcache/cache.xcache13.impl.php b/contributions/cache.xcache/cache.xcache13.impl.php deleted file mode 100644 index 2d43509e..00000000 --- a/contributions/cache.xcache/cache.xcache13.impl.php +++ /dev/null @@ -1,11 +0,0 @@ -merge('XCache is not enabled - XCache cache persister will not work!'); - } - return $ret; -} diff --git a/contributions/cache.xcache/license.txt b/contributions/cache.xcache/license.txt deleted file mode 100644 index 4e721a95..00000000 --- a/contributions/cache.xcache/license.txt +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2010 Gerd Riesselmann - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - \ No newline at end of file diff --git a/contributions/cache.xcache/session.xcache.impl.php b/contributions/cache.xcache/session.xcache.impl.php deleted file mode 100644 index b20524cf..00000000 --- a/contributions/cache.xcache/session.xcache.impl.php +++ /dev/null @@ -1,79 +0,0 @@ -gc(ini_get('session.gc_maxlifetime')); - return true; - } - - /** - * Load session data from xcache - */ - public function read($key) { - // Write and Close handlers are called after destructing objects since PHP 5.0.5 - // Thus destructors can use sessions but session handler can't use objects. - // So we are moving session closure before destructing objects. - register_shutdown_function('session_write_close'); - $key = $this->create_key($key); - if (xcache_isset($key)) { - return xcache_get($key); - } - return ''; - } - - /** - * Write session data to XCache - */ - public function write($key, $value) { - try { - xcache_set($this->create_key($key), $value, ini_get('session.gc_maxlifetime')); - return true; - } - catch(Exception $ex) { - return false; - } - } - - /** - * Delete a session - */ - public function destroy($key) { - xcache_unset($this->create_key($key)); - return true; - } - - /** - * Delete outdated sessions - */ - public function gc($lifetime) { - // XCache does this for us - return true; - } - - protected function create_key($key) { - return 'g$s' . Config::get_url(Config::URL_DOMAIN) . '_' . $key; - } -} diff --git a/contributions/cache.xcache/start.inc.php b/contributions/cache.xcache/start.inc.php deleted file mode 100644 index 3db8d15c..00000000 --- a/contributions/cache.xcache/start.inc.php +++ /dev/null @@ -1,25 +0,0 @@ -: resolve_path($tpl)); } else { diff --git a/contributions/deletedialog/view/templates/default/deletedialog/inc/message.tpl.php b/contributions/deletedialog/view/templates/default/deletedialog/inc/message.tpl.php index abd77993..ac673eca 100644 --- a/contributions/deletedialog/view/templates/default/deletedialog/inc/message.tpl.php +++ b/contributions/deletedialog/view/templates/default/deletedialog/inc/message.tpl.php @@ -1,5 +1,6 @@ get_table_name(); +$safe_table_name = basename(str_replace(array('/', '\\', '..'), '', $instance->get_table_name())); +$test = 'deletedialog/messages/' . $safe_table_name; If (TemplatePathResolver::exists($test)) { include($this->resolve_path($test)); } diff --git a/contributions/deletedialog/view/templates/default/deletedialog/inc/status/message.tpl.php b/contributions/deletedialog/view/templates/default/deletedialog/inc/status/message.tpl.php index abd77993..ac673eca 100644 --- a/contributions/deletedialog/view/templates/default/deletedialog/inc/status/message.tpl.php +++ b/contributions/deletedialog/view/templates/default/deletedialog/inc/status/message.tpl.php @@ -1,5 +1,6 @@ get_table_name(); +$safe_table_name = basename(str_replace(array('/', '\\', '..'), '', $instance->get_table_name())); +$test = 'deletedialog/messages/' . $safe_table_name; If (TemplatePathResolver::exists($test)) { include($this->resolve_path($test)); } diff --git a/contributions/javascript.cleditor/behaviour/base/javascript.cleditor.eventsink.php b/contributions/javascript.cleditor/behaviour/base/javascript.cleditor.eventsink.php deleted file mode 100644 index e4d3cefb..00000000 --- a/contributions/javascript.cleditor/behaviour/base/javascript.cleditor.eventsink.php +++ /dev/null @@ -1,42 +0,0 @@ - $config) { - $compressed_name = 'cleditor.' . $name; - if ($config->lang) { - $result[$compressed_name][] = 'js/cleditor/lang/jquery.cleditor.' . strtolower($config->lang) . '.js'; - } - $result[$compressed_name][] = 'js/cleditor/jquery.cleditor.js'; - foreach($config->plugins as $p) { - $result[$compressed_name][] = $p; - } - $result[$compressed_name][] = $config->init_file; - } - break; - case JCSSManager::TYPE_CSS: - $result[] = 'js/cleditor/jquery.cleditor.css'; - break; - } - } - } -} diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/default.js b/contributions/javascript.cleditor/data/1.3/js/cleditor/default.js deleted file mode 100644 index 98428126..00000000 --- a/contributions/javascript.cleditor/data/1.3/js/cleditor/default.js +++ /dev/null @@ -1,36 +0,0 @@ -/* See http://premiumsoftware.net/cleditor/docs/GettingStarted.html for more details about how to start CLEditor */ -$(document).ready(function() { - $("textarea.rte").cleditor({ - width: 500, // width not including margins, borders or padding - height: 250, // height not including margins, borders or padding - controls: // controls to add to the toolbar - "bold italic underline strikethrough subscript superscript | font size " + - "style | color highlight removeformat | bullets numbering | outdent " + - "indent | alignleft center alignright justify | undo redo | " + - "rule image link unlink | cut copy paste pastetext | print source", - colors: // colors in the color popup - "FFF FCC FC9 FF9 FFC 9F9 9FF CFF CCF FCF " + - "CCC F66 F96 FF6 FF3 6F9 3FF 6FF 99F F9F " + - "BBB F00 F90 FC6 FF0 3F3 6CC 3CF 66C C6C " + - "999 C00 F60 FC3 FC0 3C0 0CC 36F 63F C3C " + - "666 900 C60 C93 990 090 399 33F 60C 939 " + - "333 600 930 963 660 060 366 009 339 636 " + - "000 300 630 633 330 030 033 006 309 303", - fonts: // font names in the font popup - "Arial,Arial Black,Comic Sans MS,Courier New,Narrow,Garamond," + - "Georgia,Impact,Sans Serif,Serif,Tahoma,Trebuchet MS,Verdana", - sizes: // sizes in the font size popup - "1,2,3,4,5,6,7", - styles: // styles in the style popup - [["Paragraph", "

"], ["Header 1", "

"], ["Header 2", "

"], - ["Header 3", "

"], ["Header 4","

"], ["Header 5","

"], - ["Header 6","
"]], - useCSS: false, // use CSS to style HTML when possible (not supported in ie) - docType: // Document type contained within the editor - '', - docCSSFile: // CSS file used to style the document contained within the editor - "", - bodyStyle: // style to assign to document body contained within the editor - "margin:4px; font:10pt Arial,Verdana; cursor:text" - }); - }); \ No newline at end of file diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/buttons.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/buttons.gif deleted file mode 100644 index 2e464d0c..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/buttons.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/buttons.png b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/buttons.png deleted file mode 100644 index a329cad1..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/buttons.png and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/1.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/1.gif deleted file mode 100644 index 9f9f923f..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/1.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/10.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/10.gif deleted file mode 100644 index eea026a6..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/10.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/11.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/11.gif deleted file mode 100644 index e26144c9..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/11.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/12.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/12.gif deleted file mode 100644 index e9eb35bc..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/12.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/2.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/2.gif deleted file mode 100644 index 7087f48e..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/2.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/3.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/3.gif deleted file mode 100644 index 6db9d411..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/3.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/4.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/4.gif deleted file mode 100644 index 4e399bf9..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/4.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/5.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/5.gif deleted file mode 100644 index f6e1b995..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/5.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/6.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/6.gif deleted file mode 100644 index 621bf47e..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/6.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/7.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/7.gif deleted file mode 100644 index 44bf7f79..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/7.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/8.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/8.gif deleted file mode 100644 index ab44a75f..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/8.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/9.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/9.gif deleted file mode 100644 index 5a1630eb..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/9.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/icons.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/icons.gif deleted file mode 100644 index b579921b..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/icons/icons.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/quote.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/quote.gif deleted file mode 100644 index 84dc007f..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/quote.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/smilies.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/smilies.gif deleted file mode 100644 index 5b967b79..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/smilies.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/table.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/table.gif deleted file mode 100644 index 4af3725b..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/table.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/toolbar.gif b/contributions/javascript.cleditor/data/1.3/js/cleditor/images/toolbar.gif deleted file mode 100644 index e6eb2da5..00000000 Binary files a/contributions/javascript.cleditor/data/1.3/js/cleditor/images/toolbar.gif and /dev/null differ diff --git a/contributions/javascript.cleditor/data/1.3/js/cleditor/jquery.cleditor.bbcode.js b/contributions/javascript.cleditor/data/1.3/js/cleditor/jquery.cleditor.bbcode.js deleted file mode 100644 index 1fcb78c3..00000000 --- a/contributions/javascript.cleditor/data/1.3/js/cleditor/jquery.cleditor.bbcode.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - @preserve CLEditor BBCode Plugin v1.0.0 - http://premiumsoftware.net/cleditor - requires CLEditor v1.3.0 or later - - Copyright 2010, Chris Landowski, Premium Software, LLC - Dual licensed under the MIT or GPL Version 2 licenses. -*/ - -// ==ClosureCompiler== -// @compilation_level SIMPLE_OPTIMIZATIONS -// @output_file_name jquery.cleditor.bbcode.min.js -// ==/ClosureCompiler== - -/* - - The CLEditor useCSS optional parameter should be set to false for this plugin - to function properly. - - Supported HTML and BBCode Tags: - - Bold Hello - [b]Hello[/b] - Italics Hello - [i]Hello[/i] - Underlined Hello - [u]Hello[/u] - Strikethrough Hello - [s]Hello[/s] - Unordered Lists - [list][*]Red[/*][*]Green[/*][*]Blue[/*][/list] - Ordered Lists
  1. Red
  2. Blue
  3. Green
- [list=1][*]Red[/*][*]Green[/*][*]Blue[/*][/list] - Images - [img]http://premiumsoftware.net/image.jpg[/img] - Links Premium Software - [url=http://premiumsoftware.net]Premium Software[/url] - -*/ - -(function($) { - - // BBCode only supports a small subset of HTML, so remove - // any toolbar buttons that are not currently supported. - $.cleditor.defaultOptions.controls = - "bold italic underline strikethrough removeformat | bullets numbering | " + - "undo redo | image link unlink | cut copy paste pastetext | print source"; - - // Save the previously assigned callback handlers - var oldAreaCallback = $.cleditor.defaultOptions.updateTextArea; - var oldFrameCallback = $.cleditor.defaultOptions.updateFrame; - - // Wireup the updateTextArea callback handler - $.cleditor.defaultOptions.updateTextArea = function(html) { - - // Fire the previously assigned callback handler - if (oldAreaCallback) - html = oldAreaCallback(html); - - // Convert the HTML to BBCode - return $.cleditor.convertHTMLtoBBCode(html); - - } - - // Wireup the updateFrame callback handler - $.cleditor.defaultOptions.updateFrame = function(code) { - - // Fire the previously assigned callback handler - if (oldFrameCallback) - code = oldFrameCallback(code); - - // Convert the BBCode to HTML - return $.cleditor.convertBBCodeToHTML(code); - - } - - // Expose the convertHTMLtoBBCode method - $.cleditor.convertHTMLtoBBCode = function(html) { - - $.each([ - [/[\r|\n]/g, ""], - [//gi, "\n"], - [/(.*?)<\/b>/gi, "[b]$1[/b]"], - [/(.*?)<\/strong>/gi, "[b]$1[/b]"], - [/(.*?)<\/i>/gi, "[i]$1[/i]"], - [/(.*?)<\/em>/gi, "[i]$1[/i]"], - [/(.*?)<\/u>/gi, "[u]$1[/u]"], - [/(.*?)<\/ins>/gi, "[u]$1[/u]"], - [/(.*?)<\/strike>/gi, "[s]$1[/s]"], - [/(.*?)<\/del>/gi, "[s]$1[/s]"], - [/(.*?)<\/a>/gi, "[url=$1]$2[/url]"], - [//gi, "[img]$1[/img]"], - [/
-
+
-
+
diff --git a/contributions/punycode/3rdparty/idna_convert/uctc.php b/contributions/punycode/3rdparty/idna_convert/uctc.php index ea5e4769..24ed9a67 100644 --- a/contributions/punycode/3rdparty/idna_convert/uctc.php +++ b/contributions/punycode/3rdparty/idna_convert/uctc.php @@ -39,8 +39,8 @@ public static function convert($data, $from, $to, $safe_mode = false, $safe_char if (self::$safe_mode) self::$allow_overlong = true; if (!in_array($from, self::$mechs)) throw new Exception('Invalid input format specified'); if (!in_array($to, self::$mechs)) throw new Exception('Invalid output format specified'); - if ($from != 'ucs4array') eval('$data = self::'.$from.'_ucs4array($data);'); - if ($to != 'ucs4array') eval('$data = self::ucs4array_'.$to.'($data);'); + if ($from != 'ucs4array') $data = call_user_func(array('self', $from.'_ucs4array'), $data); + if ($to != 'ucs4array') $data = call_user_func(array('self', 'ucs4array_'.$to), $data); return $data; } diff --git a/contributions/punycode/lib/helpers/converters/punycode.converter.php b/contributions/punycode/lib/helpers/converters/punycode.converter.php index 5f6244f8..7623b6e7 100644 --- a/contributions/punycode/lib/helpers/converters/punycode.converter.php +++ b/contributions/punycode/lib/helpers/converters/punycode.converter.php @@ -8,7 +8,7 @@ * @ingroup Punycode */ class ConverterPunycode implements IConverter { - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { $inst = $this->create_converter(); $ret = false; if ($inst) { @@ -17,7 +17,7 @@ public function encode($value, $params = false) { return $ret; } - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { $inst = $this->create_converter(); $ret = false; if ($inst) { diff --git a/contributions/sphinx/model/drivers/sphinx/dbdriver.sphinx.php b/contributions/sphinx/model/drivers/sphinx/dbdriver.sphinx.php index 00711763..befbf0e1 100644 --- a/contributions/sphinx/model/drivers/sphinx/dbdriver.sphinx.php +++ b/contributions/sphinx/model/drivers/sphinx/dbdriver.sphinx.php @@ -202,7 +202,7 @@ public function execute($sql) { public function query($query) { $this->connect(); - $arr_query = unserialize($query); + $arr_query = unserialize($query, ['allowed_classes' => false]); $features = Arr::get_item($arr_query, 'features', false); $terms = Arr::get_item_recursive($arr_query, 'conditions[query]', ''); diff --git a/contributions/sphinx/model/drivers/sphinx/dbresultset.count.sphinx.php b/contributions/sphinx/model/drivers/sphinx/dbresultset.count.sphinx.php index f4a79cc5..a1f15cc9 100644 --- a/contributions/sphinx/model/drivers/sphinx/dbresultset.count.sphinx.php +++ b/contributions/sphinx/model/drivers/sphinx/dbresultset.count.sphinx.php @@ -7,7 +7,7 @@ * @ingroup Sphinx */ class DBResultSetCountSphinx extends DBResultSetSphinx { - protected $done = false; + protected bool $done = false; /** * Returns row as associative array diff --git a/contributions/sphinx/model/drivers/sphinx/dbresultset.sphinx.php b/contributions/sphinx/model/drivers/sphinx/dbresultset.sphinx.php index c3ca11b4..6582974b 100644 --- a/contributions/sphinx/model/drivers/sphinx/dbresultset.sphinx.php +++ b/contributions/sphinx/model/drivers/sphinx/dbresultset.sphinx.php @@ -1,7 +1,7 @@ result = $result; $this->status = $status; } - - /** - * Closes internal cursor - * - * @return void - */ - public function close() { + + public function close(): void { $this->result = null; } - - /** - * Returns number of columns in result set - * - * @return int - */ - public function get_column_count() { + + public function get_column_count(): int { return 0; } - - /** - * Returns number of rows in result set - * - * @return int - */ - public function get_row_count() { + + public function get_row_count(): int { $ret = 0; if ($this->result) { $ret = $this->result['total']; } return $ret; } - - /** - * Returns row as associative array - * - * @return array | bool False if no more data is available - */ - public function fetch() { + + public function fetch(): array|false { $ret = false; if ($this->result) { $record = each($this->result['matches']); @@ -65,7 +45,7 @@ public function fetch() { } return $ret; } - + protected function read_record($arr_record) { $ret = array(); foreach($arr_record as $key => $value) { @@ -74,17 +54,12 @@ protected function read_record($arr_record) { } else { $ret[$key] = $value; - } + } } return $ret; } - - /** - * Returns status - * - * @param Status - */ - public function get_status() { + + public function get_status(): Status { return $this->status; } } diff --git a/contributions/text.htmlpurifier/lib/helpers/converters/htmlpurifier.converter.php b/contributions/text.htmlpurifier/lib/helpers/converters/htmlpurifier.converter.php index 4e67a212..60da8a17 100644 --- a/contributions/text.htmlpurifier/lib/helpers/converters/htmlpurifier.converter.php +++ b/contributions/text.htmlpurifier/lib/helpers/converters/htmlpurifier.converter.php @@ -12,7 +12,7 @@ class ConverterHtmlPurifier implements IConverter { * @param string $value * @param array See http://htmlpurifier.org/live/configdoc/plain.html for all possible values */ - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { require_once Load::get_module_dir('text.htmlpurifier') . '3rdparty/htmlpurifier-4/HTMLPurifier.standalone.php'; $config = HTMLPurifier_Config::createDefault(); @@ -33,7 +33,7 @@ public function encode($value, $params = false) { /** * This function does nothing! Especially it does NOT purify HTML! */ - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { return $value; } } diff --git a/contributions/text.placeholders/lib/helpers/converters/textplaceholders.converter.php b/contributions/text.placeholders/lib/helpers/converters/textplaceholders.converter.php index 386a92cd..5e669038 100644 --- a/contributions/text.placeholders/lib/helpers/converters/textplaceholders.converter.php +++ b/contributions/text.placeholders/lib/helpers/converters/textplaceholders.converter.php @@ -12,14 +12,14 @@ class ConverterTextPlaceholders implements IConverter { * @param string $value * @param array See http://htmlpurifier.org/live/configdoc/plain.html for all possible values */ - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { return TextPlaceholders::apply($value); } /** * This function does nothing! Especially it does NOT purify HTML! */ - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { return $value; } } diff --git a/contributions/text.unidecode/lib/helpers/converters/unidecode.converter.php b/contributions/text.unidecode/lib/helpers/converters/unidecode.converter.php index f686d241..2076790b 100644 --- a/contributions/text.unidecode/lib/helpers/converters/unidecode.converter.php +++ b/contributions/text.unidecode/lib/helpers/converters/unidecode.converter.php @@ -6,7 +6,7 @@ * @ingroup Unidecode */ class ConverterUnidecode implements IConverter { - private static $groups = array(); + private static array $groups = array(); /** * Convert Unicode chars to ASCII transliterals @@ -14,7 +14,7 @@ class ConverterUnidecode implements IConverter { * @param string $value * @param string Encoding of $value, if different from current GyroLocale */ - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { // We need if (empty($params)) { $params = GyroLocale::get_charset(); @@ -28,7 +28,7 @@ public function encode($value, $params = false) { /** * Using "ConverterUnidecode::encode() may be confusing, so let decode8) just do the same */ - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { return $this->encode($value, $params); } diff --git a/contributions/usermanagement.notifications/model/classes/notificationssettings.model.php b/contributions/usermanagement.notifications/model/classes/notificationssettings.model.php index ca32d6a1..b30edb40 100644 --- a/contributions/usermanagement.notifications/model/classes/notificationssettings.model.php +++ b/contributions/usermanagement.notifications/model/classes/notificationssettings.model.php @@ -140,7 +140,7 @@ public function source_matches($source, $type) { */ protected function create_feed_token() { $user = Users::get($this->id_user); - $seed = rand(1000000, 9999999); + $seed = random_int(1000000, 9999999); if ($user) { $seed .= $user->password . $user->creationdate; } diff --git a/contributions/usermanagement/behaviour/commands/users/hashes/bcryp.hash.php b/contributions/usermanagement/behaviour/commands/users/hashes/bcryp.hash.php new file mode 100644 index 00000000..ae14db82 --- /dev/null +++ b/contributions/usermanagement/behaviour/commands/users/hashes/bcryp.hash.php @@ -0,0 +1,36 @@ + 12)); + } + + /** + * Validate if given hash matches source using password_verify() + * + * Timing-safe comparison via password_verify(). + * + * @param string $source + * @param string $hash + * @return bool + */ + public function check(string $source, string $hash): bool { + return password_verify($source, $hash); + } +} diff --git a/contributions/usermanagement/behaviour/commands/users/hashes/bcrypt.hash.php b/contributions/usermanagement/behaviour/commands/users/hashes/bcrypt.hash.php new file mode 100644 index 00000000..fbea5abd --- /dev/null +++ b/contributions/usermanagement/behaviour/commands/users/hashes/bcrypt.hash.php @@ -0,0 +1,16 @@ + 12]); + } + + public function check(string $source, string $hash): bool { + return password_verify($source, $hash); + } +} diff --git a/contributions/usermanagement/behaviour/commands/users/hashes/md5.hash.php b/contributions/usermanagement/behaviour/commands/users/hashes/md5.hash.php index 07287c15..ad2784dd 100644 --- a/contributions/usermanagement/behaviour/commands/users/hashes/md5.hash.php +++ b/contributions/usermanagement/behaviour/commands/users/hashes/md5.hash.php @@ -8,11 +8,11 @@ * @ingroup Usermanagement */ class Md5Hash implements IHashAlgorithm { - public function hash($source) { + public function hash(string $source): string { return md5($source); } - public function check($source, $hash) { - return $hash == $this->hash($source); + public function check(string $source, string $hash): bool { + return hash_equals($hash, $this->hash($source)); } } \ No newline at end of file diff --git a/contributions/usermanagement/behaviour/commands/users/hashes/pas2p.hash.php b/contributions/usermanagement/behaviour/commands/users/hashes/pas2p.hash.php index ecd7adda..06d8a1bb 100644 --- a/contributions/usermanagement/behaviour/commands/users/hashes/pas2p.hash.php +++ b/contributions/usermanagement/behaviour/commands/users/hashes/pas2p.hash.php @@ -19,12 +19,12 @@ protected function create_pass2_instance() { return new PasswordHash02(8, TRUE); } - public function hash($source) { + public function hash(string $source): string { $o_hash = $this->create_pass2_instance(); return $o_hash->HashPassword($source); } - public function check($source, $hash) { + public function check(string $source, string $hash): bool { $o_hash = $this->create_pass2_instance(); return $o_hash->CheckPassword($source, $hash); } diff --git a/contributions/usermanagement/behaviour/commands/users/hashes/pas3p.hash.php b/contributions/usermanagement/behaviour/commands/users/hashes/pas3p.hash.php index e380ced7..dc6876b3 100644 --- a/contributions/usermanagement/behaviour/commands/users/hashes/pas3p.hash.php +++ b/contributions/usermanagement/behaviour/commands/users/hashes/pas3p.hash.php @@ -19,12 +19,12 @@ protected function create_pass3_instance() { return new PasswordHash03(8, TRUE); } - public function hash($source) { + public function hash(string $source): string { $o_hash = $this->create_pass3_instance(); return $o_hash->HashPassword($source); } - public function check($source, $hash) { + public function check(string $source, string $hash): bool { $o_hash = $this->create_pass3_instance(); return $o_hash->CheckPassword($source, $hash); } diff --git a/contributions/usermanagement/behaviour/commands/users/hashes/sha1.hash.php b/contributions/usermanagement/behaviour/commands/users/hashes/sha1.hash.php index 3e02b381..61299ee7 100644 --- a/contributions/usermanagement/behaviour/commands/users/hashes/sha1.hash.php +++ b/contributions/usermanagement/behaviour/commands/users/hashes/sha1.hash.php @@ -8,11 +8,11 @@ * @ingroup Usermanagement */ class Sha1Hash implements IHashAlgorithm { - public function hash($source) { + public function hash(string $source): string { return sha1($source); } - public function check($source, $hash) { - return $hash == $this->hash($source); + public function check(string $source, string $hash): bool { + return hash_equals($hash, $this->hash($source)); } } diff --git a/contributions/usermanagement/lib/interfaces/ihash.cls.php b/contributions/usermanagement/lib/interfaces/ihash.cls.php index 323f3745..498e8e0e 100644 --- a/contributions/usermanagement/lib/interfaces/ihash.cls.php +++ b/contributions/usermanagement/lib/interfaces/ihash.cls.php @@ -23,14 +23,14 @@ interface IHashAlgorithm { * @param string $source * @return string */ - public function hash($source); - + public function hash(string $source): string; + /** - * Validate if given hash matches source - * + * Validate if given hash matches source + * * @param string $source * @param string $hash * @return bool */ - public function check($source, $hash); + public function check(string $source, string $hash): bool; } \ No newline at end of file diff --git a/contributions/usermanagement/model/classes/users.model.php b/contributions/usermanagement/model/classes/users.model.php index fb15f1ee..31a7fb30 100644 --- a/contributions/usermanagement/model/classes/users.model.php +++ b/contributions/usermanagement/model/classes/users.model.php @@ -29,7 +29,7 @@ protected function create_table_object() { new DBFieldText('name', 100, null, DBField::NOT_NULL), new DBFieldTextEmail('email', null, DBField::NOT_NULL), new DBFieldText('password', 100, null, DBField::NOT_NULL), - new DBFieldText('hash_type', 5, 'md5', DBField::NOT_NULL | DBField::INTERNAL), + new DBFieldText('hash_type', 5, 'bcryp', DBField::NOT_NULL | DBField::INTERNAL), new DBFieldDateTime('emailconfirmationdate', null, DBField::NONE | DBField::INTERNAL), new DBFieldEnum('emailstatus', array_keys(Users::get_email_statuses()), Users::EMAIL_STATUS_UNCONFIRMED, DBField::NOT_NULL | DBField::INTERNAL), new DBFieldInt('tos_version', 0, DBFieldInt::UNSIGNED | DBField::NOT_NULL | DBField::INTERNAL), diff --git a/contributions/usermanagement/start.inc.php b/contributions/usermanagement/start.inc.php index 2417bb30..b5e7b2f0 100644 --- a/contributions/usermanagement/start.inc.php +++ b/contributions/usermanagement/start.inc.php @@ -217,7 +217,7 @@ class ConfigUsermanagement { Config::set_value_from_constant(ConfigUsermanagement::DEFAULT_PAGE, 'APP_USER_DEFAULT_PAGE', Config::get_url(Config::URL_BASEURL_SAFE) . 'user'); Config::set_value_from_constant(ConfigUsermanagement::DEFAULT_ROLE, 'APP_USER_DEFAULT_ROLE', USER_ROLE_USER); Config::set_value_from_constant(ConfigUsermanagement::USER_403_BEHAVIOUR, 'APP_USER_403_BEHAVIOUR', 'DENY'); -Config::set_value_from_constant(ConfigUsermanagement::HASH_TYPE, 'APP_USER_HASH_TYPE', 'pas3p'); +Config::set_value_from_constant(ConfigUsermanagement::HASH_TYPE, 'APP_USER_HASH_TYPE', 'bcryp'); Config::set_value_from_constant(ConfigUsermanagement::PERMANENT_LOGIN_DURATION, 'APP_USER_PERMANENT_LOGIN_DURATION', 14); Config::set_value_from_constant(ConfigUsermanagement::TOS_VERSION, 'APP_USER_TOS_VERSION', 0); Config::set_value_from_constant(ConfigUsermanagement::CACHEHEADER_CLASS_LOGGEDIN, 'APP_USER_CACHEHEADER_CLASS_LOGGEDIN', 'PrivateRigidEtagOnly'); diff --git a/gyro/core/cli/bootstrap.cli.php b/gyro/core/cli/bootstrap.cli.php new file mode 100644 index 00000000..f33fcdaf --- /dev/null +++ b/gyro/core/cli/bootstrap.cli.php @@ -0,0 +1,89 @@ +get_name(); + } + + /** + * Execute the command + * + * @param array $args Parsed arguments + * @return int Exit code (0 = success) + */ + abstract public function execute(array $args): int; + + // ----------------------------------------------- + // Output helpers + // ----------------------------------------------- + + protected function writeln(string $text): void { + echo $text . PHP_EOL; + } + + protected function error(string $text): void { + fwrite(STDERR, "\033[31mError:\033[0m $text" . PHP_EOL); + } + + protected function success(string $text): void { + echo "\033[32m$text\033[0m" . PHP_EOL; + } + + protected function warning(string $text): void { + echo "\033[33m$text\033[0m" . PHP_EOL; + } + + protected function info(string $text): void { + echo "\033[36m$text\033[0m" . PHP_EOL; + } +} diff --git a/gyro/core/cli/clikernel.cls.php b/gyro/core/cli/clikernel.cls.php new file mode 100644 index 00000000..0f74f16a --- /dev/null +++ b/gyro/core/cli/clikernel.cls.php @@ -0,0 +1,126 @@ +commands[$command->get_name()] = $command; + } + + /** + * Get all registered commands + * + * @return CLICommand[] + */ + public function get_commands(): array { + return $this->commands; + } + + /** + * Run the CLI with given argv + * + * @param array $argv Command-line arguments + * @return int Exit code (0 = success) + */ + public function run(array $argv): int { + $script = array_shift($argv); // Remove script name + + if (empty($argv) || in_array($argv[0], array('--help', '-h'))) { + return $this->run_command('help', array()); + } + + if (in_array($argv[0], array('--version', '-v'))) { + $this->writeln('Gyro CLI ' . self::VERSION); + return 0; + } + + $command_name = array_shift($argv); + + // Parse --flags and positional args + $args = self::parse_args($argv); + + return $this->run_command($command_name, $args); + } + + /** + * Run a specific command by name + */ + private function run_command(string $name, array $args): int { + if (!isset($this->commands[$name])) { + $this->error("Unknown command: $name"); + $this->writeln("Run 'gyro help' for a list of commands."); + return 1; + } + + try { + return $this->commands[$name]->execute($args); + } catch (\Exception $e) { + $this->error($e->getMessage()); + return 1; + } + } + + /** + * Parse argv into associative array + * + * Supports: --key=value, --flag, positional args (numeric keys) + * + * @return array + */ + public static function parse_args(array $argv): array { + $args = array(); + $positional = 0; + + foreach ($argv as $arg) { + if (str_starts_with($arg, '--')) { + $arg = substr($arg, 2); + if (str_contains($arg, '=')) { + list($key, $value) = explode('=', $arg, 2); + $args[$key] = $value; + } else { + $args[$arg] = true; + } + } else { + $args[$positional] = $arg; + $positional++; + } + } + + return $args; + } + + // ----------------------------------------------- + // Output helpers (static for use from commands) + // ----------------------------------------------- + + public function writeln(string $text): void { + echo $text . PHP_EOL; + } + + public function error(string $text): void { + fwrite(STDERR, "\033[31mError:\033[0m $text" . PHP_EOL); + } + + public function success(string $text): void { + echo "\033[32m$text\033[0m" . PHP_EOL; + } + + public function warning(string $text): void { + echo "\033[33m$text\033[0m" . PHP_EOL; + } + + public function info(string $text): void { + echo "\033[36m$text\033[0m" . PHP_EOL; + } +} diff --git a/gyro/core/cli/clitable.cls.php b/gyro/core/cli/clitable.cls.php new file mode 100644 index 00000000..b6e6651d --- /dev/null +++ b/gyro/core/cli/clitable.cls.php @@ -0,0 +1,88 @@ +headers = $headers; + } + + /** + * Add a row of data + * + * @param array $row Values in same order as headers + */ + public function add_row(array $row): void { + $this->rows[] = array_values($row); + } + + /** + * Render the table to string + */ + public function render(): string { + $col_count = count($this->headers); + $widths = array(); + + // Calculate column widths + for ($i = 0; $i < $col_count; $i++) { + $widths[$i] = mb_strlen($this->headers[$i]); + } + foreach ($this->rows as $row) { + for ($i = 0; $i < $col_count; $i++) { + $val = $row[$i] ?? ''; + $widths[$i] = max($widths[$i], mb_strlen((string)$val)); + } + } + + $lines = array(); + + // Separator + $sep = '+'; + for ($i = 0; $i < $col_count; $i++) { + $sep .= str_repeat('-', $widths[$i] + 2) . '+'; + } + + $lines[] = $sep; + + // Header row + $line = '|'; + for ($i = 0; $i < $col_count; $i++) { + $line .= ' ' . str_pad($this->headers[$i], $widths[$i]) . ' |'; + } + $lines[] = $line; + $lines[] = $sep; + + // Data rows + foreach ($this->rows as $row) { + $line = '|'; + for ($i = 0; $i < $col_count; $i++) { + $val = (string)($row[$i] ?? ''); + $line .= ' ' . str_pad($val, $widths[$i]) . ' |'; + } + $lines[] = $line; + } + + $lines[] = $sep; + + return implode(PHP_EOL, $lines); + } + + /** + * Print the table to stdout + */ + public function print(): void { + echo $this->render() . PHP_EOL; + } +} diff --git a/gyro/core/cli/commands/dbsynccommand.cli.php b/gyro/core/cli/commands/dbsynccommand.cli.php new file mode 100644 index 00000000..1d2f2f95 --- /dev/null +++ b/gyro/core/cli/commands/dbsynccommand.cli.php @@ -0,0 +1,264 @@ +]"; + } + + public function execute(array $args): int { + $execute = !empty($args['execute']); + $target_table = $args['table'] ?? null; + + // Check DB connection + try { + $driver = DB::get_connection(DB::DEFAULT_CONNECTION); + } catch (\Exception $e) { + $this->error('No database connection available.'); + $this->writeln('Configure APP_DB_* constants in .env or your config.'); + return 1; + } + + $models = ModelListCommand::discover_models(); + + if (empty($models)) { + $this->warning('No models found.'); + return 0; + } + + // Filter to specific table if requested + if ($target_table !== null) { + $models = array_filter($models, function ($m) use ($target_table) { + return $m['table'] === $target_table; + }); + if (empty($models)) { + $this->error("Table '$target_table' not found in models."); + return 1; + } + } + + $total_statements = array(); + $new_tables = array(); + $altered_tables = array(); + + foreach ($models as $model) { + $table_name = $model['table']; + $db_columns = $this->get_db_columns($table_name); + + if ($db_columns === false) { + // Table doesn't exist — generate CREATE TABLE + $sql = ModelShowCommand::generate_create_sql($model); + $new_tables[] = $table_name; + $total_statements[] = $sql; + continue; + } + + // Table exists — compare columns + $alter_parts = $this->compare_table($model, $db_columns); + + if (!empty($alter_parts)) { + $altered_tables[] = $table_name; + foreach ($alter_parts as $part) { + $total_statements[] = "ALTER TABLE `$table_name` $part;"; + } + } + } + + if (empty($total_statements)) { + $this->success('Database is in sync with models. No changes needed.'); + return 0; + } + + // Report + $this->writeln(''); + if (!empty($new_tables)) { + $this->info('New tables: ' . implode(', ', $new_tables)); + } + if (!empty($altered_tables)) { + $this->info('Tables to alter: ' . implode(', ', $altered_tables)); + } + $this->writeln(''); + + $this->writeln('Generated SQL:'); + $this->writeln(str_repeat('-', 60)); + foreach ($total_statements as $sql) { + $this->writeln($sql); + $this->writeln(''); + } + $this->writeln(str_repeat('-', 60)); + $this->writeln(count($total_statements) . ' statement(s).'); + + if ($execute) { + $this->writeln(''); + $this->warning('Executing...'); + $errors = 0; + foreach ($total_statements as $sql) { + $status = DB::execute($sql); + if ($status->is_error()) { + $this->error('Failed: ' . $status->to_string(Status::OUTPUT_PLAIN)); + $this->writeln(" SQL: $sql"); + $errors++; + } + } + if ($errors === 0) { + $this->success('All statements executed successfully.'); + } else { + $this->error("$errors statement(s) failed."); + return 1; + } + } else { + $this->writeln(''); + $this->writeln('This is a dry run. Use --execute to apply changes.'); + } + + return 0; + } + + /** + * Get columns from the actual database for a table + * + * @return array|false Array of column info, or false if table doesn't exist + */ + private function get_db_columns(string $table_name): array|false { + try { + $result = DB::query("SHOW COLUMNS FROM `$table_name`"); + if ($result->get_status()->is_error()) { + return false; + } + + $columns = array(); + while ($row = $result->fetch()) { + $columns[$row['Field']] = array( + 'type' => $row['Type'], + 'null' => $row['Null'] === 'YES', + 'default' => $row['Default'], + 'extra' => $row['Extra'] ?? '', + 'key' => $row['Key'] ?? '', + ); + } + + return empty($columns) ? false : $columns; + } catch (\Exception $e) { + return false; + } + } + + /** + * Compare model fields with DB columns and return ALTER TABLE parts + * + * @return array Array of ALTER TABLE clause strings (without the ALTER TABLE prefix) + */ + private function compare_table(array $model, array $db_columns): array { + $alter_parts = array(); + $prev_column = null; + + foreach ($model['fields'] as $name => $field) { + $col_sql = ModelShowCommand::field_to_sql($name, $field); + + if (!isset($db_columns[$name])) { + // Column missing — ADD + $position = ($prev_column !== null) ? " AFTER `$prev_column`" : ' FIRST'; + $alter_parts[] = "ADD COLUMN $col_sql$position"; + } else { + // Column exists — check if it differs + $diff = $this->column_differs($field, $db_columns[$name]); + if ($diff) { + $alter_parts[] = "MODIFY COLUMN $col_sql"; + } + } + $prev_column = $name; + } + + // Check for columns in DB that are NOT in the model + foreach ($db_columns as $col_name => $col_info) { + if (!isset($model['fields'][$col_name])) { + // Don't auto-drop — too dangerous. Just warn. + $alter_parts[] = "-- WARNING: Column `$col_name` exists in DB but not in model (not auto-dropped)"; + } + } + + return $alter_parts; + } + + /** + * Check if a model field definition differs from the DB column + */ + private function column_differs(IDBField $field, array $db_col): bool { + $db_type = strtoupper($db_col['type']); + $db_null = $db_col['null']; + $db_extra = strtoupper($db_col['extra'] ?? ''); + + // Check nullability + $model_null = $field->get_null_allowed(); + if ($model_null !== $db_null) { + return true; + } + + // Check auto_increment + if ($field instanceof DBFieldInt && $field->has_policy(DBFieldInt::AUTOINCREMENT)) { + if (!str_contains($db_extra, 'AUTO_INCREMENT')) { + return true; + } + } + + // Check unsigned + if (($field instanceof DBFieldInt || $field instanceof DBFieldFloat) && $field->has_policy(DBFieldInt::UNSIGNED)) { + if (!str_contains($db_type, 'UNSIGNED')) { + return true; + } + } + + // Basic type matching (simplified — exact type comparison is complex) + $expected_base = $this->get_expected_base_type($field); + if ($expected_base !== null && !str_contains($db_type, $expected_base)) { + return true; + } + + return false; + } + + /** + * Get expected base SQL type for a field + */ + private function get_expected_base_type(IDBField $field): ?string { + $class = get_class($field); + + if ($field instanceof DBFieldDateTime && $field->has_policy(DBFieldDateTime::TIMESTAMP)) { + return 'TIMESTAMP'; + } + + return match ($class) { + 'DBFieldInt' => 'INT', + 'DBFieldFloat' => 'DOUBLE', + 'DBFieldBool' => 'ENUM', + 'DBFieldDate' => 'DATE', + 'DBFieldDateTime' => 'DATETIME', + 'DBFieldTime' => 'TIME', + 'DBFieldBlob' => 'BLOB', + 'DBFieldSerialized' => 'TEXT', + default => null, // Text/Enum/Set — too many variations + }; + } +} diff --git a/gyro/core/cli/commands/helpcommand.cli.php b/gyro/core/cli/commands/helpcommand.cli.php new file mode 100644 index 00000000..821135fd --- /dev/null +++ b/gyro/core/cli/commands/helpcommand.cli.php @@ -0,0 +1,77 @@ +kernel = $kernel; + } + + public function get_name(): string { + return 'help'; + } + + public function get_description(): string { + return 'Show available commands or help for a specific command'; + } + + public function get_usage(): string { + return 'gyro help [command]'; + } + + public function execute(array $args): int { + // Help for a specific command? + if (isset($args[0])) { + return $this->show_command_help($args[0]); + } + + $this->writeln(''); + $this->info('Gyro CLI ' . CLIKernel::VERSION); + $this->writeln(''); + $this->writeln('Usage: gyro [options]'); + $this->writeln(''); + $this->writeln('Available commands:'); + $this->writeln(''); + + $table = new CLITable(array('Command', 'Description')); + foreach ($this->kernel->get_commands() as $command) { + $table->add_row(array($command->get_name(), $command->get_description())); + } + $table->print(); + + $this->writeln(''); + $this->writeln("Run 'gyro help ' for more details."); + $this->writeln(''); + + return 0; + } + + private function show_command_help(string $name): int { + $commands = $this->kernel->get_commands(); + + if (!isset($commands[$name])) { + $this->error("Unknown command: $name"); + return 1; + } + + $command = $commands[$name]; + $this->writeln(''); + $this->info($command->get_name()); + $this->writeln(' ' . $command->get_description()); + $this->writeln(''); + $this->writeln('Usage:'); + $this->writeln(' ' . $command->get_usage()); + $this->writeln(''); + + return 0; + } +} diff --git a/gyro/core/cli/commands/modellistcommand.cli.php b/gyro/core/cli/commands/modellistcommand.cli.php new file mode 100644 index 00000000..61527e17 --- /dev/null +++ b/gyro/core/cli/commands/modellistcommand.cli.php @@ -0,0 +1,216 @@ +warning('No models found.'); + return 0; + } + + if ($verbose) { + $table = new CLITable(array('Class', 'Table', 'Fields', 'Primary Key', 'Relations', 'Source')); + } else { + $table = new CLITable(array('Class', 'Table', 'Fields', 'Primary Key')); + } + + $count = 0; + foreach ($models as $info) { + $count++; + if ($verbose) { + $table->add_row(array( + $info['class'], + $info['table'], + $info['field_count'], + $info['primary_key'], + $info['relation_count'], + $info['source'], + )); + } else { + $table->add_row(array( + $info['class'], + $info['table'], + $info['field_count'], + $info['primary_key'], + )); + } + } + + $table->print(); + $this->writeln("$count model(s) found."); + + return 0; + } + + /** + * Discover all DAO model files and extract schema info. + * + * @return array Array of model info arrays + */ + public static function discover_models(): array { + $models = array(); + $model_files = self::find_model_files(); + + foreach ($model_files as $file) { + $info = self::load_model_info($file); + if ($info !== false) { + $models[] = $info; + } + } + + // Sort by table name + usort($models, function ($a, $b) { + return strcmp($a['table'], $b['table']); + }); + + return $models; + } + + /** + * Find all .model.php files in core, modules, and contributions + * + * @return array File paths + */ + public static function find_model_files(): array { + $files = array(); + $search_dirs = array( + GYRO_CORE_DIR . 'model/classes/', + ); + + // Add module directories + foreach (Load::get_loaded_modules() as $module) { + $dir = Load::get_module_dir($module); + if ($dir !== false) { + $search_dirs[] = $dir . 'model/classes/'; + } + } + + foreach ($search_dirs as $dir) { + if (is_dir($dir)) { + foreach (glob($dir . '*.model.php') as $file) { + $files[] = $file; + } + } + } + + return $files; + } + + /** + * Load a model file and extract schema information + * + * @param string $file Path to .model.php file + * @return array|false Model info or false on failure + */ + public static function load_model_info(string $file): array|false { + $filename = basename($file, '.model.php'); + + // Derive class name: DAO + CamelCase of filename + $classname = 'DAO' . Load::filename_to_classname($filename); + + // Load the file + $classes_before = get_declared_classes(); + try { + require_once $file; + } catch (\Exception $e) { + return false; + } + + // If derived classname doesn't exist, scan for new DAO* class from this file + if (!class_exists($classname, false)) { + $classes_after = get_declared_classes(); + $new_classes = array_diff($classes_after, $classes_before); + foreach ($new_classes as $cls) { + if (str_starts_with($cls, 'DAO') && is_subclass_of($cls, 'DataObjectBase')) { + $classname = $cls; + break; + } + } + } + + if (!class_exists($classname, false)) { + return false; + } + + // Try to instantiate and read schema + try { + $dao = new $classname(); + + if (!($dao instanceof DataObjectBase)) { + return false; + } + + /** @var DataObjectBase $dao */ + $fields = $dao->get_table_fields(); + $keys = $dao->get_table_keys(); + $relations = $dao->get_table_relations(); + $table_name = $dao->get_table_name(); + + $pk_names = array(); + foreach ($keys as $name => $field) { + $pk_names[] = $name; + } + + // Determine source (core, module, or contribution) + $source = 'core'; + if (str_contains($file, '/contributions/')) { + $source = self::extract_module_name($file, 'contributions'); + } elseif (str_contains($file, '/modules/')) { + $source = self::extract_module_name($file, 'modules'); + } + + return array( + 'class' => $classname, + 'table' => $table_name, + 'field_count' => count($fields), + 'primary_key' => implode(', ', $pk_names), + 'relation_count' => count($relations), + 'fields' => $fields, + 'keys' => $keys, + 'relations' => $relations, + 'source' => $source, + 'file' => $file, + ); + } catch (\Exception $e) { + return false; + } + } + + /** + * Extract module name from file path + */ + private static function extract_module_name(string $file, string $dir_name): string { + $pattern = '/' . preg_quote($dir_name, '/') . '\/([^\/]+)\//'; + if (preg_match($pattern, $file, $matches)) { + return $dir_name . '/' . $matches[1]; + } + return $dir_name; + } +} diff --git a/gyro/core/cli/commands/modelshowcommand.cli.php b/gyro/core/cli/commands/modelshowcommand.cli.php new file mode 100644 index 00000000..e65e49d7 --- /dev/null +++ b/gyro/core/cli/commands/modelshowcommand.cli.php @@ -0,0 +1,285 @@ +'; + } + + public function execute(array $args): int { + if (!isset($args[0])) { + $this->error('Please specify a table name.'); + $this->writeln('Usage: ' . $this->get_usage()); + return 1; + } + + $table_name = $args[0]; + + // Find the model + $models = ModelListCommand::discover_models(); + $found = null; + foreach ($models as $info) { + if ($info['table'] === $table_name || strtolower($info['class']) === 'dao' . strtolower($table_name)) { + $found = $info; + break; + } + } + + if ($found === null) { + $this->error("Model '$table_name' not found."); + $this->writeln('Run "gyro model:list" to see available models.'); + return 1; + } + + $this->writeln(''); + $this->info($found['class'] . ' -> ' . $found['table']); + $this->writeln('Source: ' . $found['file']); + $this->writeln(''); + + // Fields table + $this->writeln('Fields:'); + $table = new CLITable(array('Name', 'Type', 'Nullable', 'Default', 'Flags')); + + foreach ($found['fields'] as $name => $field) { + $type = self::get_field_type_label($field); + $nullable = $field->get_null_allowed() ? 'YES' : 'NO'; + $default = self::get_default_label($field); + $flags = self::get_flags_label($field); + $table->add_row(array($name, $type, $nullable, $default, $flags)); + } + $table->print(); + + // Primary Key + if (!empty($found['keys'])) { + $pk_names = array_keys($found['keys']); + $this->writeln('Primary Key: ' . implode(', ', $pk_names)); + } + + // Relations + if (!empty($found['relations'])) { + $this->writeln(''); + $this->writeln('Relations:'); + $rel_table = new CLITable(array('Target Table', 'Type', 'Fields')); + foreach ($found['relations'] as $relation) { + $type_label = self::get_relation_type_label($relation->get_type()); + $field_pairs = array(); + foreach ($relation->get_fields() as $field_rel) { + $field_pairs[] = $field_rel->get_source_field_name() . ' -> ' . $field_rel->get_target_field_name(); + } + $rel_table->add_row(array( + $relation->get_target_table_name(), + $type_label, + implode(', ', $field_pairs), + )); + } + $rel_table->print(); + } + + // SQL CREATE TABLE suggestion + $this->writeln(''); + $this->writeln('SQL (CREATE TABLE):'); + $this->writeln(self::generate_create_sql($found)); + + return 0; + } + + /** + * Get a human-readable type label for a field + */ + public static function get_field_type_label(IDBField $field): string { + $class = get_class($field); + $type = match ($class) { + 'DBFieldInt' => 'INT', + 'DBFieldText' => 'VARCHAR(' . ($field instanceof DBFieldText ? $field->get_length() : 255) . ')', + 'DBFieldTextEmail' => 'VARCHAR (email)', + 'DBFieldFloat' => 'FLOAT', + 'DBFieldBool' => 'BOOL', + 'DBFieldDate' => 'DATE', + 'DBFieldDateTime' => 'DATETIME', + 'DBFieldTime' => 'TIME', + 'DBFieldBlob' => 'BLOB', + 'DBFieldEnum' => 'ENUM', + 'DBFieldSet' => 'SET', + 'DBFieldSerialized' => 'TEXT (serialized)', + default => $class, + }; + + // Add UNSIGNED for int/float + if (($field instanceof DBFieldInt || $field instanceof DBFieldFloat) && $field->has_policy(DBFieldInt::UNSIGNED)) { + $type .= ' UNSIGNED'; + } + + // Add AUTO_INCREMENT + if ($field instanceof DBFieldInt && $field->has_policy(DBFieldInt::AUTOINCREMENT)) { + $type .= ' AUTO_INCREMENT'; + } + + // Add TIMESTAMP + if ($field instanceof DBFieldDateTime && $field->has_policy(DBFieldDateTime::TIMESTAMP)) { + $type = 'TIMESTAMP'; + } + + return $type; + } + + /** + * Get default value label + */ + public static function get_default_label(IDBField $field): string { + if ($field instanceof DBFieldInt && $field->has_policy(DBFieldInt::AUTOINCREMENT)) { + return '(auto)'; + } + + if ($field instanceof DBFieldDateTime && $field->has_policy(DBFieldDateTime::TIMESTAMP)) { + return 'CURRENT_TIMESTAMP'; + } + + $default = $field->get_field_default(); + if (is_null($default)) { + return 'NULL'; + } + if (is_bool($default)) { + return $default ? 'TRUE' : 'FALSE'; + } + return (string)$default; + } + + /** + * Get flags label + */ + public static function get_flags_label(IDBField $field): string { + $flags = array(); + + if ($field->has_policy(DBField::INTERNAL)) { + $flags[] = 'INTERNAL'; + } + + return implode(', ', $flags); + } + + /** + * Get relation type label + */ + private static function get_relation_type_label(int $type): string { + return match ($type) { + DBRelation::ONE_TO_ONE => '1:1', + DBRelation::ONE_TO_MANY => '1:N', + DBRelation::MANY_TO_MANY => 'N:M', + default => '?', + }; + } + + /** + * Generate a CREATE TABLE SQL statement from model info + */ + public static function generate_create_sql(array $model_info): string { + $table_name = $model_info['table']; + $lines = array(); + + foreach ($model_info['fields'] as $name => $field) { + $lines[] = ' ' . self::field_to_sql($name, $field); + } + + // Primary key + if (!empty($model_info['keys'])) { + $pk_names = array_keys($model_info['keys']); + $lines[] = ' PRIMARY KEY (' . implode(', ', $pk_names) . ')'; + } + + $sql = "CREATE TABLE `$table_name` (\n"; + $sql .= implode(",\n", $lines); + $sql .= "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + + return $sql; + } + + /** + * Convert a single field to SQL column definition + */ + public static function field_to_sql(string $name, IDBField $field): string { + $class = get_class($field); + + $type = match ($class) { + 'DBFieldInt' => 'INT', + 'DBFieldText', 'DBFieldTextEmail' => 'VARCHAR(' . ($field instanceof DBFieldText ? $field->get_length() : 255) . ')', + 'DBFieldFloat' => 'DOUBLE', + 'DBFieldBool' => "ENUM('TRUE','FALSE')", + 'DBFieldDate' => 'DATE', + 'DBFieldDateTime' => 'DATETIME', + 'DBFieldTime' => 'TIME', + 'DBFieldBlob' => 'LONGBLOB', + 'DBFieldEnum' => 'ENUM', // placeholder, handled below + 'DBFieldSet' => 'SET', // placeholder, handled below + 'DBFieldSerialized' => 'TEXT', + default => 'VARCHAR(255)', + }; + + // Handle text lengths for BLOB-like text + if ($field instanceof DBFieldText) { + $length = $field->get_length(); + if ($length > 65535) { + $type = ($length > 16777215) ? 'LONGTEXT' : 'MEDIUMTEXT'; + } elseif ($length > 255) { + $type = 'TEXT'; + } + } + + // UNSIGNED for int/float + if (($field instanceof DBFieldInt || $field instanceof DBFieldFloat) && $field->has_policy(DBFieldInt::UNSIGNED)) { + $type .= ' UNSIGNED'; + } + + // NOT NULL + $null = $field->get_null_allowed() ? 'NULL' : 'NOT NULL'; + + // AUTO_INCREMENT + $extra = ''; + if ($field instanceof DBFieldInt && $field->has_policy(DBFieldInt::AUTOINCREMENT)) { + $extra = ' AUTO_INCREMENT'; + } + + // TIMESTAMP + if ($field instanceof DBFieldDateTime && $field->has_policy(DBFieldDateTime::TIMESTAMP)) { + $type = 'TIMESTAMP'; + $extra = ' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'; + } + + // Default + $default_str = ''; + if (empty($extra)) { + $default = $field->get_field_default(); + if ($default !== null && !($field instanceof DBFieldInt && $field->has_policy(DBFieldInt::AUTOINCREMENT))) { + if (is_bool($default)) { + $default_str = " DEFAULT '" . ($default ? 'TRUE' : 'FALSE') . "'"; + } elseif (is_int($default) || is_float($default)) { + $default_str = " DEFAULT $default"; + } else { + $default_str = " DEFAULT '" . addslashes((string)$default) . "'"; + } + } elseif ($field->get_null_allowed() && $default === null) { + $default_str = ' DEFAULT NULL'; + } + } + + return "`$name` $type $null$default_str$extra"; + } +} diff --git a/gyro/core/controller/tools/formhandler.cls.php b/gyro/core/controller/tools/formhandler.cls.php index 11a81ef1..71a834ef 100644 --- a/gyro/core/controller/tools/formhandler.cls.php +++ b/gyro/core/controller/tools/formhandler.cls.php @@ -159,10 +159,10 @@ public function validate($data = false) { if ($this->token_policy != self::TOKEN_POLICY_NONE) { $token = Arr::get_item($data, Config::get_value(Config::FORMVALIDATION_FIELD_NAME), ''); // Validate if token is in DB - $success = $success && ($this->name == Arr::get_item($data, Config::get_value(Config::FORMVALIDATION_HANDLER_NAME), '')); + $success = $success && ($this->name === Arr::get_item($data, Config::get_value(Config::FORMVALIDATION_HANDLER_NAME), '')); $success = $success && FormValidations::validate_token($this->name, $token); } - if ($success == false) { + if ($success === false) { $ret->append(tr('Form verification token is too old. Please try again.', 'core')); } return $ret; diff --git a/gyro/core/lib/components/logger.cls.php b/gyro/core/lib/components/logger.cls.php index 3a56e4c6..f19fbcdd 100644 --- a/gyro/core/lib/components/logger.cls.php +++ b/gyro/core/lib/components/logger.cls.php @@ -1,27 +1,187 @@ 0, + self::ALERT => 1, + self::CRITICAL => 2, + self::ERROR => 3, + self::WARNING => 4, + self::NOTICE => 5, + self::INFO => 6, + self::DEBUG => 7, + ); + + /** + * Minimum log level. Messages below this level are discarded. + * Default: DEBUG (log everything) + * + * @var string + */ + private static $min_level = self::DEBUG; + + /** + * Set minimum log level + * + * @param string $level One of the Logger level constants + */ + public static function set_min_level(string $level): void { + if (isset(self::$levels[$level])) { + self::$min_level = $level; + } + } + + /** + * Legacy log method - backwards compatible + * + * @param string $file Log file name + * @param array $data Data to log + */ + public static function log(string $file, $data): void { $file_name = Config::get_value(Config::LOG_FILE_NAME_PATTERN); $file_name = str_replace('%date%', date('Y-m-d', time()), $file_name); $file_name = str_replace('%name%', $file, $file_name); $file_path = Config::get_value(Config::LOG_DIR) . $file_name; $handle = @fopen($file_path, 'a'); - if ($handle) { - $log = array_merge(array(date('Y/m/d, H:i:s', time()), Url::current()->build()), Arr::force($data)); + if ($handle) { + $log = array_merge(array(date('Y/m/d, H:i:s', time()), Url::current()->build()), Arr::force($data)); @fputcsv($handle, $log, ';'); @fclose($handle); - } + } + } + + /** + * Log with an arbitrary level + * + * @param string $level Log level + * @param string $message Message with optional {placeholder} tokens + * @param array $context Key-value pairs to interpolate into message and attach as metadata + */ + public static function log_level(string $level, string $message, array $context = array()): void { + if (!self::should_log($level)) { + return; + } + + $interpolated = self::interpolate($message, $context); + + $entry = array( + 'timestamp' => date('c'), + 'level' => $level, + 'message' => $interpolated, + ); + + if (!empty($context)) { + // Add exception info if present + if (isset($context['exception']) && $context['exception'] instanceof \Throwable) { + $ex = $context['exception']; + $entry['exception'] = array( + 'class' => get_class($ex), + 'message' => $ex->getMessage(), + 'code' => $ex->getCode(), + 'file' => $ex->getFile(), + 'line' => $ex->getLine(), + 'trace' => $ex->getTraceAsString(), + ); + unset($context['exception']); + } + if (!empty($context)) { + $entry['context'] = $context; + } + } + + self::write_entry($level, $entry); + } + + public static function emergency(string $message, array $context = array()): void { + self::log_level(self::EMERGENCY, $message, $context); + } + + public static function alert(string $message, array $context = array()): void { + self::log_level(self::ALERT, $message, $context); + } + + public static function critical(string $message, array $context = array()): void { + self::log_level(self::CRITICAL, $message, $context); + } + + public static function error(string $message, array $context = array()): void { + self::log_level(self::ERROR, $message, $context); + } + + public static function warning(string $message, array $context = array()): void { + self::log_level(self::WARNING, $message, $context); + } + + public static function notice(string $message, array $context = array()): void { + self::log_level(self::NOTICE, $message, $context); + } + + public static function info(string $message, array $context = array()): void { + self::log_level(self::INFO, $message, $context); + } + + public static function debug(string $message, array $context = array()): void { + self::log_level(self::DEBUG, $message, $context); + } + + /** + * Check if a message at the given level should be logged + */ + private static function should_log(string $level): bool { + $level_value = isset(self::$levels[$level]) ? self::$levels[$level] : self::$levels[self::DEBUG]; + $min_value = self::$levels[self::$min_level]; + return $level_value <= $min_value; + } + + /** + * Interpolate {placeholder} tokens in message with context values + */ + private static function interpolate(string $message, array $context): string { + $replace = array(); + foreach ($context as $key => $val) { + if ($key === 'exception') { + continue; + } + if (is_string($val) || is_numeric($val) || (is_object($val) && method_exists($val, '__toString'))) { + $replace['{' . $key . '}'] = (string)$val; + } + } + return strtr($message, $replace); + } + + /** + * Write a structured log entry as JSON + */ + private static function write_entry(string $level, array $entry): void { + $file_name = Config::get_value(Config::LOG_FILE_NAME_PATTERN); + $file_name = str_replace('%date%', date('Y-m-d', time()), $file_name); + $file_name = str_replace('%name%', $level, $file_name); + $file_path = Config::get_value(Config::LOG_DIR) . $file_name; + $handle = @fopen($file_path, 'a'); + if ($handle) { + @fwrite($handle, json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"); + @fclose($handle); + } } -} \ No newline at end of file +} diff --git a/gyro/core/lib/helpers/cast.cls.php b/gyro/core/lib/helpers/cast.cls.php index b06b6c2c..da2ea856 100644 --- a/gyro/core/lib/helpers/cast.cls.php +++ b/gyro/core/lib/helpers/cast.cls.php @@ -48,7 +48,7 @@ public static function string($value) { return ''; } else if (is_object($value)) { - if (isset($value->__toString)) { + if (method_exists($value, '__toString')) { return $value->__toString(); } else { diff --git a/gyro/core/lib/helpers/common.cls.php b/gyro/core/lib/helpers/common.cls.php index cbafd1da..aeacce6a 100644 --- a/gyro/core/lib/helpers/common.cls.php +++ b/gyro/core/lib/helpers/common.cls.php @@ -167,34 +167,12 @@ public static function header_restore($arr_headers) { } /** - * Strips possible slashes added by magic quotes + * Previously stripped slashes added by magic quotes. + * Magic quotes were removed in PHP 7.4 / PHP 8.0, so this is now a no-op. + * Kept for backwards compatibility with callers. */ public static function preprocess_input() { - // Is magic quotes on? - // deprecated ssince PHP 7.4, hence version check - if (PHP_VERSION_ID < 70400 && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() ) { - // Yes? Strip the added slashes - $_REQUEST = self::transcribe($_REQUEST); - $_GET = self::transcribe($_GET); - $_POST = self::transcribe($_POST); - $_COOKIE = self::transcribe($_COOKIE); - } - } - - // Taken from here: http://de.php.net/manual/de/function.get-magic-quotes-gpc.php#49612 - private static function transcribe($aList, $aIsTopLevel = true) { - $gpcList = array(); - foreach ($aList as $key => $value) { - if (is_array($value)) { - $decodedKey = (!$aIsTopLevel) ? stripslashes($key) : $key; - $decodedValue = self::transcribe($value, false); - } else { - $decodedKey = stripslashes($key); - $decodedValue = stripslashes($value); - } - $gpcList[$decodedKey] = $decodedValue; - } - return $gpcList; + // No-op: magic quotes removed in PHP 7.4+ } /** @@ -285,17 +263,17 @@ public static function is_google() { * @return string */ public static function create_token($salt = false) { - return sha1(uniqid($salt ? $salt : mt_rand(), true)); + return bin2hex(random_bytes(20)); } /** * Creates a token, which is 64 characters long * - * @param string|false $salt Optional extra salt, if omitted mt_rand() is used + * @param string|false $salt Optional extra salt (unused, kept for API compatibility) * @return string */ public static function create_long_token($salt = false) { - return hash('sha3-256', uniqid($salt ? $salt : mt_rand(), true)); + return bin2hex(random_bytes(32)); } /** diff --git a/gyro/core/lib/helpers/converters/callback.converter.php b/gyro/core/lib/helpers/converters/callback.converter.php index b04b8fcf..e24bfc94 100644 --- a/gyro/core/lib/helpers/converters/callback.converter.php +++ b/gyro/core/lib/helpers/converters/callback.converter.php @@ -6,14 +6,14 @@ * @ingroup Lib */ class ConverterCallback implements IConverter { - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { if (is_callable($params)) { return $params($value); } throw new Exception('Callback in ConverterCallback::encode not callable'); } - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { if (is_callable($params)) { return $params($value); } diff --git a/gyro/core/lib/helpers/converters/chain.converter.php b/gyro/core/lib/helpers/converters/chain.converter.php index b828aca5..5d1e9491 100644 --- a/gyro/core/lib/helpers/converters/chain.converter.php +++ b/gyro/core/lib/helpers/converters/chain.converter.php @@ -6,10 +6,10 @@ * @ingroup Lib */ class ConverterChain implements IConverter { - protected $converters = array(); - protected $params = array(); + protected array $converters = array(); + protected array $params = array(); - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { reset($this->params); foreach($this->converters as $c) { $p = current($this->params); @@ -19,7 +19,7 @@ public function encode($value, $params = false) { return $value; } - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { reset($this->params); foreach($this->converters as $c) { $p = current($this->params); @@ -35,7 +35,7 @@ public function decode($value, $params = false) { * @param IConverter $converter The converter * @param mixed $params The converters params */ - public function append(IConverter $converter, $params = false) { + public function append(IConverter $converter, mixed $params = false): void { $this->converters[] = $converter; $this->params[] = $params; } diff --git a/gyro/core/lib/helpers/converters/html.converter.php b/gyro/core/lib/helpers/converters/html.converter.php index 242cadc4..0f6749b7 100644 --- a/gyro/core/lib/helpers/converters/html.converter.php +++ b/gyro/core/lib/helpers/converters/html.converter.php @@ -6,7 +6,7 @@ * @ingroup Lib */ class ConverterHtml implements IConverter { - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { $value = $this->decode($value); $value = str_replace("\r", "\n", $value); //var_dump(str_replace("\n", '\n', $value)); @@ -36,7 +36,7 @@ protected function process_paragraph($text, $params) { return html::tag('p', GyroString::escape($text)); } - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { //$value = str_replace("\n", ' ', $value); //$value = str_replace("\r", ' ', $value); $value = str_replace('

', "

\n", $value); diff --git a/gyro/core/lib/helpers/converters/htmlex.converter.php b/gyro/core/lib/helpers/converters/htmlex.converter.php index ea91f386..1c2cc782 100644 --- a/gyro/core/lib/helpers/converters/htmlex.converter.php +++ b/gyro/core/lib/helpers/converters/htmlex.converter.php @@ -19,7 +19,7 @@ class ConverterHtmlEx extends ConverterHtml { protected function process_paragraph($text, $params) { if (GyroString::length($text) <= 70 && GyroString::right($text, 1) != '.') { $level = intval(Arr::get_item($params, 'h', 2)); - return html::tag('h' . $level, $text); + return html::tag('h' . $level, GyroString::escape($text)); } else { return parent::process_paragraph($text, $params); @@ -39,7 +39,7 @@ protected function process_paragraph($text, $params) { * @li br: Text after a
tag. Default is "\n" * @li a: Format to decode tags. Supports $title$ and $url$ placeholders. Default is "$title$: $url$" */ - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { // If there is no HTML, decoding would do more harm than good if (!preg_match('@<\w+.*?>@', $value)) { return $value; diff --git a/gyro/core/lib/helpers/converters/mimeheader.converter.php b/gyro/core/lib/helpers/converters/mimeheader.converter.php index 6b820504..3f9c0ef9 100644 --- a/gyro/core/lib/helpers/converters/mimeheader.converter.php +++ b/gyro/core/lib/helpers/converters/mimeheader.converter.php @@ -11,7 +11,7 @@ class ConverterMimeHeader implements IConverter { /** * ENcode. Takes optional charset as parameter */ - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { if (!$params) { $params = GyroLocale::get_charset(); } @@ -54,7 +54,7 @@ public function encode($value, $params = false) { return $ret; } - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { return $value; } } diff --git a/gyro/core/lib/helpers/converters/none.converter.php b/gyro/core/lib/helpers/converters/none.converter.php index 9d94dea9..696a6928 100644 --- a/gyro/core/lib/helpers/converters/none.converter.php +++ b/gyro/core/lib/helpers/converters/none.converter.php @@ -6,11 +6,11 @@ * @ingroup Lib */ class ConverterNone implements IConverter { - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { return $value; } - public function decode($value, $params = false) { + public function decode(mixed $value, mixed $params = false): mixed { return $value; } } diff --git a/gyro/core/lib/helpers/env.cls.php b/gyro/core/lib/helpers/env.cls.php new file mode 100644 index 00000000..3b67b858 --- /dev/null +++ b/gyro/core/lib/helpers/env.cls.php @@ -0,0 +1,201 @@ += 2) { + $first = $value[0]; + $last = $value[$len - 1]; + if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) { + return substr($value, 1, $len - 2); + } + } + return $value; + } + + /** + * Cast string value to appropriate PHP type + * + * Handles: true, false, null, integers, floats + * + * @param string $value + * @return mixed + */ + private static function cast_value($value) { + $lower = strtolower($value); + + if ($lower === 'true') { + return true; + } + if ($lower === 'false') { + return false; + } + if ($lower === 'null' || $lower === '') { + return ''; + } + + // Integer + if (ctype_digit($value) || ($value !== '' && $value[0] === '-' && ctype_digit(substr($value, 1)))) { + return (int)$value; + } + + // Float + if (is_numeric($value) && strpos($value, '.') !== false) { + return (float)$value; + } + + return $value; + } +} diff --git a/gyro/core/lib/helpers/requestinfo.cls.php b/gyro/core/lib/helpers/requestinfo.cls.php index c961dcf3..7e23606f 100644 --- a/gyro/core/lib/helpers/requestinfo.cls.php +++ b/gyro/core/lib/helpers/requestinfo.cls.php @@ -112,12 +112,13 @@ protected function compute_url_invoked($type) { } if ($type == self::ABSOLUTE) { $prefix = $this->is_ssl() ? 'https://' : 'http://'; - // Check proxy forwarded stuff - $prefix .= Arr::get_item( - $_SERVER, 'HTTP_X_FORWARDED_HOST', Arr::get_item( - $_SERVER, 'HTTP_HOST', Config::get_value(Config::URL_DOMAIN) - ) - ); + $configured_domain = Config::get_value(Config::URL_DOMAIN); + $host = Arr::get_item($_SERVER, 'HTTP_HOST', $configured_domain); + // Validate host against configured domain to prevent host header injection + if (!empty($configured_domain) && $host !== $configured_domain) { + $host = $configured_domain; + } + $prefix .= $host; $ret = $prefix . $ret; } return $ret; diff --git a/gyro/core/lib/helpers/session.cls.php b/gyro/core/lib/helpers/session.cls.php index 5012835e..704423a3 100644 --- a/gyro/core/lib/helpers/session.cls.php +++ b/gyro/core/lib/helpers/session.cls.php @@ -1,9 +1,15 @@ $expire, + 'path' => $cookie_params['path'], + 'domain' => $cookie_params['domain'], + 'secure' => $cookie_params['secure'], + 'httponly' => true, + 'samesite' => 'Lax' + ]); } /** diff --git a/gyro/core/lib/helpers/url.cls.php b/gyro/core/lib/helpers/url.cls.php index eb2217a5..930db415 100644 --- a/gyro/core/lib/helpers/url.cls.php +++ b/gyro/core/lib/helpers/url.cls.php @@ -49,6 +49,11 @@ class Url { private $data = array(); private $support_unicode_domains = false; + /** + * Serialized URL string (used by __sleep/__wakeup) + * @var string + */ + private $url = ''; /** * Keep track if an empty query is found during parsing, for URLs * like http://example.com/index.html? (which indicates another diff --git a/gyro/core/lib/interfaces/icacheitem.cls.php b/gyro/core/lib/interfaces/icacheitem.cls.php index 4185bb13..cc9d1159 100644 --- a/gyro/core/lib/interfaces/icacheitem.cls.php +++ b/gyro/core/lib/interfaces/icacheitem.cls.php @@ -11,33 +11,33 @@ interface ICacheItem { * * @return datetime */ - public function get_creationdate(); - + public function get_creationdate(): mixed; + /** - * Return expiration date - * - * @return datetime + * Return expiration date + * + * @return mixed Unix timestamp or datetime string */ - public function get_expirationdate(); - + public function get_expirationdate(): mixed; + /** * Return data associated with this item - * + * * @return mixed */ - public function get_data(); - + public function get_data(): mixed; + /** * Return the content in plain form - * + * * @return string */ - public function get_content_plain(); - + public function get_content_plain(): string; + /** * Return the content gzip compressed - * + * * @return string */ - public function get_content_compressed(); + public function get_content_compressed(): string; } \ No newline at end of file diff --git a/gyro/core/lib/interfaces/icachepersister.cls.php b/gyro/core/lib/interfaces/icachepersister.cls.php index ec5d50d2..c9ed8790 100644 --- a/gyro/core/lib/interfaces/icachepersister.cls.php +++ b/gyro/core/lib/interfaces/icachepersister.cls.php @@ -12,36 +12,36 @@ interface ICachePersister { * @param $cache_keys mixed A set of key params, may be an array or a string * @return bool */ - public function is_cached($cache_keys); + public function is_cached(mixed $cache_keys): bool; /** * Read from cache - * - * @param Mixed A set of key params, may be an array or a string - * @return ICacheItem False if cache is not found + * + * @param mixed $cache_keys A set of key params, may be an array or a string + * @return ICacheItem|false False if cache is not found */ - public function read($cache_keys); - + public function read(mixed $cache_keys): ICacheItem|false; + /** * Store content in cache - * + * * @param mixed $cache_keys A set of key params, may be an array or a string * @param string $content The cache * @param int $cache_life_time Cache life time in seconds * @param mixed $data Any data assoziated with this item - * @param bool $is_compressed True, if $content is already gzip compressed + * @param bool $is_compressed True, if $content is already gzip compressed */ - public function store($cache_keys, $content, $cache_life_time, $data = '', $is_compressed = false); - + public function store(mixed $cache_keys, string $content, int $cache_life_time, mixed $data = '', bool $is_compressed = false): void; + /** * Clear the cache - * + * * @param mixed $cache_keys A set of key params, may be an array or a string, or an ICachable instance. If NULL, all is cleared */ - public function clear($cache_keys = NULL); + public function clear(mixed $cache_keys = NULL): void; /** * Removes expired cache entries */ - public function remove_expired(); + public function remove_expired(): void; } diff --git a/gyro/core/lib/interfaces/iconverter.cls.php b/gyro/core/lib/interfaces/iconverter.cls.php index 7cf83459..e3173a23 100644 --- a/gyro/core/lib/interfaces/iconverter.cls.php +++ b/gyro/core/lib/interfaces/iconverter.cls.php @@ -6,7 +6,21 @@ * @ingroup Interfaces */ interface IConverter { - public function encode($value, $params = false); - public function decode($value, $params = false); -} -?> \ No newline at end of file + /** + * Encode (convert) the given value + * + * @param mixed $value The value to encode + * @param mixed $params Optional converter-specific parameters + * @return mixed The encoded value + */ + public function encode(mixed $value, mixed $params = false): mixed; + + /** + * Decode (reverse-convert) the given value + * + * @param mixed $value The value to decode + * @param mixed $params Optional converter-specific parameters + * @return mixed The decoded value + */ + public function decode(mixed $value, mixed $params = false): mixed; +} \ No newline at end of file diff --git a/gyro/core/lib/interfaces/idbdriver.cls.php b/gyro/core/lib/interfaces/idbdriver.cls.php index 03e7da56..7925913a 100644 --- a/gyro/core/lib/interfaces/idbdriver.cls.php +++ b/gyro/core/lib/interfaces/idbdriver.cls.php @@ -128,9 +128,29 @@ public function last_insert_id(); /** * Returns true, if a given feature is supported - * + * * @param string feature - * @return bool + * @return bool */ public function has_feature($feature); + + /** + * Execute a prepared statement (INSERT, UPDATE, DELETE) + * + * @param string $sql SQL with ? placeholders + * @param array $params Bind parameters (values for ? placeholders) + * @param string $types Type string for bind_param (e.g. 'ssi' for string, string, int) + * @return Status + */ + public function execute_prepared($sql, $params = array(), $types = ''); + + /** + * Execute a prepared SELECT statement + * + * @param string $sql SQL with ? placeholders + * @param array $params Bind parameters + * @param string $types Type string for bind_param + * @return IDBResultSet + */ + public function query_prepared($sql, $params = array(), $types = ''); } \ No newline at end of file diff --git a/gyro/core/lib/interfaces/idbresultset.cls.php b/gyro/core/lib/interfaces/idbresultset.cls.php index 0b9e545c..13da22a3 100644 --- a/gyro/core/lib/interfaces/idbresultset.cls.php +++ b/gyro/core/lib/interfaces/idbresultset.cls.php @@ -1,43 +1,35 @@ query_prepared($sql, $params, $types); + self::log_query($sql, $timer->seconds_elapsed(), $ret->get_status(), $conn); + return $ret; + } + + /** + * Execute a non-SELECT query using prepared statements + * + * @param string $sql SQL with ? placeholders + * @param array $params Parameter values + * @param string $types Optional type string (s=string, i=int, d=double, b=blob) + * @param string|IDBDriver $connection + * @return Status + */ + public static function execute_prepared($sql, $params = array(), $types = '', $connection = self::DEFAULT_CONNECTION) { + $timer = new Timer(); + $conn = self::get_connection($connection); + $ret = $conn->execute_prepared($sql, $params, $types); + self::log_query($sql, $timer->seconds_elapsed(), $ret, $conn); + return $ret; + } + /** * Explain the given query * diff --git a/gyro/core/model/base/dbresultset.cls.php b/gyro/core/model/base/dbresultset.cls.php index ca77a4aa..362bb471 100644 --- a/gyro/core/model/base/dbresultset.cls.php +++ b/gyro/core/model/base/dbresultset.cls.php @@ -1,7 +1,7 @@ pdo_statement = $pdo; } - /** - * Closes internal cursor - * - * @return void - */ - public function close() { + public function close(): void { $this->pdo_statement->closeCursor(); } - - /** - * Returns number of columns in result set - * - * @return int - */ - public function get_column_count() { + + public function get_column_count(): int { return $this->pdo_statement->columnCount(); } - - /** - * Returns number of rows in result set - * - * @return int - */ - public function get_row_count() { + + public function get_row_count(): int { return $this->pdo_statement->rowCount(); } - - /** - * Returns row as associative array - * - * @return array | bool False if no more data is available - */ - public function fetch() { + + public function fetch(): array|false { return $this->pdo_statement->fetch(PDO::FETCH_ASSOC); } - - /** - * Returns status - * - * @param Status - */ - public function get_status() { + + public function get_status(): Status { $ret = new Status(); $stub = substr($this->pdo_statement->errorCode(), 0, 2); switch ($stub) { diff --git a/gyro/core/model/base/dbsession.cls.php b/gyro/core/model/base/dbsession.cls.php index e9aa715c..3e2226e9 100644 --- a/gyro/core/model/base/dbsession.cls.php +++ b/gyro/core/model/base/dbsession.cls.php @@ -11,14 +11,14 @@ class DBSession implements ISessionHandler { /** * Open a session */ - public function open($save_path, $session_name) { + public function open(string $save_path, string $session_name): bool { return true; } /** * Close a session */ - public function close() { + public function close(): bool { //Note that for security reasons the Debian and Ubuntu distributions of //php do not call _gc to remove old sessions, but instead run /etc/cron.d/php*, //which check the value of session.gc_maxlifetime in php.ini and delete the session @@ -33,7 +33,7 @@ public function close() { /** * Load session data from database */ - public function read($key) { + public function read(string $key): string|false { // Write and Close handlers are called after destructing objects since PHP 5.0.5 // Thus destructors can use sessions but session handler can't use objects. // So we are moving session closure before destructing objects. @@ -48,7 +48,7 @@ public function read($key) { /** * Write session data to DB */ - public function write($key, $value) { + public function write(string $key, string $value): bool { try { // Rollback any open transactions, if there are any //DB::rollback(); @@ -74,7 +74,7 @@ public function write($key, $value) { /** * Delete a session */ - public function destroy($key) { + public function destroy(string $key): bool { try { $sess = new DAOSessions(); $sess->id = $key; @@ -89,7 +89,7 @@ public function destroy($key) { /** * Delete outdated sessions */ - public function gc($lifetime) { + public function gc(int $lifetime): int|false { if (Session::is_started()) { try { $sess = new DAOSessions(); diff --git a/gyro/core/model/base/fields/dbfield.serialized.cls.php b/gyro/core/model/base/fields/dbfield.serialized.cls.php index 172fa046..261dd020 100644 --- a/gyro/core/model/base/fields/dbfield.serialized.cls.php +++ b/gyro/core/model/base/fields/dbfield.serialized.cls.php @@ -40,7 +40,7 @@ protected function do_format_not_null($value) { * @return mixed */ public function convert_result($value) { - return is_null($value) ? null : unserialize($value); + return is_null($value) ? null : unserialize($value, ['allowed_classes' => false]); } /** @@ -51,7 +51,7 @@ public function convert_result($value) { public function get_field_default() { $ret = parent::get_field_default(); if ($ret) { - $ret = unserialize($ret); + $ret = unserialize($ret, ['allowed_classes' => false]); } return $ret; } diff --git a/gyro/core/model/classes/cache.db.impl.php b/gyro/core/model/classes/cache.db.impl.php index 72854bee..8721b448 100644 --- a/gyro/core/model/classes/cache.db.impl.php +++ b/gyro/core/model/classes/cache.db.impl.php @@ -6,12 +6,12 @@ * @ingroup Model */ class CacheDBImpl implements ICachePersister { - private $cache_item = null; + private mixed $cache_item = null; /** * Returns true, if item is chaced */ - public function is_cached($cache_keys) { + public function is_cached(mixed $cache_keys): bool { $dao = new DAOCache(); $dao->add_where('content_gzip', DBWhere::OP_NOT_NULL); $dao->set_keys($this->extract_keys($cache_keys)); @@ -33,7 +33,7 @@ public function is_cached($cache_keys) { * @param Mixed A set of key params, may be an array or a string * @return ICacheItem The cache as array with members "content" and "data", false if cache is not found */ - public function read($cache_keys) { + public function read(mixed $cache_keys): ICacheItem|false { $dao = new DAOCache(); $dao->add_where('content_gzip', DBWhere::OP_NOT_NULL); $dao->set_keys($this->extract_keys($cache_keys)); @@ -53,7 +53,7 @@ public function read($cache_keys) { * @param Mixed A set of key params, may be an array or a string * @param string The cache */ - public function store($cache_keys, $content, $cache_life_time, $data = '', $is_compressed = false) { + public function store(mixed $cache_keys, string $content, int $cache_life_time, mixed $data = '', bool $is_compressed = false): void { try { // Clear old items $this->remove_expired(); @@ -87,7 +87,7 @@ public function store($cache_keys, $content, $cache_life_time, $data = '', $is_c * * @param Mixed A set of key params, may be an array or a string. If NULL, all is cleared */ - public function clear($cache_keys = NULL) { + public function clear(mixed $cache_keys = NULL): void { $dao = new DAOCache(); if (!empty($cache_keys)) { $keys = $this->extract_keys($cache_keys); @@ -114,7 +114,7 @@ private function extract_keys($cache_keys) { /** * Removes expired cache entries */ - public function remove_expired() { + public function remove_expired(): void { $dao = new DAOCache(); $dao->add_where('expirationdate', '<', DBFieldDateTime::NOW); $dao->delete(DAOCache::WHERE_ONLY); diff --git a/gyro/core/model/classes/cache.model.php b/gyro/core/model/classes/cache.model.php index eaeb68ff..e3659fc1 100644 --- a/gyro/core/model/classes/cache.model.php +++ b/gyro/core/model/classes/cache.model.php @@ -1,86 +1,86 @@ 3) { - $c = 3; - } - for ($i = 0; $i < $c; $i++) { - $name = 'key' . $i; - if ($force_where) { - $this->add_where($name, '=', $keys[$i]); - } - else { - $val = $keys[$i]; - if (!empty($val)) { - $this->$name = $val; + /** + * Set cache keys + * + * @param Array Array of keys that are set as key0, key1 etc + * @param Boolean If TRUE a phrase keyX like 'valueX%' etc is added to where clause for each key + */ + public function set_keys($keys, $force_where = false) { + $c = count($keys); + if ($c > 3) { + $c = 3; + } + for ($i = 0; $i < $c; $i++) { + $name = 'key' . $i; + if ($force_where) { + $this->add_where($name, '=', $keys[$i]); + } + else { + $val = $keys[$i]; + if (!empty($val)) { + $this->$name = $val; } else { $this->add_where($name, DBWhere::OP_IS_NULL); - } - } - } - } + } + } + } + } /** * Return creation date * * @return datetime */ - public function get_creationdate() { + public function get_creationdate(): mixed { return $this->creationdate; - } - + } + /** - * Return expiration date - * - * @return datetime + * Return expiration date + * + * @return mixed */ - public function get_expirationdate() { + public function get_expirationdate(): mixed { return $this->expirationdate; } - + /** * Return data associated with this item - * + * * @return mixed */ - public function get_data() { + public function get_data(): mixed { return $this->data; } - - public function get_content_plain() { + + public function get_content_plain(): string { $ret = $this->content_gzip; if ($ret && function_exists('gzinflate')) { $ret = gzinflate($ret); } return $ret; } - - public function get_content_compressed() { + + public function get_content_compressed(): string { return $this->content_gzip; } @@ -94,25 +94,25 @@ public function set_content_plain($content) { public function set_content_compressed($content) { $this->content_gzip = $content; } - - // now define your table structure. - // key is column name, value is type - protected function create_table_object() { + + // now define your table structure. + // key is column name, value is type + protected function create_table_object() { return new DBTable( 'cache', - array( + array( new DBFieldInt('id', null, DBFieldInt::AUTOINCREMENT | DBFieldInt::UNSIGNED | DBFieldInt::NOT_NULL), new DBFieldText('key0', 255), new DBFieldText('key1', 255), new DBFieldText('key2', 255), new DBFieldBlob('content_gzip', DBFieldText::BLOB_LENGTH_LARGE), new DBFieldDateTime('creationdate', null, DBFieldDateTime::TIMESTAMP), - new DBFieldDateTime('expirationdate', null, DBFieldDateTime::NOT_NULL), + new DBFieldDateTime('expirationdate', null, DBFieldDateTime::NOT_NULL), new DBFieldSerialized('data', DBFieldText::BLOB_LENGTH_SMALL) ), - 'id' - ); - } + 'id' + ); + } /** @@ -124,4 +124,4 @@ protected function configure_insert_query($query) { $query->set_policy(DBQueryInsert::IGNORE); parent::configure_insert_query($query); } -} +} diff --git a/gyro/core/model/drivers/mysql/dbdriver.mysql.php b/gyro/core/model/drivers/mysql/dbdriver.mysql.php index 1f2b1b21..1a88fa2e 100644 --- a/gyro/core/model/drivers/mysql/dbdriver.mysql.php +++ b/gyro/core/model/drivers/mysql/dbdriver.mysql.php @@ -144,8 +144,10 @@ public function quote($value) { */ public function escape_database_entity($obj, $type = self::FIELD) { $ret = ''; + $obj = str_replace('`', '``', $obj); if ($type === self::TABLE) { - $ret .= '`' . $this->get_db_name() . '`.'; + $db_name = str_replace('`', '``', $this->get_db_name()); + $ret .= '`' . $db_name . '`.'; } $ret .= '`' . $obj . '`'; return $ret; @@ -298,5 +300,94 @@ public function has_feature($feature) { default: return false; } - } + } + + /** + * Auto-detect mysqli bind_param type string from parameter values. + * + * @param array $params + * @return string Type string (e.g. 'ssi') + */ + protected function detect_param_types($params) { + $types = ''; + foreach ($params as $param) { + if (is_int($param)) { + $types .= 'i'; + } elseif (is_float($param)) { + $types .= 'd'; + } else { + $types .= 's'; + } + } + return $types; + } + + /** + * Prepare and bind a mysqli statement + * + * @param string $sql SQL with ? placeholders + * @param array $params Bind parameters + * @param string $types Type string for bind_param (auto-detected if empty) + * @return mysqli_stmt|false + */ + protected function prepare_statement($sql, $params, $types) { + $this->connect(); + $stmt = $this->conn->prepare($sql); + if ($stmt === false) { + return false; + } + if (!empty($params)) { + if (empty($types)) { + $types = $this->detect_param_types($params); + } + $stmt->bind_param($types, ...$params); + } + return $stmt; + } + + /** + * Execute a prepared statement (INSERT, UPDATE, DELETE) + * + * @param string $sql SQL with ? placeholders + * @param array $params Bind parameters + * @param string $types Type string for bind_param (auto-detected if empty) + * @return Status + */ + public function execute_prepared($sql, $params = array(), $types = '') { + $ret = new Status(); + $stmt = $this->prepare_statement($sql, $params, $types); + if ($stmt === false) { + $ret->append($this->conn->error); + return $ret; + } + if (!$stmt->execute()) { + $ret->append($stmt->error); + } + $stmt->close(); + return $ret; + } + + /** + * Execute a prepared SELECT statement + * + * @param string $sql SQL with ? placeholders + * @param array $params Bind parameters + * @param string $types Type string for bind_param (auto-detected if empty) + * @return IDBResultSet + */ + public function query_prepared($sql, $params = array(), $types = '') { + $ret = new Status(); + $stmt = $this->prepare_statement($sql, $params, $types); + if ($stmt === false) { + $ret->append($this->conn->error); + return new DBResultSetMysql(false, $ret); + } + $stmt->execute(); + $result = $stmt->get_result(); + if ($stmt->errno) { + $ret->append($stmt->error); + } + $stmt->close(); + return new DBResultSetMysql($result, $ret); + } } diff --git a/gyro/core/model/drivers/mysql/dbresultset.mysql.php b/gyro/core/model/drivers/mysql/dbresultset.mysql.php index 400b3dce..65c6af31 100644 --- a/gyro/core/model/drivers/mysql/dbresultset.mysql.php +++ b/gyro/core/model/drivers/mysql/dbresultset.mysql.php @@ -1,7 +1,7 @@ result_set = $result_set; $this->status = $status; } - + public function __destruct() { $this->close(); } - /** - * Closes internal cursor - * - * @return void - */ - public function close() { + public function close(): void { if ($this->result_set) { $this->result_set->close(); $this->result_set = null; } } - - /** - * Returns number of columns in result set - * - * @return int - */ - public function get_column_count() { + + public function get_column_count(): int { if ($this->result_set) { return $this->result_set->field_count; } else { return 0; } } - - /** - * Returns number of rows in result set - * - * @return int - */ - public function get_row_count() { + + public function get_row_count(): int { if ($this->result_set) { return $this->result_set->num_rows; } @@ -66,27 +51,18 @@ public function get_row_count() { return 0; } } - - /** - * Returns row as associative array - * - * @return array | bool False if no more data is available - */ - public function fetch() { + + public function fetch(): array|false { if ($this->result_set) { - return $this->result_set->fetch_assoc(); + $row = $this->result_set->fetch_assoc(); + return $row === null ? false : $row; } else { - return array(); + return false; } } - - /** - * Returns status - * - * @param Status - */ - public function get_status() { + + public function get_status(): Status { return $this->status; } } diff --git a/gyro/core/start.php b/gyro/core/start.php index 6cd0b362..c0772fa3 100644 --- a/gyro/core/start.php +++ b/gyro/core/start.php @@ -17,22 +17,24 @@ define ('GYRO_ROOT_DIR', GYRO_CORE_DIR . '../'); require_once GYRO_CORE_DIR . 'config.cls.php'; Config::set_value(Config::VERSION, 0.6); + +// Load .env file if present (defines APP_* constants before constants.inc.php reads them) +require_once GYRO_CORE_DIR . 'lib/helpers/env.cls.php'; +if (defined('APP_INCLUDE_ABSPATH')) { + Env::load(APP_INCLUDE_ABSPATH . '.env'); +} + require_once GYRO_CORE_DIR . 'constants.inc.php'; // Set error reporting settings if (Config::has_feature(Config::TESTMODE)) { ini_set('display_errors', 1); ini_set('log_errors', 1); - error_reporting(E_ALL | E_STRICT); + error_reporting(E_ALL); } else { ini_set('display_errors', 0); ini_set('log_errors', 1); - if (defined('E_DEPRECATED')) { - // PHP 5.3 - error_reporting(E_ALL ^ E_NOTICE ^ E_DEPRECATED); - } else { - error_reporting(E_ALL ^ E_NOTICE); - } + error_reporting(E_ALL ^ E_NOTICE ^ E_DEPRECATED); } diff --git a/gyro/core/view/base/pageviewbase.cls.php b/gyro/core/view/base/pageviewbase.cls.php index 77a48fd3..64a30a0b 100644 --- a/gyro/core/view/base/pageviewbase.cls.php +++ b/gyro/core/view/base/pageviewbase.cls.php @@ -91,7 +91,13 @@ protected function render_postprocess(&$rendered_content, $policy) { } GyroHeaders::set('Vary', 'Accept-Encoding', false); GyroHeaders::set('Date', GyroDate::http_date(time()), true); - + + // Security headers (set with override=false so apps can customize) + GyroHeaders::set('X-Content-Type-Options', 'nosniff', false); + GyroHeaders::set('X-Frame-Options', 'SAMEORIGIN', false); + GyroHeaders::set('Referrer-Policy', 'strict-origin-when-cross-origin', false); + GyroHeaders::set('Permissions-Policy', 'geolocation=(), camera=(), microphone=()', false); + GyroHeaders::send(); } } diff --git a/gyro/install/check_preconditions.php b/gyro/install/check_preconditions.php index 0d181937..23905c18 100644 --- a/gyro/install/check_preconditions.php +++ b/gyro/install/check_preconditions.php @@ -12,16 +12,12 @@ function core_check_preconditions() { foreach($subdirs as $subdir) { $dir = rtrim($tempdir . $subdir, '/'); if (!file_exists($dir)) { - $cmd = 'mkdir -p ' . $dir; - if (shell_exec($cmd)) { - $ret->append('Could not create temporary directory ' . $dir); - } - else { - chmod($dir, 0777); + if (!@mkdir($dir, 0755, true)) { + $ret->append('Could not create temporary directory ' . $dir); } } // Try to place file into temp dir - $file = $dir . '/test' . md5(uniqid()); + $file = $dir . '/test' . bin2hex(random_bytes(16)); if (touch($file)) { unlink($file); } diff --git a/gyro/modules/json/lib/helpers/converters/json.cls.php b/gyro/modules/json/lib/helpers/converters/json.cls.php index 369af08b..61f94a59 100644 --- a/gyro/modules/json/lib/helpers/converters/json.cls.php +++ b/gyro/modules/json/lib/helpers/converters/json.cls.php @@ -20,7 +20,7 @@ class GyroJSON implements IConverter { * @param string $str * @return mixed */ - public function decode($str, $params = false) { + public function decode(mixed $str, mixed $params = false): mixed { $ret = false; if (function_exists('json_decode')) { // PHP 5.2 or PECL extension @@ -49,7 +49,7 @@ public function decode($str, $params = false) { * @param mixed $data * @return string */ - public function encode($data, $params = false) { + public function encode(mixed $data, mixed $params = false): mixed { if (function_exists('json_encode')) { // PHP 5.2 or PECL extension // There is a bug in json_encode and floating values before PHP 5.2.2 diff --git a/gyro/modules/phpinfo/controller/phpinfo.controller.php b/gyro/modules/phpinfo/controller/phpinfo.controller.php index afb86d87..ae49c098 100644 --- a/gyro/modules/phpinfo/controller/phpinfo.controller.php +++ b/gyro/modules/phpinfo/controller/phpinfo.controller.php @@ -37,6 +37,9 @@ public function get_routes() { * @return void */ public function action_phpinfo(PageData $page_data) { + if (!Config::has_feature(Config::TESTMODE)) { + return self::NOT_FOUND; + } print phpinfo(); exit; } diff --git a/gyro/modules/simpletest/model/classes/studentstest.model.php b/gyro/modules/simpletest/model/classes/studentstest.model.php index 105067ec..197a26e1 100644 --- a/gyro/modules/simpletest/model/classes/studentstest.model.php +++ b/gyro/modules/simpletest/model/classes/studentstest.model.php @@ -8,6 +8,7 @@ class DAOStudentsTest extends DataObjectBase { public $id; public $name; + public $modificationdate; protected function create_table_object() { return new DBTable( diff --git a/gyro/modules/tidy/lib/helpers/converters/htmltidy.converter.php b/gyro/modules/tidy/lib/helpers/converters/htmltidy.converter.php index a54dee5b..c9223718 100644 --- a/gyro/modules/tidy/lib/helpers/converters/htmltidy.converter.php +++ b/gyro/modules/tidy/lib/helpers/converters/htmltidy.converter.php @@ -12,7 +12,7 @@ * @ingroup Tidy */ class ConverterHtmlTidy implements IConverter { - private $predefined_params; + private array $predefined_params; public function __construct($global_params = array()) { $predefined_params = array( @@ -51,7 +51,7 @@ public function __construct($global_params = array()) { * @param $params Associative array containing tidy config parameters * @return string */ - public function encode($value, $params = false) { + public function encode(mixed $value, mixed $params = false): mixed { //TODO this is a hotfix to keep tidy from striping <', 'value')); // XSS + $this->assertEquals('', html::attr('>', 'value')); // XSS + $this->assertEquals('', html::attr('name', '')); + } +} diff --git a/tests/core/LocaleTest.php b/tests/core/LocaleTest.php new file mode 100644 index 00000000..537f8cab --- /dev/null +++ b/tests/core/LocaleTest.php @@ -0,0 +1,48 @@ +old_lang = GyroLocale::get_language(); + $this->old_charset = GyroLocale::get_charset(); + } + + protected function tearDown(): void { + GyroLocale::set_locale($this->old_lang, $this->old_charset); + } + + public function test_set_language() { + GyroLocale::set_language('fr'); + $this->assertEquals('fr', GyroLocale::get_language()); + GyroLocale::set_language('de'); + $this->assertEquals('de', GyroLocale::get_language()); + } + + public function test_set_charset() { + GyroLocale::set_charset('Latin1'); + $this->assertEquals('Latin1', GyroLocale::get_charset()); + GyroLocale::set_charset('UTF-8'); + $this->assertEquals('UTF-8', GyroLocale::get_charset()); + } + + public function test_get_locales() { + $arr = GyroLocale::get_locales('de'); + $this->assertContains('de_DE', $arr); + $this->assertContains('de', $arr); + + $arr = GyroLocale::get_locales('fr'); + $this->assertContains('fr_FR', $arr); + $this->assertContains('fr', $arr); + + $arr = GyroLocale::get_locales('en'); + $this->assertContains('en_US', $arr); + $this->assertContains('en', $arr); + + $arr = GyroLocale::get_locales('pt_BR'); + $this->assertContains('pt_BR', $arr); + $this->assertNotContains('pt_BR_PT_BR', $arr); + } +} diff --git a/tests/core/ModelShowCommandTest.php b/tests/core/ModelShowCommandTest.php new file mode 100644 index 00000000..faa8ffa9 --- /dev/null +++ b/tests/core/ModelShowCommandTest.php @@ -0,0 +1,190 @@ +assertStringContainsString('INT', $label); + $this->assertStringContainsString('UNSIGNED', $label); + $this->assertStringContainsString('AUTO_INCREMENT', $label); + } + + public function test_field_type_label_varchar() { + $field = new DBFieldText('name', 100, null, DBField::NOT_NULL); + $label = ModelShowCommand::get_field_type_label($field); + + $this->assertEquals('VARCHAR(100)', $label); + } + + public function test_field_type_label_text() { + $field = new DBFieldText('body', DBFieldText::BLOB_LENGTH_SMALL, null); + $label = ModelShowCommand::get_field_type_label($field); + + $this->assertEquals('VARCHAR(65535)', $label); + } + + public function test_field_type_label_bool() { + $field = new DBFieldBool('active', false); + $label = ModelShowCommand::get_field_type_label($field); + + $this->assertEquals('BOOL', $label); + } + + public function test_field_type_label_datetime() { + $field = new DBFieldDateTime('created'); + $label = ModelShowCommand::get_field_type_label($field); + + $this->assertEquals('DATETIME', $label); + } + + public function test_field_type_label_timestamp() { + $field = new DBFieldDateTime('modified', null, DBFieldDateTime::TIMESTAMP | DBField::NOT_NULL); + $label = ModelShowCommand::get_field_type_label($field); + + $this->assertEquals('TIMESTAMP', $label); + } + + public function test_field_type_label_float_unsigned() { + $field = new DBFieldFloat('price', 0, DBFieldFloat::UNSIGNED | DBField::NOT_NULL); + $label = ModelShowCommand::get_field_type_label($field); + + $this->assertStringContainsString('FLOAT', $label); + $this->assertStringContainsString('UNSIGNED', $label); + } + + public function test_default_label_auto_increment() { + $field = new DBFieldInt('id', null, DBFieldInt::AUTOINCREMENT); + $label = ModelShowCommand::get_default_label($field); + + $this->assertEquals('(auto)', $label); + } + + public function test_default_label_string() { + $field = new DBFieldText('status', 20, 'active', DBField::NOT_NULL); + $label = ModelShowCommand::get_default_label($field); + + $this->assertEquals('active', $label); + } + + public function test_default_label_null() { + $field = new DBFieldText('note', 255, null); + $label = ModelShowCommand::get_default_label($field); + + $this->assertEquals('NULL', $label); + } + + public function test_default_label_bool() { + $field = new DBFieldBool('active', true); + $label = ModelShowCommand::get_default_label($field); + + $this->assertEquals('TRUE', $label); + } + + public function test_field_to_sql_int_primary_key() { + $field = new DBFieldInt('id', null, DBFieldInt::AUTOINCREMENT | DBFieldInt::UNSIGNED | DBField::NOT_NULL); + $sql = ModelShowCommand::field_to_sql('id', $field); + + $this->assertStringContainsString('`id`', $sql); + $this->assertStringContainsString('INT', $sql); + $this->assertStringContainsString('UNSIGNED', $sql); + $this->assertStringContainsString('NOT NULL', $sql); + $this->assertStringContainsString('AUTO_INCREMENT', $sql); + } + + public function test_field_to_sql_varchar() { + $field = new DBFieldText('email', 100, null, DBField::NOT_NULL); + $sql = ModelShowCommand::field_to_sql('email', $field); + + $this->assertStringContainsString('`email`', $sql); + $this->assertStringContainsString('VARCHAR(100)', $sql); + $this->assertStringContainsString('NOT NULL', $sql); + } + + public function test_field_to_sql_nullable_with_default() { + $field = new DBFieldText('note', 255, null, DBField::NONE); + $sql = ModelShowCommand::field_to_sql('note', $field); + + $this->assertStringContainsString('NULL', $sql); + $this->assertStringContainsString('DEFAULT NULL', $sql); + } + + public function test_field_to_sql_timestamp() { + $field = new DBFieldDateTime('modificationdate', null, DBFieldDateTime::TIMESTAMP | DBField::NOT_NULL); + $sql = ModelShowCommand::field_to_sql('modificationdate', $field); + + $this->assertStringContainsString('TIMESTAMP', $sql); + $this->assertStringContainsString('CURRENT_TIMESTAMP', $sql); + } + + public function test_generate_create_sql() { + $fields = array( + 'id' => new DBFieldInt('id', null, DBFieldInt::AUTOINCREMENT | DBFieldInt::UNSIGNED | DBField::NOT_NULL), + 'name' => new DBFieldText('name', 40, null, DBField::NOT_NULL), + ); + $keys = array('id' => $fields['id']); + + $model_info = array( + 'table' => 'test_table', + 'fields' => $fields, + 'keys' => $keys, + ); + + $sql = ModelShowCommand::generate_create_sql($model_info); + + $this->assertStringContainsString('CREATE TABLE `test_table`', $sql); + $this->assertStringContainsString('`id`', $sql); + $this->assertStringContainsString('`name`', $sql); + $this->assertStringContainsString('PRIMARY KEY (id)', $sql); + $this->assertStringContainsString('ENGINE=InnoDB', $sql); + } + + public function test_flags_label_internal() { + $field = new DBFieldText('hash_type', 5, 'bcryp', DBField::NOT_NULL | DBField::INTERNAL); + $label = ModelShowCommand::get_flags_label($field); + + $this->assertEquals('INTERNAL', $label); + } + + public function test_flags_label_empty() { + $field = new DBFieldText('name', 100, null, DBField::NOT_NULL); + $label = ModelShowCommand::get_flags_label($field); + + $this->assertEquals('', $label); + } + + public function test_discover_models_finds_core_models() { + // The test bootstrap loads core model classes (cache, formvalidations, sessions) + $models = ModelListCommand::discover_models(); + + $this->assertNotEmpty($models, 'Should find at least one model'); + + // All discovered models must have required keys + foreach ($models as $model) { + $this->assertArrayHasKey('class', $model); + $this->assertArrayHasKey('table', $model); + $this->assertArrayHasKey('field_count', $model); + $this->assertArrayHasKey('primary_key', $model); + $this->assertGreaterThan(0, $model['field_count']); + } + } + + public function test_load_model_info_from_file() { + $file = GYRO_ROOT_DIR . 'modules/simpletest/model/classes/studentstest.model.php'; + $info = ModelListCommand::load_model_info($file); + + $this->assertNotFalse($info); + // Class name may be DAOStudentstest (derived) or DAOStudentsTest (actual) + $this->assertStringStartsWith('daostudentstest', strtolower($info['class'])); + $this->assertEquals('studentstest', $info['table']); + $this->assertEquals(3, $info['field_count']); + $this->assertEquals('id', $info['primary_key']); + } +} diff --git a/tests/core/ParameterizedRouteTest.php b/tests/core/ParameterizedRouteTest.php new file mode 100644 index 00000000..df0af84a --- /dev/null +++ b/tests/core/ParameterizedRouteTest.php @@ -0,0 +1,191 @@ +weight_against_path('some/url'); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $weight); + + $weight = $token1->weight_against_path('some/url/string'); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $weight); + + $noweight = $token1->weight_against_path('totally/different'); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $noweight); + } + + public function test_int() { + $token1 = new ParameterizedRoute('some/url/{test:i}', null, ''); + + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/123')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/-123')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/0')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/string')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/123string')); + } + + public function test_unsigned_int() { + $token1 = new ParameterizedRoute('some/url/{test:ui}', null, ''); + + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/123')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/-123')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/0')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/string')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/123string')); + } + + public function test_unsigned_positive_int() { + $token1 = new ParameterizedRoute('some/url/{test:ui>}', null, ''); + + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/123')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/-123')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/0')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/string')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/123string')); + } + + public function test_string() { + $token1 = new ParameterizedRoute('some/url/{test:s}', null, ''); + + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/123')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/-123')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/0')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/string')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/123/string')); + + $token2 = new ParameterizedRoute('some/url/{test:s}.html', null, ''); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token2->weight_against_path('some/url/123.html')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token2->weight_against_path('some/url/123.htm')); + + $token3 = new ParameterizedRoute('some/url/{test:s:2}', null, ''); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token3->weight_against_path('some/url/abc')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token3->weight_against_path('some/url/ab')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token3->weight_against_path('some/url/a')); + } + + public function test_string_plain() { + $token1 = new ParameterizedRoute('some/url/{test:sp}', null, ''); + + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/123')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/-123')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/_123')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/0')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/string')); + + // No matches + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/123/string')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/!123')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/#123')); + + $token2 = new ParameterizedRoute('some/url/{test:sp}.html', null, ''); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token2->weight_against_path('some/url/123.html')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token2->weight_against_path('some/url/123.htm')); + + $token3 = new ParameterizedRoute('some/url/{test:sp:2}', null, ''); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token3->weight_against_path('some/url/abc')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token3->weight_against_path('some/url/ab')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token3->weight_against_path('some/url/a')); + } + + public function test_enum() { + $token1 = new ParameterizedRoute('some/url/{test:e:one,two,three}', null, ''); + + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/one')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/two')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/three')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/four')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/123/one_string')); + } + + public function test_complex() { + $token1 = new ParameterizedRoute('some/{url:e:url,test}/{a:i}-{b:ui}.text.{c:s}{i:ui>}.html', null, ''); + + $weight = $token1->weight_against_path('some/url/-1-2.text.abc2.html'); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $weight); + } + + public function test_placeholders() { + // * + $token1 = new ParameterizedRoute('some/url/{path:s}*', null, ''); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/one')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/one/two')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/')); + + // % + $token1 = new ParameterizedRoute('some/url/{path:s}%', null, ''); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/one')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/one/two')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/')); + + // ! + $token1 = new ParameterizedRoute('some/url/{path:s}!', null, ''); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/one')); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $token1->weight_against_path('some/url/one/two')); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $token1->weight_against_path('some/url/')); + } + + public function test_build_url() { + $token1 = new ParameterizedRoute('some/{url}/{path:s}*', null, ''); + $url = $token1->build_url(false, array('url' => 'url', 'path' => 'path')); + $this->assertEquals($this->normalizePath('/some/url/path'), $this->normalizePath($url)); + + $url = $token1->build_url(false, array('url' => 'url')); + $this->assertEquals($this->normalizePath('/some/url/'), $this->normalizePath($url)); + + $token1 = new ParameterizedRoute('some/{url}/{path:s}%', null, ''); + $url = $token1->build_url(false, array('url' => 'url', 'path' => 'path')); + $this->assertEquals($this->normalizePath('/some/url/path'), $this->normalizePath($url)); + + $url = $token1->build_url(false, array('url' => 'url')); + $this->assertEquals($this->normalizePath('/some/url/{path:s}%'), $this->normalizePath($url)); + + $token1 = new ParameterizedRoute('some/{url}/{path:s}*', null, ''); + $url = $token1->build_url(false, array('url' => 'url', 'path' => 'some/path%')); + $this->assertEquals($this->normalizePath('/some/url/some/path%25'), $this->normalizePath($url)); + + $token1 = new ParameterizedRoute('some/{url}/{path:s}-*', null, ''); + $url = $token1->build_url(false, array('url' => 'url', 'path' => 'path')); + $this->assertEquals($this->normalizePath('/some/url/path-*'), $this->normalizePath($url)); + + $token1 = new ParameterizedRoute('some/{url}/{path:s}!', null, ''); + $url = $token1->build_url(false, array('url' => 'url', 'path' => 'path')); + $this->assertEquals($this->normalizePath('/some/url/path'), $this->normalizePath($url)); + + $url = $token1->build_url(false, array('url' => 'url')); + $this->assertEquals($this->normalizePath('/some/url/'), $this->normalizePath($url)); + + $token1 = new ParameterizedRoute('some/{url}/{url_path:s}!', null, ''); + $url = $token1->build_url(false, array('url' => 'url', 'url_path' => 'path')); + $this->assertEquals($this->normalizePath('/some/url/path'), $this->normalizePath($url)); + } + + public function test_build_url_sp() { + $token1 = new ParameterizedRoute('some/{test:sp}', null, ''); + + $url = $token1->build_url(false, array('test' => '_test')); + $this->assertEquals($this->normalizePath('/some/_test'), $this->normalizePath($url)); + + $url = $token1->build_url(false, array('test' => '!test-ö!')); + $this->assertEquals($this->normalizePath('/some/test-oe'), $this->normalizePath($url)); + } + + public function test_function_parameter() { + $token1 = new ParameterizedRoute('some/{test():sp}', null, ''); + + $url = $token1->build_url(false, array('test()' => '_test')); + $this->assertEquals($this->normalizePath('/some/_test'), $this->normalizePath($url)); + + $url = $token1->build_url(false, new ParameterizedRouteTestMockObjectPHPUnit()); + $this->assertEquals($this->normalizePath('/some/test_'), $this->normalizePath($url)); + } +} diff --git a/tests/core/PathStackTest.php b/tests/core/PathStackTest.php new file mode 100644 index 00000000..cd77227d --- /dev/null +++ b/tests/core/PathStackTest.php @@ -0,0 +1,20 @@ +assertEquals('some', $stack->current()); + } + + public function test_next() { + $stack = new PathStack('/some/path/'); + $this->assertEquals('path', $stack->next()); + } + + public function test_adjust() { + $stack = new PathStack('/some/path/to/somewhere'); + $this->assertTrue($stack->adjust('some/path/')); + $this->assertEquals('to', $stack->current()); + } +} diff --git a/tests/core/RefererTest.php b/tests/core/RefererTest.php new file mode 100644 index 00000000..006590fc --- /dev/null +++ b/tests/core/RefererTest.php @@ -0,0 +1,68 @@ +assertTrue($refer->is_empty()); + + $refer = Referer::create(' '); + $this->assertTrue($refer->is_empty()); + + $refer = Referer::create(null); + $this->assertTrue($refer->is_empty()); + + $refer = Referer::create('-'); + $this->assertFalse($refer->is_empty()); + } + + public function test_internal() { + $referer = new Referer(); + $this->assertFalse($referer->is_internal()); + + $referer = new Referer(Url::current()->build()); + $this->assertTrue($referer->is_internal()); + + $referer = new Referer('http://www.google.de?q=34343434'); + $this->assertFalse($referer->is_internal()); + } + + public function test_external() { + $referer = new Referer(); + $this->assertFalse($referer->is_external()); + + $referer = new Referer(Url::current()->build()); + $this->assertFalse($referer->is_external()); + + $referer = new Referer('http://www.google.de?q=34343434'); + $this->assertTrue($referer->is_external()); + } + + public function test_searchengine() { + $referer = new Referer(); + $this->assertFalse($referer->search_engine_info()); + + $referer = new Referer(Url::current()->build()); + $this->assertFalse($referer->search_engine_info()); + + $referer = new Referer('http://www.google.de?q=searchme'); + $sei = $referer->search_engine_info(); + $this->assertIsArray($sei); + $this->assertEquals('google', $sei['searchengine']); + $this->assertEquals('google.de', $sei['domain']); + $this->assertEquals('www.google.de', $sei['host']); + $this->assertEquals('searchme', $sei['keywords']); + + $test = 'http://www.google.de/search?hl=de&q=www.weihnachtspl%C3%A4tzchen.de&meta='; + $referer = new Referer($test); + $this->assertEquals($test, $referer->build()); + $sei = $referer->search_engine_info(); + $this->assertEquals('www.weihnachtsplätzchen.de', $sei['keywords']); + } + + public function test_fragment() { + $in = 'http://www.example.org/in/a/dir/#p403187'; + $test = Referer::create($in); + $this->assertEquals($in, $test->build()); + } +} diff --git a/tests/core/RequestInfoTest.php b/tests/core/RequestInfoTest.php new file mode 100644 index 00000000..2bdb0d6c --- /dev/null +++ b/tests/core/RequestInfoTest.php @@ -0,0 +1,97 @@ + 'on')); + $this->assertTrue($ri->is_ssl()); + } + + public function test_is_ssl_with_https_off() { + $ri = RequestInfo::create(array('HTTPS' => 'off')); + $this->assertFalse($ri->is_ssl()); + } + + public function test_is_ssl_with_port_443() { + $ri = RequestInfo::create(array('SERVER_PORT' => '443')); + $this->assertTrue($ri->is_ssl()); + } + + public function test_is_ssl_with_port_80() { + $ri = RequestInfo::create(array('SERVER_PORT' => '80')); + $this->assertFalse($ri->is_ssl()); + } + + public function test_is_ssl_no_https() { + $ri = RequestInfo::create(array()); + $this->assertFalse($ri->is_ssl()); + } + + public function test_method() { + $ri = RequestInfo::create(array('REQUEST_METHOD' => 'POST')); + $this->assertEquals('POST', $ri->method()); + + $ri = RequestInfo::create(array('REQUEST_METHOD' => 'get')); + $this->assertEquals('GET', $ri->method()); + + // Default is GET + $ri = RequestInfo::create(array()); + $this->assertEquals('GET', $ri->method()); + } + + public function test_is_forwarded() { + $ri = RequestInfo::create(array('HTTP_X_FORWARDED_FOR' => '1.2.3.4')); + $this->assertTrue($ri->is_forwarded()); + + $ri = RequestInfo::create(array()); + $this->assertFalse($ri->is_forwarded()); + } + + public function test_remote_address() { + $ri = RequestInfo::create(array('REMOTE_ADDR' => '192.168.1.1')); + $this->assertEquals('192.168.1.1', $ri->remote_address()); + + // Forwarded takes precedence + $ri = RequestInfo::create(array( + 'REMOTE_ADDR' => '192.168.1.1', + 'HTTP_X_FORWARDED_FOR' => '10.0.0.1' + )); + $this->assertEquals('10.0.0.1', $ri->remote_address(true)); + $this->assertEquals('192.168.1.1', $ri->remote_address(false)); + } + + public function test_remote_address_comma_separated() { + $ri = RequestInfo::create(array( + 'HTTP_X_FORWARDED_FOR' => '10.0.0.1, 10.0.0.2' + )); + $this->assertEquals('10.0.0.1', $ri->remote_address()); + } + + public function test_user_agent() { + $ri = RequestInfo::create(array('HTTP_USER_AGENT' => 'TestBot/1.0')); + $this->assertEquals('TestBot/1.0', $ri->user_agent()); + + $ri = RequestInfo::create(array()); + $this->assertEquals('', $ri->user_agent()); + } + + public function test_header_value() { + $ri = RequestInfo::create(array( + 'HTTP_ACCEPT' => 'text/html', + 'HTTP_ACCEPT_LANGUAGE' => 'en-US', + 'HTTP_X_CUSTOM_HEADER' => 'custom-value' + )); + $this->assertEquals('text/html', $ri->header_value('Accept')); + $this->assertEquals('en-US', $ri->header_value('Accept-Language')); + $this->assertEquals('custom-value', $ri->header_value('X-Custom-Header')); + $this->assertEquals('', $ri->header_value('Nonexistent')); + } + + public function test_referer() { + $ri = RequestInfo::create(array('HTTP_REFERER' => 'http://example.com')); + $this->assertEquals('http://example.com', $ri->referer()); + + $ri = RequestInfo::create(array()); + $this->assertEquals('', $ri->referer()); + } +} diff --git a/tests/core/RouteBaseTest.php b/tests/core/RouteBaseTest.php new file mode 100644 index 00000000..7a43353d --- /dev/null +++ b/tests/core/RouteBaseTest.php @@ -0,0 +1,116 @@ +invoked = true; + } +} + +class RouteBaseTest extends TestCase { + private function normalizePath(string $path): string { + $parsed = parse_url($path); + return isset($parsed['path']) ? $parsed['path'] : '/' . ltrim($path, '/'); + } + + public function test_invoke() { + $controler = new RouteTestControllerPHPUnit(); + $token = new RouteBase('some/url', $controler, 'invoke_me'); + + $data = new PageData(null, $_GET, $_POST); + $token->invoke($data); + + $this->assertTrue($controler->invoked); + } + + public function test_initialize() { + $controler = new RouteTestControllerPHPUnit(); + $token = new RouteBase('some/url', $controler, 'invoke_me'); + + $data = new PageData(null, $_GET, $_POST); + $data->set_path('some/url/to/process'); + $token->initialize($data); + + $this->assertNotNull($data->get_cache_manager()); + $this->assertEquals('to', $data->get_pathstack()->current()); + } + + public function test_weight() { + $token1 = new RouteBase('some/url', null, '', ''); + + $weight = $token1->weight_against_path('some/url'); + $this->assertEquals(RouteBase::WEIGHT_FULL_MATCH, $weight); + + $weight = $token1->weight_against_path('some/url/string'); + $this->assertEquals(1, $weight); + + $weight2 = $token1->weight_against_path('some/url/stringdingsbums'); + $this->assertEquals($weight, $weight2); + + $weight3 = $token1->weight_against_path('some/url/string/dings'); + $this->assertTrue($weight < $weight3); + $this->assertTrue($weight2 < $weight3); + + $noweight = $token1->weight_against_path('totally/different'); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $noweight); + + $token2 = new RouteBase('.', null, '', ''); + + $weight = $token2->weight_against_path('some/url'); + $this->assertEquals(RouteBase::WEIGHT_NO_MATCH, $weight); + + $weight2 = $token2->weight_against_path('.'); + $this->assertEquals(0, $weight2); + } + + public function test_identify() { + $route = new RouteBase('path', new RouteTestControllerPHPUnit(), 'action'); + $this->assertEquals('RouteTestControllerPHPUnit::action', $route->identify()); + } + + public function test_build_url() { + $route = new RouteBase('path', new RouteTestControllerPHPUnit(), 'action'); + $this->assertEquals( + $this->normalizePath(Config::get_url(Config::URL_BASEDIR) . 'path'), + $this->normalizePath($route->build_url(RouteBase::RELATIVE)) + ); + $this->assertEquals(Config::get_url(Config::URL_BASEURL) . 'path', $route->build_url(RouteBase::ABSOLUTE)); + + $this->assertEquals( + $this->normalizePath(Config::get_value(Config::URL_BASEDIR) . 'path/some/params'), + $this->normalizePath($route->build_url(RouteBase::RELATIVE, array('some', 'params'))) + ); + $this->assertEquals( + $this->normalizePath(Config::get_value(Config::URL_BASEDIR) . 'path/a_param'), + $this->normalizePath($route->build_url(RouteBase::RELATIVE, 'a_param')) + ); + + $this->assertEquals(Config::get_url(Config::URL_BASEURL) . 'path/some/params', $route->build_url(RouteBase::ABSOLUTE, array('some', 'params'))); + $this->assertEquals(Config::get_url(Config::URL_BASEURL) . 'path/a_param', $route->build_url(RouteBase::ABSOLUTE, 'a_param')); + + // Test HTTPS stuff + $route = new RouteBase('https://path', new RouteTestControllerPHPUnit(), 'action'); + if (Config::has_feature(Config::ENABLE_HTTPS)) { + $this->assertEquals( + $this->normalizePath(Config::get_url(Config::URL_BASEURL_SAFE) . 'path'), + $this->normalizePath($route->build_url(RouteBase::RELATIVE)) + ); + $this->assertEquals(Config::get_url(Config::URL_BASEURL_SAFE) . 'path', $route->build_url(RouteBase::ABSOLUTE)); + } else { + $this->assertEquals( + $this->normalizePath(Config::get_value(Config::URL_BASEDIR) . 'path'), + $this->normalizePath($route->build_url(RouteBase::RELATIVE)) + ); + $this->assertEquals(Config::get_url(Config::URL_BASEURL) . 'path', $route->build_url(RouteBase::ABSOLUTE)); + } + + $route = new RouteBase('http://path', new RouteTestControllerPHPUnit(), 'action'); + $this->assertEquals( + $this->normalizePath(Config::get_value(Config::URL_BASEDIR) . 'path'), + $this->normalizePath($route->build_url(RouteBase::RELATIVE)) + ); + $this->assertEquals(Config::get_url(Config::URL_BASEURL) . 'path', $route->build_url(RouteBase::ABSOLUTE)); + } +} diff --git a/tests/core/RuntimeCacheTest.php b/tests/core/RuntimeCacheTest.php new file mode 100644 index 00000000..f1b95641 --- /dev/null +++ b/tests/core/RuntimeCacheTest.php @@ -0,0 +1,18 @@ +assertEquals(12345, RuntimeCache::get('key', false)); + + RuntimeCache::set('key', 'abcde'); + $this->assertEquals('abcde', RuntimeCache::get('key', false)); + + RuntimeCache::set(array('key2'), 12345); + $this->assertEquals(12345, RuntimeCache::get(array('key2'), false)); + + RuntimeCache::set(array('key2'), 'abcde'); + $this->assertEquals('abcde', RuntimeCache::get(array('key2'), false)); + } +} diff --git a/tests/core/StatusTest.php b/tests/core/StatusTest.php new file mode 100644 index 00000000..f5bb7d28 --- /dev/null +++ b/tests/core/StatusTest.php @@ -0,0 +1,94 @@ +assertTrue($s->is_ok()); + + $s = new Status(''); + $this->assertTrue($s->is_ok()); + + $s = new Status('message'); + $this->assertFalse($s->is_ok()); + $this->assertEquals('message', $s->to_string()); + + $s = new Message('message'); + $this->assertTrue($s->is_ok()); + $this->assertEquals('message', $s->to_string()); + } + + public function test_append(): void { + $s = new Status(); + $this->assertTrue($s->is_ok()); + + $s->append('message1'); + $this->assertFalse($s->is_ok()); + $this->assertEquals('message1', $s->to_string()); + + $s->append('message2'); + $this->assertFalse($s->is_ok()); + $this->assertEquals('message1
message2', $s->to_string()); + + $s = new Message('message1'); + $this->assertTrue($s->is_ok()); + $this->assertEquals('message1', $s->to_string()); + + $s->append('message2'); + $this->assertTrue($s->is_ok()); + $this->assertEquals('message1
message2', $s->to_string()); + } + + public function test_merge(): void { + $s = new Status(); + $s2 = new Status(); + + $s->merge($s2); + $this->assertTrue($s->is_ok()); + + $s2->append('message1'); + $s->merge($s2); + $this->assertFalse($s->is_ok()); + $this->assertEquals('message1', $s->to_string()); + + $s->merge($s2); + $this->assertFalse($s->is_ok()); + $this->assertEquals('message1', $s->to_string()); // Messages are unique + + // Exception + $s = new Status(); + $s2 = new \Exception('message1'); + $s->merge($s2); + $this->assertFalse($s->is_ok()); + $this->assertEquals('message1', $s->to_string()); + + // String + $s = new Status(); + $s->merge('message1'); + $this->assertFalse($s->is_ok()); + $this->assertEquals('message1', $s->to_string()); + + $s->merge('message2'); + $this->assertEquals('message1
message2', $s->to_string()); + } + + public function test_to_string(): void { + $s = new Status(); + $this->assertEquals('', $s->to_string()); + $this->assertEquals('', $s->to_string(Status::OUTPUT_PLAIN)); + + $s = new Status('message1'); + $this->assertEquals('message1', $s->to_string()); + $this->assertEquals('message1', $s->to_string(Status::OUTPUT_PLAIN)); + + $s->append('\">'>" => '' => 'js', + '' => 'alert', + '