diff --git a/.paul/codebase/README.md b/.paul/codebase/README.md new file mode 100644 index 0000000..e24253c --- /dev/null +++ b/.paul/codebase/README.md @@ -0,0 +1,33 @@ +# Codebase Map — cmsPRO + +> Generated: 2026-04-26 | Auto-generated by /paul:map-codebase + +## Documents + +| File | Contents | +|------|---------| +| [overview.md](overview.md) | Project summary, modules, entry points, refactoring status | +| [stack.md](stack.md) | PHP runtime, database, frontend libs, server config, external services | +| [architecture.md](architecture.md) | Directory map, patterns, routing, caching, namespaces | +| [conventions.md](conventions.md) | Naming, class patterns, PHPDoc, return types, DB access | +| [testing.md](testing.md) | PHPUnit setup, test structure, stubs, adding new tests | +| [integrations.md](integrations.md) | Email, geolocation, analytics, update server, file manager | +| [concerns.md](concerns.md) | Technical debt prioritized CRITICAL → HIGH → MEDIUM → LOW | + +## Quick Reference + +- **Architecture**: Controls → (deprecated) Factories → Domain Repositories → Medoo/MySQL +- **New code goes in**: `autoload/Domain/{Entity}/{Entity}Repository.php` +- **Tests go in**: `tests/Unit/Domain/{Entity}/{Entity}RepositoryTest.php` +- **Global helper**: `\S::method()` (legacy) or `\Shared\Helpers\Helpers::method()` (preferred) +- **Templates**: `templates/{module}/template.php` (user override: `templates_user/`) +- **CSRF**: `\Shared\Security\CsrfToken::getToken()` / `::validate($token)` +- **Cache**: `\Shared\Cache\CacheHandler::store($key, $data, $ttl)` / `::fetch($key)` + +## Top Issues to Fix + +1. **CRITICAL**: `unserialize()` on cookie — `admin/ajax/pages.php:36,49` +2. **CRITICAL**: Path traversal in updates — `autoload/admin/factory/class.Update.php:76-80` +3. **HIGH**: Missing input validation everywhere +4. **HIGH**: Password hash in auto-login cookie — `admin/index.php:59-61` +5. **MEDIUM**: God class Helpers.php (1220 lines) — needs splitting diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md new file mode 100644 index 0000000..82c17be --- /dev/null +++ b/.paul/codebase/architecture.md @@ -0,0 +1,160 @@ +# Architecture + +> Generated: 2026-04-26 + +## Overview + +cmsPRO uses a **3-layer architecture** with clean admin/frontend separation: + +``` +Request + ↓ +Controls (admin\controls\ or front\controls\) ← request handling + ↓ +Factories (admin\factory\ or front\factory\) ← DEPRECATED wrappers → will be removed + ↓ +Domain Repositories (Domain\*\*Repository) ← data access (new pattern) + ↓ +Medoo ORM → MySQL +``` + +Views are rendered through `admin\view\*` / `front\view\*` → `Shared\Tpl\Tpl` → Savant3 templates. + +## Directory Map + +``` +autoload/ +├── autoloader.php Hybrid PSR-4 + legacy autoloader +├── class.S.php Global helper facade (deprecated wrapper) +├── class.Article.php Legacy entity (ArrayAccess) +├── class.Page.php Legacy entity +├── class.Scontainer.php Legacy entity +├── class.Cache.php / class.Cron.php / class.Image.php / class.Html.php +│ +├── Domain/ NEW — Repository pattern, DDD +│ ├── Articles/ArticlesRepository.php (648 lines) +│ ├── Authors/AuthorsRepository.php (156 lines) +│ ├── Banners/BannersRepository.php (148 lines) +│ ├── Languages/LanguagesRepository.php (213 lines) +│ ├── Layouts/LayoutsRepository.php (123 lines) +│ ├── Newsletter/NewsletterRepository.php (281 lines) +│ ├── Pages/PagesRepository.php (451 lines) +│ ├── Scontainers/ScontainersRepository.php (110 lines) +│ ├── Settings/SettingsRepository.php (73 lines) +│ └── User/UserRepository.php (235 lines) +│ +├── Shared/ Cross-cutting services +│ ├── Helpers/Helpers.php God class — 1220 lines (⚠ needs splitting) +│ ├── Tpl/Tpl.php Template renderer (checks templates_user/ first) +│ ├── Email/Email.php Email service (wraps PHPMailer) +│ ├── Cache/CacheHandler.php File-based cache (gzdeflate, TTL) +│ ├── Security/CsrfToken.php CSRF token generation + validation +│ ├── Html/Html.php HTML form element builder +│ └── Image/ImageManipulator.php Image processing +│ +├── admin/ +│ ├── class.Site.php Admin routing + 2FA +│ ├── controls/class.*.php 18 request handler classes (static methods) +│ ├── factory/class.*.php 18 @deprecated wrappers +│ └── view/class.*.php 14 template renderer classes +│ +└── front/ + ├── controls/class.Site.php Main frontend router + ├── controls/class.*.php 4 frontend controllers + ├── factory/class.*.php 17 frontend factories + └── view/class.*.php View renderers + +admin/ +├── index.php Admin entry point (IP check, session, routing) +├── ajax.php Admin AJAX dispatcher → admin/ajax/*.php +└── templates/ Admin Savant3 templates (per module) + +templates/ Frontend Savant3 templates +templates_user/ User-overridable template overrides +plugins/ +├── special-actions.php Hook: pre-routing +├── special-actions-middle.php Hook: mid-request +└── special-actions-end.php Hook: post-rendering +``` + +## Namespace Convention + +| Namespace | Path | Convention | +|-----------|------|-----------| +| `admin\controls\` | `autoload/admin/controls/class.*.php` | Legacy lowercase | +| `admin\factory\` | `autoload/admin/factory/class.*.php` | Legacy, @deprecated | +| `admin\view\` | `autoload/admin/view/class.*.php` | Legacy lowercase | +| `front\controls\` | `autoload/front/controls/class.*.php` | Legacy lowercase | +| `front\factory\` | `autoload/front/factory/class.*.php` | Legacy lowercase | +| `Domain\*\` | `autoload/Domain/*/ClassName.php` | PSR-4 PascalCase | +| `Shared\*\` | `autoload/Shared/*/ClassName.php` | PSR-4 PascalCase | + +## Key Patterns + +### Repository Pattern (Domain layer) +```php +class ArticlesRepository { + public function __construct($db) { $this->db = $db; } + public function find(int $id): ?array { ... } + public function save(...): int { ... } +} +``` + +### Factory Wrapper (deprecated bridge) +```php +/** @deprecated Używaj Domain\Articles\ArticlesRepository przez DI */ +class Articles { + private static function repo(): ArticlesRepository { + global $mdb; + return new ArticlesRepository($mdb); + } + public static function article_delete($id): bool { + return self::repo()->deleteArticle($id); + } +} +``` + +### Controls (request handler) +```php +class Articles { + public static function article_delete() { + global $user; + if (!admin\factory\Users::check_privileges('articles', $user['id'])) + return \S::alert('Brak uprawnień'); + // delegate to factory → repository + } +} +``` + +### Global Helper Facade +```php +// class.S.php — calls Shared\Helpers\Helpers via __callStatic +\S::get('param') // → Helpers::get() +\S::delete_cache() // → Helpers::delete_cache() +``` + +## Admin Routing + +`GET /admin/?a=articles&action=view_list` → `admin\controls\Articles::view_list()` + +Routing in `admin/index.php`: reads `$_GET['a']` → dynamically loads control class → calls action method. + +## Frontend Routing + +`index.php` → `front\controls\Site::route()` — checks `\S::get('search')`, `\S::get('tag')`, `\S::get('article')`, then falls through to page rendering by `page_type`. + +## Caching Strategy + +| Cache Type | Location | Engine | +|-----------|----------|--------| +| Page cache | `cache/` | Full HTML output | +| Object cache | `temp/md5[0]/md5[1]/` | gzdeflate + serialize, TTL | +| WebP images | `cache/` | Filesystem | +| Language strings | `$_SESSION` | PHP session | + +## Plugin System + +3 hook points in frontend lifecycle (files in `plugins/` directory, included if they exist): +1. `special-actions.php` — after language init, before routing +2. `special-actions-middle.php` — before cache check +3. `special-actions-end.php` — before final output diff --git a/.paul/codebase/concerns.md b/.paul/codebase/concerns.md new file mode 100644 index 0000000..927ad7d --- /dev/null +++ b/.paul/codebase/concerns.md @@ -0,0 +1,149 @@ +# Technical Debt & Concerns + +> Generated: 2026-04-26 | Prioritized by severity + +## CRITICAL + +### C1 — Unserialize on User-Controlled Cookies +**File**: `admin/ajax/pages.php` lines 36, 49 +**Code**: `$array = unserialize($_COOKIE['cookie_menus']);` +**Risk**: Object injection / RCE — classic PHP vulnerability. +**Fix**: Replace with `json_decode($_COOKIE['cookie_menus'] ?? '{}', true)`. + +### C2 — Path Traversal in Update File Deletion +**File**: `autoload/admin/factory/class.Update.php` lines 76-80, 119-128 +**Code**: `unlink('../' . $filePath)` — `$filePath` from JSON manifest, not validated. +**Risk**: Attacker-controlled manifest could delete arbitrary files. +**Fix**: +```php +$full = realpath('../' . $filePath); +$base = realpath('../'); +if (strpos($full, $base) !== 0) throw new \Exception('Path traversal'); +unlink($full); +``` + +### C3 — God Class: Helpers.php (1220 lines, 75+ static methods) +**File**: `autoload/Shared/Helpers/Helpers.php` +**Risk**: Unmaintainable, untestable, global state dependency (`global $mdb, $settings, $lang`). +**Domains mixed**: image processing, HTML DOM, caching, SEO, authentication, dates, session. +**Fix**: Extract into focused service classes (`ImageService`, `SeoHelper`, `DateHelper`, etc.). + +--- + +## HIGH + +### H1 — Direct Superglobal Access Without Validation +**File**: `autoload/Shared/Helpers/Helpers.php` lines 25-26 +**Code**: `$crop_w = $_GET['c_w'];` — no isset, no type check. +**Also**: `admin/ajax/pages.php` lines 36, 49 — `\S::get()` passed directly to queries. +**Fix**: Centralized request wrapper with typed getters. + +### H2 — SQL String Concatenation (String Values) +**File**: `autoload/Domain/Articles/ArticlesRepository.php` lines 53, 68, 87 and others. +**Code**: `"... WHERE article_id = " . (int)$id` — integer cast OK, but pattern is dangerous for string params. +**Fix**: Use Medoo parameterized methods exclusively. Audit and replace all raw `query()` calls. + +### H3 — No Input Validation / Sanitization Layer +**All entry points** — no `Validator` or `Sanitizer` class. Values flow from `$_GET`/`$_POST` → repository without validation. +**Fix**: Add validation at control layer before delegation to factory/repository. + +### H4 — Password Hash in Cookie +**File**: `admin/index.php` lines 59-61 +**Code**: `$obj = json_decode($_COOKIE[$cookie_name]); $password = $obj->{'hash'};` +**Risk**: Cookie exposure leaks credential hash, no HMAC signing. +**Fix**: Use signed JWT or HMAC-signed remember-me token, never store hashes in cookies. + +### H5 — Update Download Without Signature Verification +**File**: `autoload/admin/factory/class.Update.php` lines 12, 25, 28 +**Code**: `file_get_contents('https://www.cmspro.project-dc.pl/updates/...')` +**Risk**: MITM, supply chain — ZIP extracted without verifying integrity beyond SHA256 (if present). +**Fix**: Verify SHA256 checksum server-side before extraction; use curl with `CURLOPT_SSL_VERIFYPEER`. + +### H6 — Deprecated `mime_content_type()` Removed in PHP 8.1 +**File**: `autoload/Shared/Helpers/Helpers.php` line 39 +**Fix**: +```php +$finfo = finfo_open(FILEINFO_MIME_TYPE); +$type = finfo_file($finfo, $file); +finfo_close($finfo); +``` + +--- + +## MEDIUM + +### M1 — Global Variables as Dependency Injection +**Files**: Factory classes (`global $mdb`, `global $user`), Helpers (`global $settings, $lang`). +**Risk**: Untestable, tightly coupled, order-dependent initialization. +**Fix**: Pass `$mdb` to factories/repositories directly; remove `global` from repository code. + +### M2 — Repository Classes Contain Business Logic and Side Effects +**File**: `autoload/Domain/Articles/ArticlesRepository.php` line 45, 59 +**Code**: `\S::delete_cache()` and `\S::seo()` called inside repository methods. +**Fix**: Repositories should only do DB operations; call side effects in factories/services. + +### M3 — Mixed Procedural + OOP AJAX Handlers +**Files**: `admin/ajax/pages.php`, `admin/ajax/articles.php`, `admin/ajax/users.php` +**Pattern**: 50-90 line `if ($a == '...')` chains, no routing abstraction. +**Fix**: Create `AjaxRouter` + controller base class. + +### M4 — No Request/Response Abstraction +**All entry points** — `$_GET`/`$_POST` accessed directly everywhere. +**Fix**: `Request` class (typed getters) + `JsonResponse` class. + +### M5 — Error Suppression with `@` Operator +**Files**: `admin/index.php` lines 2, 14; Helpers.php lines 40, 98, 111, 1188-1200 +**Code**: `@file_get_contents(...)`, `@unlink(...)`. +**Fix**: Use `if (file_exists())` guards and proper try/catch. + +### M6 — Uninitialized Variables +**File**: `autoload/Domain/Articles/ArticlesRepository.php` line 72 +**Code**: `if ($out == '')` — `$out` never declared. +**Fix**: `$out = '';` before the loop. + +### M7 — No Interface Contracts for Repositories +All 10 repositories share identical method signatures but no shared interface. +**Fix**: Define `RepositoryInterface` with `find()`, `all()`, `save()`, `delete()`. + +### M8 — Hardcoded Values +- Update base URL: `'https://www.cmspro.project-dc.pl/updates/'` in 3 files +- File permissions: `chmod(..., 0755)` in 25 places +- Cookie expiry: `time() + 3600 * 24 * 365` as magic number +**Fix**: Extract to constants in a config class. + +--- + +## LOW + +### L1 — Backup Files in Repository +`libraries/medoo/medoo.bck.php` (973 lines), `libraries/grid/gdb.min.bck.php` (957 lines). +**Fix**: Delete; use Git for history. + +### L2 — `test.php` in Project Root (700 lines) +Production benchmark/test script accessible via HTTP. Contains DB credentials in lines 15-17. +**Fix**: Remove or move to `tests/` with `.htaccess` protection. + +### L3 — Legacy `class.S.php` Wrapper +200+ calls to `\S::*` throughout codebase — double indirection through `__callStatic`. +**Fix**: Gradual rename campaign to `\Shared\Helpers\Helpers::*`. + +### L4 — Legacy SQL Update Fallback Format +`class.Update.php` lines 97-132 — parses old `_sql.txt` format alongside new JSON manifest. +**Fix**: Deprecate and remove once all deployments are on manifest format. + +### L5 — Update Process Without Rollback +SQL runs before file extraction. If extraction fails, DB is inconsistent. No transaction wrapping. +**Fix**: Wrap SQL in transaction; extract files first, then run SQL; add rollback on failure. + +--- + +## Files Needing Immediate Attention + +| File | Lines | Issue | +|------|-------|-------| +| `autoload/Shared/Helpers/Helpers.php` | 1220 | God class (C3) | +| `autoload/admin/factory/class.Update.php` | 157 | Path traversal (C2), supply chain (H5) | +| `admin/ajax/pages.php` | ~90 | Unserialize (C1), missing validation (H1) | +| `admin/index.php` | — | Password hash in cookie (H4) | +| `autoload/Domain/Articles/ArticlesRepository.php` | 648 | Side effects in repo (M2), raw SQL (H2) | +| `test.php` | 700 | Remove from root (L2) | diff --git a/.paul/codebase/conventions.md b/.paul/codebase/conventions.md new file mode 100644 index 0000000..0a7a86e --- /dev/null +++ b/.paul/codebase/conventions.md @@ -0,0 +1,161 @@ +# Coding Conventions + +> Generated: 2026-04-26 + +## File Naming + +| Layer | Convention | Example | +|-------|-----------|---------| +| Legacy (admin/front) | `class.{ClassName}.php` | `class.Articles.php` | +| Domain repositories | `{ClassName}.php` (PSR-4) | `ArticlesRepository.php` | +| Shared services | `{ClassName}.php` (PSR-4) | `CacheHandler.php` | +| Templates | `{feature-name}.php` | `articles/list.php` | + +## Naming Conventions + +| Element | Legacy code | New Domain code | +|---------|------------|-----------------| +| Methods | `snake_case` | `camelCase` | +| Classes | `PascalCase` | `PascalCase` | +| Properties | `$camelCase` | `$camelCase` | +| Constants | `UPPER_CASE` | `UPPER_CASE` | +| Namespaces | lowercase (`admin\`, `front\`) | PascalCase (`Domain\`, `Shared\`) | + +## Class Patterns + +### Controls (request handlers) — static methods only +```php +namespace admin\controls; +class Articles { + public static function article_delete() { + global $user; + if (!admin\factory\Users::check_privileges('articles', $user['id'])) + return \S::alert('Brak uprawnień'); + admin\factory\Articles::article_delete(\S::get('article_id')); + } +} +``` + +### Factories — @deprecated wrappers, static methods, delegate to repo +```php +namespace admin\factory; +/** @deprecated Wrapper — używaj \Domain\Articles\ArticlesRepository przez DI */ +class Articles { + private static function repo(): \Domain\Articles\ArticlesRepository { + global $mdb; + return new \Domain\Articles\ArticlesRepository($mdb); + } + public static function article_delete($id): bool { + return self::repo()->deleteArticle((int)$id); + } +} +``` + +### Domain Repositories — constructor DI, camelCase, typed returns +```php +namespace Domain\Articles; +class ArticlesRepository { + private $db; + public function __construct($db) { $this->db = $db; } + + // ------------------------------------------------------------------------- + // Odczyt (Read) + // ------------------------------------------------------------------------- + public function find(int $id): ?array { + return $this->db->get('pp_articles', '*', ['id' => $id]) ?: null; + } + + // ------------------------------------------------------------------------- + // Zapis / usuwanie (Write / Delete) + // ------------------------------------------------------------------------- + public function deleteArticle(int $id): bool { + $this->db->delete('pp_articles', ['id' => $id]); + return true; + } +} +``` + +### View classes — static rendering +```php +namespace admin\view; +class Articles { + public static function list($articles) { + $tpl = new \Tpl; + $tpl->articles = $articles; + return $tpl->render('articles/list'); + } +} +``` + +## PHPDoc Style + +Polish-language descriptions are standard in this project: +```php +/** + * Prosta lista autorów + * @return array|bool + */ +public function authorsList() { ... } + +/** + * Zapis autora (insert lub update) + * @param int $authorId + * @param string $author + * @return object|bool + */ +public function authorSave(int $authorId, string $author) { ... } +``` + +Section separators in larger classes: +```php +// ------------------------------------------------------------------------- +// Odczyt (Read operations) +// ------------------------------------------------------------------------- +``` + +## Return Patterns + +| Pattern | Usage | +|---------|-------| +| `?array` | Single record lookup (null = not found) | +| `array` (possibly `[]`) | List queries — `?: []` fallback | +| `bool` | Write/delete operations | +| `int` | Codes: `1 = OK`, `0 = bad credentials`, `-1 = blocked` | +| `void` | Side-effect-only writes | +| `['status' => 'ok'/'error', 'msg' => '...']` | AJAX JSON responses | + +## Error Handling + +- Repositories return `null`/`false`/`[]` for "not found", don't throw +- `ImageManipulator` uses typed exceptions (`\InvalidArgumentException`, `\RuntimeException`) +- AJAX endpoints: `json_encode(['status' => 'ok/error', 'msg' => '...'])` +- Error suppression with `@` is used in legacy code (avoid in new code) + +## Database Access via Medoo + +Always use parameterized Medoo methods — never string concatenation with string values: +```php +// Good +$this->db->get('pp_articles', '*', ['id' => $id]); +$this->db->select('pp_articles', '*', ['ORDER' => ['created' => 'DESC']]); +$this->db->update('pp_articles', ['status' => 1], ['id' => $id]); +$this->db->insert('pp_articles', ['title' => $title, 'slug' => $slug]); + +// Acceptable (integer cast only) +$this->db->query("SELECT ... WHERE id = " . (int)$id)->fetchAll(); + +// Never +$this->db->query("SELECT ... WHERE slug = '" . $slug . "'"); // SQL injection risk +``` + +## Global Helper Facade (`\S::`) + +Legacy code uses `\S::method()` — new code should use `\Shared\Helpers\Helpers::method()` directly or inject the dependency. Migrate `\S::` calls opportunistically but don't block on it. + +## Template Rendering + +```php +$tpl = new \Tpl; // or: new \Shared\Tpl\Tpl +$tpl->variable = $value; // assign template variables +return $tpl->render('module/template-name'); // checks templates_user/ first, then templates/ +``` diff --git a/.paul/codebase/integrations.md b/.paul/codebase/integrations.md new file mode 100644 index 0000000..90ef43b --- /dev/null +++ b/.paul/codebase/integrations.md @@ -0,0 +1,63 @@ +# External Integrations + +> Generated: 2026-04-26 + +## Email — PHPMailer + SMTP + +- **Library**: PHPMailer (`libraries/phpmailer/class.phpmailer.php`) +- **Service class**: `autoload/Shared/Email/Email.php` +- **Configuration**: stored in `pp_settings` table + - Keys: `email_host`, `email_port`, `email_login`, `email_password`, `contact_email`, `firm_name` +- **Features**: SSL/TLS, self-signed cert support, HTML email, attachments, relative URL conversion +- **Used by**: Newsletter cron, contact forms, 2FA code sending + +## Geolocation — geoPlugin + +- **Provider**: geoPlugin (http://www.geoplugin.net/) +- **Class**: `autoload/class.geoplugin.php` +- **Features**: IP-to-country, currency detection, exchange rates +- **Integration**: loaded in frontend via autoloader, used for localization hints + +## Analytics + +- **Type**: configurable (any script tag) +- **Storage**: `pp_settings.statistic_code` field +- **Injection**: `index.php` lines ~121-122 — injected into HTML `` via string replacement +- **Default**: empty (disabled until configured in admin Settings) + +## Updates — cmspro.project-dc.pl + +- **Factory**: `autoload/admin/factory/class.Update.php` +- **Base URL**: `https://www.cmspro.project-dc.pl/updates/` (hardcoded) +- **Endpoints used**: + - `versions.php?key={update_key}` — fetch available versions list + - `{dir}/ver_{version}.zip` — download update ZIP + - `{dir}/ver_{version}_sql.txt` — legacy SQL migration fallback +- **Auth**: `update_key` from `pp_settings`, validated on server +- **License**: `pp_update_licenses` table — `valid_to_date`, `valid_to_version`, `beta` flag +- **Channels**: stable / beta + +**Security note**: `file_get_contents()` over HTTPS, no signature verification, path not sanitized. +See `concerns.md` for details. + +## File Manager + +- **Library**: FileManager 9.14.1 (`libraries/filemanager-9.14.1/`) +- **API endpoint**: `upload/filemanager/api/` +- **Features**: file upload, deletion, browsing via AJAX +- **MIME validation**: JPEG, PNG, GIF, WebP allowed +- **Organization**: files stored by article ID under `upload/` + +## Mobile Detection + +- **Library**: Mobile_Detect 2.8.16 (`autoload/class.Mobile_Detect.php`) +- **Usage**: UA-based device detection for mobile/tablet +- **Integration**: used in frontend factory to adapt output + +## No Payment Integration + +No PayPal, Stripe, or other payment processor code detected. + +## No CDN + +Images served locally. WebP conversion cached in `cache/` directory. diff --git a/.paul/codebase/overview.md b/.paul/codebase/overview.md new file mode 100644 index 0000000..43d3fdc --- /dev/null +++ b/.paul/codebase/overview.md @@ -0,0 +1,54 @@ +# cmsPRO — Project Overview + +> Generated: 2026-04-26 | Milestone: v0.1 Refaktoryzacja + +## What is cmsPRO? + +cmsPRO is a Polish-language PHP CMS with a **hybrid transitional architecture**. The codebase is actively being refactored from a legacy procedural/OOP mixed approach toward a clean Domain-Driven Design structure with Repository pattern. + +## Core Capabilities + +| Module | Description | +|--------|-------------| +| Articles | CRUD, multi-language, versioning, scheduling, galleries, tags, SEO | +| Pages | Static pages with layouts, caching, inline editing | +| Newsletter | Subscription, templates, cron-based batch sending | +| Layouts | HTML/CSS template system with Savant3 rendering | +| Users | Admin users, privileges matrix, 2FA support | +| Languages | Multi-language content, URL routing, session caching | +| Banners | Homepage banners with multi-language support | +| Scontainers | Reusable content blocks/widgets | +| Authors | Author management for articles | +| SEO | Meta tags, slugs, noindex, robots.txt, sitemap | +| File Manager | Upload, browse, thumbnail generation | +| Settings | DB-stored site config, WebP toggle, lazy loading | +| Updates | Versioned ZIP updates with license validation | +| Backups | DB backup/restore utilities | + +## Entry Points + +| File | Purpose | +|------|---------| +| `index.php` | Frontend entry point and router | +| `admin/index.php` | Admin panel entry point | +| `ajax.php` | Frontend AJAX handler | +| `admin/ajax.php` | Admin AJAX handler (routes to `admin/ajax/*.php`) | +| `api.php` | API endpoint | +| `cron.php` | Scheduled tasks (newsletter batch sending) | +| `download.php` | File download handler | + +## Current Refactoring Status + +The project is in **Phase 5 of Milestone v0.1 Refaktoryzacja**. + +Migration pattern: +- **Done**: Domain repositories created for all 10 main entities +- **Done**: Factory classes converted to deprecated wrappers delegating to repositories +- **In progress**: SeoAdditional, Cron, Releases domains +- **Pending**: Remove factory layer, inject repositories directly into controls + +## Version + +- Current app version: **1.695** +- Update channel: stable/beta via `updates/` ZIP packages +- License validation via `pp_update_licenses` table diff --git a/.paul/codebase/stack.md b/.paul/codebase/stack.md new file mode 100644 index 0000000..f74ee57 --- /dev/null +++ b/.paul/codebase/stack.md @@ -0,0 +1,80 @@ +# Technology Stack + +> Generated: 2026-04-26 + +## PHP Runtime + +- **Required**: PHP 7.4+ (nikic/php-parser constraint), PHP 7.1+ / 8.0+ (deep-copy) +- **Composer**: `composer.json` at project root +- **Dev dependency**: `phpunit/phpunit: ^10.5` +- **No runtime Composer packages** — all libraries are vendored manually in `libraries/` + +## Database + +| Item | Value | +|------|-------| +| Engine | MySQL | +| Config | `config.php` (plain-text credentials) | +| Abstraction | Medoo 1.7.3 (`libraries/medoo/medoo.php`) | +| Table prefix | `pp_` | +| Remote host | `host117523.hostido.net.pl` (hostido.net.pl hosting) | + +Key tables: `pp_articles`, `pp_articles_langs`, `pp_pages`, `pp_layouts`, `pp_users`, `pp_users_privileges`, `pp_newsletter`, `pp_newsletter_templates`, `pp_banners`, `pp_scontainers`, `pp_authors`, `pp_languages`, `pp_settings`, `pp_tags`, `pp_update_versions`, `pp_update_licenses` + +## Frontend Libraries (all vendored in `libraries/`) + +| Library | Version | Purpose | +|---------|---------|---------| +| jQuery | 2.1.3 | JavaScript DOM | +| Bootstrap | 4.1.3 | CSS/JS framework | +| Font Awesome | 4.7.0 | Icons | +| jQuery UI | — | UI widgets | +| CKEditor | — | WYSIWYG editor | +| Leaflet | — | Maps (in CKEditor plugin) | +| Plupload | 3.1.2 | File upload | +| jQuery Confirm | — | Confirmation dialogs | +| FancyBox | — | Lightbox/modal | +| CodeMirror | — | Code editor | +| Lozad.js | — | Lazy loading | +| MotionCAPTCHA | — | CAPTCHA | +| FileManager | 9.14.1 | File browse/upload UI | + +**No build tools** — no webpack, vite, or gulp. Raw JS/CSS files. + +Custom JS: `libraries/functions.js`, `libraries/functions-front.js`, `libraries/jquery/javascript.js` + +## PHP Libraries (vendored) + +| Library | Location | Purpose | +|---------|----------|---------| +| PHPMailer | `libraries/phpmailer/` | SMTP email (class.phpmailer.php, class.smtp.php) | +| Medoo | `libraries/medoo/medoo.php` | Database abstraction | +| MySQLDump | `libraries/MySQLDump.php` | SQL dump utility | +| Savant3 | `autoload/Savant3.php` | Template engine | +| Mobile_Detect | `autoload/class.Mobile_Detect.php` | 2.8.16, device detection | +| geoPlugin | `autoload/class.geoplugin.php` | IP geolocation | + +## Server + +- **Apache** with mod_rewrite, mod_deflate, mod_expires +- Config: `.htaccess` — HTTPS redirect, www enforcement, trailing slash, gzip, 1-year browser cache +- Optional admin IP whitelist: `admin/ip.conf` +- Session: PHP native sessions with IP validation and regeneration +- Cache: File-based in `cache/` and `temp/` directories + +## External Services + +| Service | Purpose | Integration | +|---------|---------|-------------| +| SMTP (configurable) | Email delivery | PHPMailer, settings in `pp_settings` | +| geoPlugin (geoplugin.net) | IP geolocation | `class.geoplugin.php` | +| cmspro.project-dc.pl | Update downloads | `autoload/admin/factory/class.Update.php` line 12, 25 | +| Analytics (configurable) | Stats injection | `pp_settings.statistic_code` → injected in `` | + +## Autoloading + +Hybrid custom autoloader at `autoload/autoloader.php`: +1. Tries `autoload/{namespace}/class.{ClassName}.php` (legacy) +2. Falls back to `autoload/{namespace}/{ClassName}.php` (PSR-4) + +Composer PSR-4 mappings: `Domain\` → `autoload/Domain/`, `Shared\` → `autoload/Shared/` diff --git a/.paul/codebase/testing.md b/.paul/codebase/testing.md new file mode 100644 index 0000000..27e8af9 --- /dev/null +++ b/.paul/codebase/testing.md @@ -0,0 +1,124 @@ +# Testing + +> Generated: 2026-04-26 + +## Framework + +- **PHPUnit 10.5+** (`phpunit/phpunit` in `composer.json` dev) +- Config: `phpunit.xml` at project root +- Bootstrap: `tests/bootstrap.php` + +## Structure + +``` +tests/ +├── bootstrap.php Test bootstrap (PSR-4 autoload for Domain\) +├── stubs/ +│ ├── CacheHandler.php In-memory stub (replaces file-based cache) +│ └── S.php Helper facade stub +└── Unit/ + └── Domain/ + ├── Languages/LanguagesRepositoryTest.php + ├── Settings/SettingsRepositoryTest.php + └── User/UserRepositoryTest.php +``` + +## Bootstrap Setup + +`tests/bootstrap.php`: +- Loads Medoo ORM (`libraries/medoo/medoo.php`) +- Loads stubs **before** autoloader (to override `Shared\Cache\CacheHandler`) +- Registers PSR-4 autoloader for `Domain\` namespace only + +**Critical**: Stubs must be loaded before autoloader. CacheHandler stub provides `reset()` method for test isolation. + +## Test Pattern + +All tests follow **AAA (Arrange-Act-Assert)** with Medoo mocked: + +```php +namespace Tests\Unit\Domain\Languages; + +use Domain\Languages\LanguagesRepository; +use PHPUnit\Framework\TestCase; + +class LanguagesRepositoryTest extends TestCase { + private function mockDb(): object { + return $this->createMock(\medoo::class); + } + + protected function setUp(): void { + \Shared\Cache\CacheHandler::reset(); // clear in-memory cache + } + + public function testLanguagesListReturnsArray(): void { + $db = $this->mockDb(); + $db->method('select')->willReturn([['id' => 'pl', 'name' => 'Polski']]); + + $repo = new LanguagesRepository($db); + $result = $repo->languagesList(); + + $this->assertSame([['id' => 'pl', 'name' => 'Polski']], $result); + } + + public function testLanguagesListReturnsEmptyWhenNull(): void { + $db = $this->mockDb(); + $db->method('select')->willReturn(null); + $this->assertSame([], (new LanguagesRepository($db))->languagesList()); + } + + public function testActiveLanguagesQueriesDbAndCaches(): void { + $expected = [['id' => 'pl', 'name' => 'Polski', 'domain' => null]]; + $db = $this->mockDb(); + $db->expects($this->once())->method('select')->willReturn($expected); + + $repo = new LanguagesRepository($db); + $this->assertSame($expected, $repo->activeLanguages()); + $this->assertSame($expected, $repo->activeLanguages()); // 2nd call hits cache + } +} +``` + +## Stubs + +### `tests/stubs/CacheHandler.php` +In-memory replacement for `Shared\Cache\CacheHandler`: +- `static::$store` — array key-value store +- `reset()` — clear all stored values (call in `setUp()`) +- `fetch($key)` — return stored value or `false` +- `store($key, $value, $ttl)` — store value (TTL ignored) +- `delete($key)` — remove value + +### `tests/stubs/S.php` +Stub for the `\S` global helper facade — prevents tests from hitting real filesystem/session code. + +## Coverage + +Currently tested: **Domain layer only** +- `Domain\Languages\LanguagesRepository` ✓ +- `Domain\Settings\SettingsRepository` ✓ +- `Domain\User\UserRepository` ✓ +- All other Domain repositories: **no tests yet** + +Not tested: +- `admin\controls\*` — static controllers +- `admin\factory\*` — deprecated wrappers +- `front\*` — frontend layer +- `Shared\*` — utilities +- AJAX handlers + +## Running Tests + +```bash +composer test +# or +./vendor/bin/phpunit +``` + +## Adding Tests for New Repositories + +When adding a new `Domain\{Entity}\{Entity}Repository`: +1. Create `tests/Unit/Domain/{Entity}/{Entity}RepositoryTest.php` +2. Call `\Shared\Cache\CacheHandler::reset()` in `setUp()` if the repo uses caching +3. Mock `\medoo` via `$this->createMock(\medoo::class)` +4. Test: null-to-empty-array coercion, cache hit (expects `once()`), write returns expected type