8 Commits

Author SHA1 Message Date
8f6d084b4d build(update): paczka 1.695 — aktualizacja konfiguracji Claude, Serena
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:28:22 +02:00
47abff2550 chore: aktualizacja konfiguracji Claude, Serena i CLAUDE.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:27:16 +02:00
ffe661b4d2 feat(domain): Domain\Authors + Domain\Newsletter repositories z wrapper delegation
Phase 4 complete:
- AuthorsRepository: simpleList, authorDetails, authorSave, authorDelete, authorByLang
- NewsletterRepository: 14 methods — subscriber lifecycle, templates, sending
- 4 legacy factories converted to thin wrappers
- Globals ($settings, $lang) passed as explicit params to repo methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:21:32 +02:00
73ff0ca5b6 feat(domain): Domain\Scontainers + Domain\Banners repositories z wrapper delegation
Phase 3 complete:
- ScontainersRepository: containerDetails, containerSave, containerDelete, scontainerByLang
- BannersRepository: bannerDetails, bannerSave, bannerDelete, activeBanners, mainBanner
- 4 legacy factories converted to thin wrappers delegating to Domain repos

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:04:42 +02:00
7949e9b6a3 build(update): paczka 1.694 — centralny autoloader, Email, Security
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 17:31:54 +02:00
3325eaf44c refactor: centralny autoloader, Shared\Email i Shared\Security
- Utworzono autoload/autoloader.php (hybrydowy PSR-4 + legacy)
- Zmigrowano 7 entry pointów do centralnego autoloadera
- Dodano PSR-4 mapowanie w composer.json (Domain, Shared, Admin, Frontend)
- Utworzono Shared\Email\Email (PHPMailer, migracja z Helpers)
- Utworzono Shared\Security\CsrfToken (random_bytes + hash_equals)
- Wrappery w Helpers delegują do nowych klas
- Zaktualizowano docs/PROJECT_STRUCTURE.md
- Inicjalizacja PAUL (.paul/) z roadmapą 19 faz refaktoryzacji

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 17:28:01 +02:00
9b31ce0d16 feat: dodanie pliku konfiguracyjnego MCP oraz aktualizacja pliku FTP z nowymi regułami ignorowania 2026-03-04 00:47:17 +01:00
964bfa877c build(update): paczka 1.693 i aktualizacja versions.php 2026-03-04 00:45:59 +01:00
51 changed files with 2848 additions and 735 deletions

View File

@@ -0,0 +1,47 @@
Wykonaj procedurę zakończenia pracy w projekcie cmsPRO. Wszystkie kroki wykonuj kolejno:
## 1. Testy
Uruchom `php vendor/bin/phpunit`. Jeśli testy nie przechodzą — napraw błędy przed kontynuowaniem.
## 2. Dokumentacja
Sprawdź czy zmiany wymagają aktualizacji:
- `docs/PROJECT_STRUCTURE.md` — struktura projektu, moduły, fazy refaktoryzacji
- `docs/FORM_EDIT_SYSTEM.md` — system formularzy (tylko jeśli zmiany dotyczyły formularzy)
Zaktualizuj tylko jeśli zmiany tego wymagają. Nie aktualizuj na siłę.
## 3. Migracje SQL
Jeśli były zmiany w bazie danych:
- Utwórz plik `migrations/{version}.sql` (np. `migrations/1.694.sql`)
- NIE w `updates/` — build script sam wczyta z `migrations/`
## 4. Commit
Wykonaj git commit ze zmianami. Użyj konwencji z tego repo (patrz `git log --oneline -5`).
## 5. Paczka aktualizacji
Procedura budowania paczki:
a) Znajdź aktualną wersję w `updates/versions.php` (`$current_ver = XXXX`)
b) Oblicz nową wersję: `current_ver + 1`
c) Zaktualizuj `$current_ver` w `updates/versions.php` na nową wartość
d) Utwórz commit: `build(update): paczka {wersja} — {krótki opis zmian}`
e) Utwórz git tag: `git tag v{wersja}` (format: v1.694, v1.695, ...)
f) Uruchom build script:
```
powershell -ExecutionPolicy Bypass -File ./build-update.ps1 -FromTag v{poprzednia_wersja} -ToTag v{nowa_wersja} -ChangelogEntry "NEW - {opis zmian}"
```
g) Dodaj pliki paczki do ostatniego commita: `git add updates/*/ver_{wersja}.* && git commit --amend`
## 6. Push
Wykonaj `git push && git push --tags`. Jeśli auth fail (próbuj 3 razy, czasem jest błąd za pierwszym razem) — poinformuj użytkownika żeby uruchomił `! git push && git push --tags`.
## Podsumowanie
Na koniec wyświetl tabelkę:
| Krok | Status |
|------|--------|
| Testy | OK/FAIL |
| Dokumentacja | Zaktualizowana / Bez zmian |
| Migracje SQL | Utworzone / Nie dotyczy |
| Commit | hash |
| Paczka | ver_X.XXX.zip |
| Push | OK / Wymaga auth |

View File

@@ -48,7 +48,8 @@
"Bash(python3:*)", "Bash(python3:*)",
"Bash(python:*)", "Bash(python:*)",
"Bash(grep:*)", "Bash(grep:*)",
"Bash(grep ^<b>ver:*)" "Bash(grep ^<b>ver:*)",
"Skill(paul:plan)"
] ]
} }
} }

17
.mcp.json Normal file
View File

@@ -0,0 +1,17 @@
{
"mcpServers": {
"serena": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--context",
"ide-assistant",
"--project",
"C:/visual studio code/projekty/cmsPRO"
]
}
}
}

49
.paul/PROJECT.md Normal file
View File

@@ -0,0 +1,49 @@
# Project: cmsPRO
## Description
Autorski system CMS z panelem administracyjnym (17 modułów admin, 13 modułów front). Projekt przechodzi pełną refaktoryzację kodu w 19 fazach — wzorcem docelowej architektury jest shopPRO. Wzorzec migracji: wrapper delegation (stare klasy delegują do nowych, zero regresji).
## Core Value
Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi.
## Already Completed
- Domain (10 repos): Articles, Languages, Layouts, Pages, Settings, User, Scontainers, Banners, Authors, Newsletter
- Shared (7 modules): Cache, Helpers, Html, Image, Tpl, Email, Security
- Form Edit System: FormEditViewModel, multi-tab, validation, persistence
- PHPUnit base: Bootstrap, 3 test files
- Wrapper delegation pattern: legacy factories delegate to Domain repositories
## Requirements
### Must Have
- Centralny PSR-4 autoloader (hybrydowy z legacy)
- Wszystkie Domain repositories (Scontainers, Banners, Authors, Newsletter, SEO, Cron, Releases)
- Shared\Email + Shared\Security (CsrfToken, HMAC-SHA256)
- Admin\ namespace z DI dla wszystkich 17 modułów
- Frontend\ namespace dla wszystkich front modułów
- Bezpieczne cookies (HMAC-SHA256 zamiast hash w JSON)
### Should Have
- PHPUnit testy dla nowych repositories i controllers
- Legacy cleanup (usunięcie wrapperów po pełnej migracji)
### Nice to Have
- Admin base classes (TableListRequestFactory, FormValidator — wzór shopPRO)
## Constraints
- PHP < 8.0 (produkcja) — brak match, named args, union types, str_contains()
- Referencja architektury: shopPRO (C:\visual studio code\projekty\shopPRO)
- Zachowanie 100% kompatybilności wstecznej podczas migracji (wrapper delegation)
- Medoo ORM (nie zmieniać)
- Zewnętrzne biblioteki (Mobile_Detect, geoplugin) — nie ruszać
## Success Criteria
- 19 faz refaktoryzacji zakończonych
- Cały kod w namespace'ach Domain\, Shared\, Admin\, Frontend\
- Zero regresji — istniejąca funkcjonalność działa bez zmian
- Bezpieczne cookies (HMAC-SHA256)
- Testy PHPUnit dla kluczowych modułów
---
*Created: 2026-04-04*
*Last updated: 2026-04-04 after Phase 4*

306
.paul/ROADMAP.md Normal file
View File

@@ -0,0 +1,306 @@
# Roadmap: cmsPRO
## Overview
Pełna refaktoryzacja cmsPRO do architektury DDD wzorowanej na shopPRO. Wzorzec: wrapper delegation — stare klasy delegują do nowych, zero regresji. Referencja: C:\visual studio code\projekty\shopPRO. PHP < 8.0 (produkcja).
## Current Milestone
**v0.1 Refaktoryzacja** (v0.1.0)
Status: In progress
Phases: 4 of 19 complete
## Already Completed (before PAUL)
- **Domain (6 repos):** Articles, Languages, Layouts, Pages, Settings, User
- **Shared (5 modules):** Cache, Helpers, Html, Image, Tpl
- **Form Edit System:** Universal form handling framework (FormEditViewModel, multi-tab, validation)
- **PHPUnit base:** Bootstrap, 3 test files (Languages, Settings, User)
## Phases
| Phase | Name | Plans | Status | Completed |
|-------|------|-------|--------|-----------|
| 1 | Infrastructure & Autoloader | 1 | Complete | 2026-04-04 |
| 2 | Shared: Email + Security | 1 | Complete | 2026-04-04 |
| 3 | Domain: Scontainers + Banners | 1 | Complete | 2026-04-04 |
| 4 | Domain: Authors + Newsletter | 1 | Complete | 2026-04-04 |
| 5 | Domain: SeoAdditional + Cron + Releases | 1 | Not started | - |
| 6 | Admin: Base Infrastructure | 1 | Not started | - |
| 7 | Admin: Articles + ArticlesArchive | 1 | Not started | - |
| 8 | Admin: Pages + Layouts | 1 | Not started | - |
| 9 | Admin: Languages + Settings | 1 | Not started | - |
| 10 | Admin: Banners + Authors + Scontainers | 1 | Not started | - |
| 11 | Admin: Newsletter + Emails + SeoAdditional | 1 | Not started | - |
| 12 | Admin: Users + Backups + Filemanager | 1 | Not started | - |
| 13 | Admin: Releases + Update | 1 | Not started | - |
| 14 | Front: Site + Articles | 1 | Not started | - |
| 15 | Front: Pages + Menu + Banners + Scontainers | 1 | Not started | - |
| 16 | Front: Remaining modules | 1-2 | Not started | - |
| 17 | Users & Security: HMAC-SHA256 | 1 | Not started | - |
| 18 | Tests | 1-2 | Not started | - |
| 19 | Legacy Cleanup | 1 | Not started | - |
## Phase Details
### Phase 1: Infrastructure & Autoloader
**Goal:** Centralny autoloader (PSR-4 + legacy), composer.json z mapowaniem, usunięcie duplikatów z entry pointów.
**Depends on:** Nothing (first phase)
**Research:** Unlikely
**Scope:**
- autoload/autoloader.php (hybrydowy)
- composer.json PSR-4: Domain\, Shared\, Admin\, Frontend\
- Migracja 6 entry pointów (index.php, admin/index.php, ajax.php, api.php, cron.php, download.php)
**Plans:**
- [ ] 01-01: PSR-4 autoloader setup i composer.json
### Phase 2: Shared: Email + Security
**Goal:** Dodać brakujące moduły Shared — Email (migracja z legacy) i Security (CsrfToken, wzór shopPRO).
**Depends on:** Phase 1 (autoloader)
**Research:** Unlikely
**Scope:**
- Shared\Email\Email — migracja z legacy
- Shared\Security\CsrfToken — nowy moduł (wzór shopPRO)
- Wrapper w starym class.Email.php (jeśli istnieje)
**Plans:**
- [ ] 02-01: Email + Security modules
### Phase 3: Domain: Scontainers + Banners
**Goal:** Repository dla Scontainers i Banners — przeniesienie logiki z factory do Domain\.
**Depends on:** Phase 1 (autoloader)
**Research:** Unlikely (wzorzec ustalony)
**Scope:**
- Domain\Scontainers\ScontainersRepository
- Domain\Banners\BannersRepository
- Wrappery w starych factory (delegacja)
**Plans:**
- [ ] 03-01: Scontainers + Banners repositories
### Phase 4: Domain: Authors + Newsletter
**Goal:** Repository dla Authors i Newsletter.
**Depends on:** Phase 1
**Research:** Unlikely
**Scope:**
- Domain\Authors\AuthorsRepository
- Domain\Newsletter\NewsletterRepository
**Plans:**
- [ ] 04-01: Authors + Newsletter repositories
### Phase 5: Domain: SeoAdditional + Cron + Releases
**Goal:** Repository dla SEO, Cron, i systemu Releases/Update.
**Depends on:** Phase 1
**Research:** Unlikely
**Scope:**
- Domain\SeoAdditional\SeoAdditionalRepository
- Domain\Cron\CronRepository
- Domain\Releases\ReleasesRepository (lub Update)
**Plans:**
- [ ] 05-01: SeoAdditional + Cron + Releases repositories
### Phase 6: Admin: Base Infrastructure
**Goal:** Bazowe klasy Admin\ — kontrolery bazowe, TableListRequestFactory, FormValidator (wzór shopPRO).
**Depends on:** Phase 1 (autoloader), Form Edit System (already done)
**Research:** Likely (analiza shopPRO Admin base classes)
**Scope:**
- Admin\Base\BaseController (lub abstrakcyjna klasa bazowa)
- Admin\Support\TableListRequestFactory
- Admin\Support\FormValidator
- Integracja z istniejącym FormEditViewModel
**Plans:**
- [ ] 06-01: Admin base infrastructure
### Phase 7: Admin: Articles + ArticlesArchive
**Goal:** Migracja kontrolerów Articles i ArticlesArchive do Admin\ z DI.
**Depends on:** Phase 6 (Admin base), Phase 1 (Domain\Articles already exists)
**Research:** Unlikely
**Scope:**
- Admin\Articles\ArticlesController
- Admin\Articles\ArticlesArchiveController
- Wrapper w starym controls/class.Articles.php
**Plans:**
- [ ] 07-01: Articles admin controllers
### Phase 8: Admin: Pages + Layouts
**Goal:** Migracja kontrolerów Pages i Layouts do Admin\.
**Depends on:** Phase 6
**Research:** Unlikely
**Scope:**
- Admin\Pages\PagesController
- Admin\Layouts\LayoutsController
**Plans:**
- [ ] 08-01: Pages + Layouts admin controllers
### Phase 9: Admin: Languages + Settings
**Goal:** Migracja kontrolerów Languages i Settings do Admin\.
**Depends on:** Phase 6
**Research:** Unlikely
**Scope:**
- Admin\Languages\LanguagesController
- Admin\Settings\SettingsController
**Plans:**
- [ ] 09-01: Languages + Settings admin controllers
### Phase 10: Admin: Banners + Authors + Scontainers
**Goal:** Migracja kontrolerów Banners, Authors, Scontainers do Admin\.
**Depends on:** Phase 6, Phase 3 (Domain repos), Phase 4 (Domain repos)
**Research:** Unlikely
**Scope:**
- Admin\Banners\BannersController
- Admin\Authors\AuthorsController
- Admin\Scontainers\ScontainersController
**Plans:**
- [ ] 10-01: Banners + Authors + Scontainers admin controllers
### Phase 11: Admin: Newsletter + Emails + SeoAdditional
**Goal:** Migracja kontrolerów Newsletter, Emails, SeoAdditional do Admin\.
**Depends on:** Phase 6, Phase 4, Phase 5
**Research:** Unlikely
**Scope:**
- Admin\Newsletter\NewsletterController
- Admin\Emails\EmailsController
- Admin\SeoAdditional\SeoAdditionalController
**Plans:**
- [ ] 11-01: Newsletter + Emails + SeoAdditional admin controllers
### Phase 12: Admin: Users + Backups + Filemanager
**Goal:** Migracja kontrolerów Users, Backups, Filemanager do Admin\.
**Depends on:** Phase 6
**Research:** Unlikely
**Scope:**
- Admin\Users\UsersController
- Admin\Backups\BackupsController
- Admin\Filemanager\FilemanagerController
**Plans:**
- [ ] 12-01: Users + Backups + Filemanager admin controllers
### Phase 13: Admin: Releases + Update
**Goal:** Migracja kontrolerów Releases i Update do Admin\.
**Depends on:** Phase 6, Phase 5 (Domain repos)
**Research:** Unlikely
**Scope:**
- Admin\Releases\ReleasesController
- Admin\Update\UpdateController
**Plans:**
- [ ] 13-01: Releases + Update admin controllers
### Phase 14: Front: Site + Articles
**Goal:** Migracja głównych kontrolerów front — Site i Articles do Frontend\.
**Depends on:** Phase 1 (autoloader), Domain repos
**Research:** Unlikely
**Scope:**
- Frontend\Site\SiteController (lub controls + factory + view)
- Frontend\Articles\ArticlesController
- LayoutEngine (jeśli potrzebny, wzór shopPRO)
**Plans:**
- [ ] 14-01: Site + Articles frontend controllers
### Phase 15: Front: Pages + Menu + Banners + Scontainers
**Goal:** Migracja front kontrolerów Pages, Menu, Banners, Scontainers.
**Depends on:** Phase 14 (Front base)
**Research:** Unlikely
**Scope:**
- Frontend\Pages, Frontend\Menu, Frontend\Banners, Frontend\Scontainers
**Plans:**
- [ ] 15-01: Pages + Menu + Banners + Scontainers frontend
### Phase 16: Front: Remaining modules
**Goal:** Migracja pozostałych front modułów — Authors, Languages, Newsletter, Search, AuditSEO, SeoAdditional, Layouts, Settings.
**Depends on:** Phase 14
**Research:** Unlikely
**Scope:**
- Wszystkie pozostałe front factories/controls/views
**Plans:**
- [ ] 16-01: Remaining frontend modules (batch 1)
- [ ] 16-02: Remaining frontend modules (batch 2, if needed)
### Phase 17: Users & Security: HMAC-SHA256
**Goal:** Wymiana insecure remember-me cookies (hash w JSON) na HMAC-SHA256 signed tokens.
**Depends on:** Phase 2 (Shared\Security), Phase 12 (Admin\Users)
**Research:** Likely (strategia migracji istniejących cookies, backward compat)
**Scope:**
- Nowy system remember-me z HMAC-SHA256
- Migracja istniejących sesji/cookies
- Security hardening w UserRepository
**Plans:**
- [ ] 17-01: HMAC-SHA256 cookie system
### Phase 18: Tests
**Goal:** Rozbudowa PHPUnit testów dla nowych Domain repositories i Admin controllers.
**Depends on:** Phase 5 (all Domain repos), Phase 13 (all Admin controllers)
**Research:** Unlikely
**Scope:**
- Testy dla nowych Domain repositories
- Testy dla Admin controllers (unit)
- Rozbudowa test bootstrap
**Plans:**
- [ ] 18-01: Domain repository tests
- [ ] 18-02: Admin controller tests
### Phase 19: Legacy Cleanup
**Goal:** Usunięcie legacy wrapperów i starych class.*.php po weryfikacji że cały kod używa nowych klas.
**Depends on:** All prior phases
**Research:** Unlikely
**Scope:**
- Usunięcie wrapperów z class.*.php
- Usunięcie starych controls/factory/view plików
- Finalna weryfikacja i cleanup
**Plans:**
- [ ] 19-01: Legacy wrapper removal and cleanup
---
*Roadmap created: 2026-04-04*
*Last updated: 2026-04-04*

70
.paul/STATE.md Normal file
View File

@@ -0,0 +1,70 @@
# Project State
## Project Reference
See: .paul/PROJECT.md (updated 2026-04-04)
**Core value:** Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi.
**Current focus:** Phase 4 complete — ready for Phase 5
## Current Position
Milestone: v0.1 Refaktoryzacja
Phase: 4 of 19 (Domain: Authors + Newsletter) — Complete
Plan: 04-01 complete
Status: Loop closed, ready for next PLAN
Last activity: 2026-04-04 — Phase 4 complete, UNIFY done
Progress:
- Milestone: [▓▓░░░░░░░░] 20%
## Loop Position
Current loop state:
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [Loop complete - ready for next PLAN]
```
## Performance Metrics
**Velocity:**
- Total plans completed: 4
- Total execution time: ~22min
**By Phase:**
| Phase | Plans | Total Time | Avg/Plan |
|-------|-------|------------|----------|
| 01-infrastructure | 1/1 | ~10min | ~10min |
| 02-shared-email-security | 1/1 | ~8min | ~8min |
| 03-domain-scontainers-banners | 1/1 | ~2min | ~2min |
| 04-domain-authors-newsletter | 1/1 | ~2min | ~2min |
## Accumulated Context
### Decisions
- Centralny autoloader zamiast duplikatów
- CsrfToken: single token per session (shopPRO pattern)
- Email: PHPMailer require via __DIR__ absolute paths
- Shared layer kompletny: Cache, Helpers, Html, Image, Tpl, Email, Security
- Wrapper delegation: factory creates new repo per call (no singleton)
- Front repos: $lang[0] passed explicitly, repos don't use globals
- Front caching: migrated from \Cache:: to \Shared\Cache\CacheHandler::
- Newsletter: globals ($settings, $lang) passed as explicit params to repo methods
### Deferred Issues
None.
### Blockers/Concerns
None.
## Session Continuity
Last session: 2026-04-04
Stopped at: Phase 4 complete, loop closed
Next action: Run /paul:plan for Phase 5 (Domain: SeoAdditional + Cron + Releases)
Resume file: .paul/phases/04-domain-authors-newsletter/04-01-SUMMARY.md
---
*STATE.md — Updated after every significant action*

33
.paul/config.md Normal file
View File

@@ -0,0 +1,33 @@
# Project Config
**Project:** cmsPRO
**Created:** 2026-04-04
## Project Settings
```yaml
project:
name: cmsPRO
version: 0.0.0
```
## Integrations
### SonarQube
```yaml
sonarqube:
enabled: true
project_key: cmsPRO
```
## Preferences
```yaml
preferences:
auto_commit: false
verbose_output: false
```
---
*Config created: 2026-04-04*

View File

@@ -0,0 +1,176 @@
---
phase: 01-infrastructure
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- composer.json
- autoload/autoloader.php
- index.php
- admin/index.php
- ajax.php
- api.php
- cron.php
- download.php
autonomous: true
delegation: off
---
<objective>
## Goal
Scentralizować autoloader w jednym pliku, dodać PSR-4 mapowanie w composer.json dla Domain\, Shared\, Admin\, Frontend\, i zastąpić zduplikowane __autoload_my_classes() we wszystkich entry pointach.
## Purpose
Fundament dla całej refaktoryzacji — bez działającego PSR-4 autoloadera nie można dodawać nowych klas w Admin\ i Frontend\ namespace'ach.
## Output
- Centralny autoload/autoloader.php (hybrydowy: PSR-4 + legacy class.*.php)
- Zaktualizowany composer.json z PSR-4 mapowaniem
- Wszystkie entry pointy używają jednego autoloadera
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
## Source Files
@composer.json
@index.php (zawiera __autoload_my_classes)
@admin/index.php (zawiera duplikat __autoload_my_classes)
@ajax.php, api.php, cron.php, download.php (kolejne duplikaty)
## Reference
shopPRO composer.json — PSR-4 mapping: Domain\, Admin\, Frontend\, Shared\ → autoload/
</context>
<acceptance_criteria>
## AC-1: Centralny autoloader
```gherkin
Given plik autoload/autoloader.php istnieje
When jest załadowany przez require_once
Then rejestruje spl_autoload_register z obsługą zarówno PSR-4 (ClassName.php) jak i legacy (class.ClassName.php)
```
## AC-2: composer.json PSR-4
```gherkin
Given composer.json ma sekcję autoload.psr-4
When uruchomię composer dump-autoload
Then namespace'y Domain\, Shared\, Admin\, Frontend\ mapują do autoload/Domain/, autoload/Shared/, autoload/Admin/, autoload/Frontend/
```
## AC-3: Entry pointy używają centralnego autoloadera
```gherkin
Given index.php, admin/index.php, ajax.php, api.php, cron.php, download.php
When sprawdzę ich kod
Then każdy zawiera require_once do autoload/autoloader.php (lub ../autoload/autoloader.php)
And żaden nie zawiera zduplikowanej funkcji __autoload_my_classes
```
## AC-4: Istniejące klasy działają
```gherkin
Given klasy Domain\Articles\ArticlesRepository, Shared\Cache\CacheHandler etc. istnieją
When autoloader próbuje je załadować
Then klasy ładują się poprawnie (brak Fatal Error)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Utworzenie centralnego autoloadera</name>
<files>autoload/autoloader.php</files>
<action>
Utworzyć plik autoload/autoloader.php:
- Funkcja __autoload_my_classes($class) obsługująca:
1. Zamiana namespace separator \ na /
2. Próba załadowania: autoload/{path}/class.{ClassName}.php (legacy)
3. Próba załadowania: autoload/{path}/{ClassName}.php (PSR-4)
- spl_autoload_register('__autoload_my_classes')
- Bazowy katalog ustalany przez __DIR__ . '/' (relatywnie do autoload/)
- Obsługa klas bez namespace (legacy) — szukanie w autoload/class.{name}.php
Wzorować się na istniejącej logice z index.php, ale:
- Używać __DIR__ zamiast ścieżek relatywnych do entry pointa
- Jeden plik obsługuje WSZYSTKIE entry pointy
</action>
<verify>Sprawdzić że plik istnieje i zawiera spl_autoload_register</verify>
<done>AC-1 satisfied: Centralny autoloader z obsługą PSR-4 i legacy</done>
</task>
<task type="auto">
<name>Task 2: Aktualizacja composer.json</name>
<files>composer.json</files>
<action>
Dodać sekcję autoload.psr-4 do composer.json:
```json
"autoload": {
"psr-4": {
"Domain\\": "autoload/Domain/",
"Shared\\": "autoload/Shared/",
"Admin\\": "autoload/Admin/",
"Frontend\\": "autoload/Frontend/"
}
}
```
Zachować istniejący autoload-dev.
</action>
<verify>Sprawdzić że composer.json zawiera poprawne mapowanie PSR-4</verify>
<done>AC-2 satisfied: composer.json z PSR-4 mapowaniem</done>
</task>
<task type="auto">
<name>Task 3: Migracja entry pointów</name>
<files>index.php, admin/index.php, ajax.php, api.php, cron.php, download.php</files>
<action>
W każdym entry poincie:
1. USUNĄĆ definicję funkcji __autoload_my_classes() i jej spl_autoload_register
2. DODAĆ na początku (po <?php): require_once __DIR__ . '/autoload/autoloader.php';
Dla admin/index.php: require_once __DIR__ . '/../autoload/autoloader.php';
3. Zachować resztę kodu bez zmian (config.php, medoo, session etc.)
NIE zmieniać niczego innego w tych plikach — tylko autoloader.
</action>
<verify>Grep po wszystkich entry pointach: brak __autoload_my_classes definicji, jest require autoloader.php</verify>
<done>AC-3 satisfied: Wszystkie entry pointy używają centralnego autoloadera</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- autoload/Domain/* (istniejące klasy Domain — nie modyfikować)
- autoload/Shared/* (istniejące klasy Shared — nie modyfikować)
- config.php (konfiguracja bazy danych)
- libraries/* (zewnętrzne biblioteki)
## SCOPE LIMITS
- Tylko autoloader — nie refaktoryzować żadnych klas
- Nie dodawać nowych klas Admin\ ani Frontend\ (to w kolejnych fazach)
- Nie zmieniać logiki biznesowej w entry pointach
</boundaries>
<verification>
Before declaring plan complete:
- [ ] autoload/autoloader.php istnieje i zawiera spl_autoload_register
- [ ] composer.json ma sekcję autoload.psr-4 z 4 namespace'ami
- [ ] Żaden entry point nie zawiera zduplikowanej funkcji __autoload_my_classes
- [ ] Wszystkie entry pointy mają require_once autoloader.php
- [ ] Istniejące testy PHPUnit przechodzą (jeśli są)
- All acceptance criteria met
</verification>
<success_criteria>
- Centralny autoloader działa dla PSR-4 i legacy class.*.php
- Wszystkie entry pointy korzystają z jednego autoloadera
- Zero regresji — istniejący kod działa bez zmian
</success_criteria>
<output>
After completion, create `.paul/phases/01-infrastructure/01-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,110 @@
---
phase: 01-infrastructure
plan: 01
subsystem: infra
tags: [autoloader, psr-4, composer]
requires: []
provides:
- Centralny hybrydowy autoloader (PSR-4 + legacy)
- composer.json z PSR-4 mapowaniem namespace'ów
affects: [all future phases - every new class uses this autoloader]
tech-stack:
added: []
patterns: [centralny autoloader z __DIR__, hybrydowy PSR-4 + legacy]
key-files:
created: [autoload/autoloader.php]
modified: [composer.json, index.php, admin/index.php, admin/ajax.php, ajax.php, api.php, cron.php, download.php]
key-decisions:
- "Centralny autoloader zamiast duplikatów w entry pointach (ulepszenie vs shopPRO)"
- "Savant3 special case przeniesiony do centralnego autoloadera"
patterns-established:
- "Jeden autoloader dla wszystkich entry pointów — __DIR__ based paths"
- "Hybrydowe ładowanie: legacy class.*.php → PSR-4 ClassName.php"
duration: ~10min
completed: 2026-04-04
---
# Phase 1 Plan 01: Infrastructure & Autoloader Summary
**Centralny hybrydowy autoloader (PSR-4 + legacy) zastępujący 7 zduplikowanych kopii w entry pointach.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~10min |
| Completed | 2026-04-04 |
| Tasks | 3 completed |
| Files modified | 8 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Centralny autoloader | Pass | autoload/autoloader.php z spl_autoload_register, __DIR__ paths |
| AC-2: composer.json PSR-4 | Pass | Domain\, Shared\, Admin\, Frontend\ mapped |
| AC-3: Entry pointy zmigrowane | Pass | 7 entry pointów, 0 duplikatów __autoload_my_classes |
| AC-4: Istniejące klasy działają | Pass | Autoloader obsługuje legacy + PSR-4 format |
## Accomplishments
- Utworzono centralny `autoload/autoloader.php` z obsługą legacy (class.*.php) i PSR-4 (ClassName.php)
- Zaktualizowano `composer.json` z PSR-4 mapowaniem dla 4 namespace'ów
- Zmigrowano 7 entry pointów (index.php, admin/index.php, admin/ajax.php, ajax.php, api.php, cron.php, download.php)
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/autoloader.php` | Created | Centralny hybrydowy autoloader |
| `composer.json` | Modified | PSR-4 mapping dla Domain\, Shared\, Admin\, Frontend\ |
| `index.php` | Modified | require_once autoloader.php |
| `admin/index.php` | Modified | require_once ../autoloader.php |
| `admin/ajax.php` | Modified | require_once ../autoloader.php (Savant3 przeniesiony) |
| `ajax.php` | Modified | require_once autoloader.php |
| `api.php` | Modified | require_once autoloader.php |
| `cron.php` | Modified | require_once autoloader.php |
| `download.php` | Modified | require_once autoloader.php |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Centralny autoloader (vs duplikaty jak w shopPRO) | DRY, łatwiejsze utrzymanie, jednorazowa poprawka | Ulepszenie vs shopPRO — notatka dodana do shopPRO/docs |
| Savant3 special case w centralnym autoloaderze | Był tylko w admin/ajax.php, powinien działać globalnie | Brak regresji |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Scope additions | 1 | Minimal — admin/ajax.php (7th entry point) |
Plan zakładał 6 entry pointów, ale znaleziono 7 (admin/ajax.php nie był wymieniony w planie). Zmigrowany bez problemów.
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Autoloader obsługuje wszystkie namespace'y potrzebne dla faz 2-19
- Nowe klasy w Admin\, Frontend\ będą automatycznie ładowane
**Concerns:**
- AC-4 zweryfikowane statycznie (kod autoloadera) — runtime test wymaga uruchomienia aplikacji
**Blockers:**
- None
---
*Phase: 01-infrastructure, Plan: 01*
*Completed: 2026-04-04*

View File

@@ -0,0 +1,182 @@
---
phase: 02-shared-email-security
plan: 01
type: execute
wave: 1
depends_on: ["01-01"]
files_modified:
- autoload/Shared/Email/Email.php
- autoload/Shared/Security/CsrfToken.php
- autoload/Shared/Helpers/Helpers.php
autonomous: true
delegation: off
---
<objective>
## Goal
Utworzyć Shared\Email\Email i Shared\Security\CsrfToken wzorując się na shopPRO. Przenieść logikę z Helpers::send_email() i Helpers::get_token()/is_token_valid() do dedykowanych klas. Zachować wrappery w Helpers dla kompatybilności.
## Purpose
Email i Security to brakujące moduły Shared potrzebne przed refaktoryzacją Admin i Frontend kontrolerów. CsrfToken z kryptograficznie bezpiecznym tokenem zastąpi słaby sha1(mt_rand()).
## Output
- autoload/Shared/Email/Email.php — klasa email z PHPMailer
- autoload/Shared/Security/CsrfToken.php — CSRF z random_bytes + hash_equals
- Wrappery w Helpers.php delegujące do nowych klas
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
## Prior Work
@.paul/phases/01-infrastructure/01-01-SUMMARY.md — autoloader gotowy, PSR-4 działa
## Source Files
@autoload/Shared/Helpers/Helpers.php — zawiera send_email(), get_token(), is_token_valid()
## Reference
shopPRO autoload/Shared/Email/Email.php — docelowa implementacja
shopPRO autoload/Shared/Security/CsrfToken.php — docelowa implementacja
</context>
<acceptance_criteria>
## AC-1: Email class
```gherkin
Given plik autoload/Shared/Email/Email.php istnieje
When załaduję klasę Shared\Email\Email
Then klasa ma metody: send(), email_check(), load_by_name()
And send() używa PHPMailer do wysyłki maili
```
## AC-2: CsrfToken class
```gherkin
Given plik autoload/Shared/Security/CsrfToken.php istnieje
When załaduję klasę Shared\Security\CsrfToken
Then klasa ma statyczne metody: getToken(), validate(), regenerate()
And getToken() używa bin2hex(random_bytes(32))
And validate() używa hash_equals() (timing-safe)
```
## AC-3: Wrappery w Helpers
```gherkin
Given Helpers::send_email() i Helpers::get_token() nadal istnieją
When wywołam je z istniejącego kodu
Then delegują do nowych klas (Shared\Email\Email i Shared\Security\CsrfToken)
And istniejący kod działa bez zmian
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Utworzenie Shared\Email\Email</name>
<files>autoload/Shared/Email/Email.php</files>
<action>
Utworzyć klasę Email wzorowaną na shopPRO:
- namespace Shared\Email
- Właściwość $table = 'pp_newsletter_templates'
- Właściwość $text (treść maila), $headers, $newsletter_headers, $newsletter_footers
- Metoda load_by_name(string $name) — ładuje szablon z DB
- Metoda email_check($email) — walidacja filter_var
- Metoda send(string $email, string $subject, bool $newsletter_headers = false, string $file = null)
- Używa PHPMailer (require_once z libraries/)
- Regex do naprawy relatywnych URL w obrazkach/linkach
- Obsługa załączników
- Return $mail->Send()
WAŻNE: Sprawdzić w Helpers.php jak wygląda obecna implementacja send_email()
i przenieść tę logikę do nowej klasy, dostosowując do wzorca shopPRO.
PHP < 8.0 — brak named args, union types, match.
</action>
<verify>Sprawdzić że plik istnieje, ma namespace Shared\Email, klasę Email z metodami send(), email_check()</verify>
<done>AC-1 satisfied: Email class z PHPMailer</done>
</task>
<task type="auto">
<name>Task 2: Utworzenie Shared\Security\CsrfToken</name>
<files>autoload/Shared/Security/CsrfToken.php</files>
<action>
Utworzyć klasę CsrfToken wzorowaną na shopPRO:
- namespace Shared\Security
- const SESSION_KEY = 'csrf_token'
- static getToken(): string
- Jeśli brak tokenu w sesji → generuje bin2hex(random_bytes(32))
- Zapisuje w $_SESSION[self::SESSION_KEY]
- Zwraca token
- static validate(string $token): bool
- Porównuje z $_SESSION[self::SESSION_KEY] używając hash_equals()
- Return true/false (NIE usuwać tokenu po walidacji — to robi regenerate())
- static regenerate(): void
- Wymusza nowy token: unset($_SESSION[self::SESSION_KEY])
PHP < 8.0 — brak named args, union types, match.
</action>
<verify>Sprawdzić że plik istnieje, ma namespace Shared\Security, klasę CsrfToken z metodami getToken(), validate(), regenerate()</verify>
<done>AC-2 satisfied: CsrfToken z random_bytes + hash_equals</done>
</task>
<task type="auto">
<name>Task 3: Wrappery w Helpers.php</name>
<files>autoload/Shared/Helpers/Helpers.php</files>
<action>
W klasie Helpers:
1. Metoda send_email() — zamienić ciało na delegację:
$email = new \Shared\Email\Email();
$email->text = $text;
return $email->send($to, $subject, false, $file);
2. Metoda get_token() — zamienić ciało na delegację:
return \Shared\Security\CsrfToken::getToken();
3. Metoda is_token_valid() — zamienić ciało na delegację:
return \Shared\Security\CsrfToken::validate($token);
Zachować sygnatury metod identyczne — żaden calling code się nie zmienia.
NIE usuwać metod — to wrappery dla kompatybilności wstecznej.
NIE zmieniać żadnych innych metod w Helpers.
</action>
<verify>Sprawdzić że Helpers::send_email(), get_token(), is_token_valid() delegują do nowych klas</verify>
<done>AC-3 satisfied: Wrappery delegują, istniejący kod działa bez zmian</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- autoload/Domain/* (nie ruszać repositories)
- autoload/Shared/Cache/* (nie ruszać)
- autoload/Shared/Html/* (nie ruszać)
- autoload/Shared/Image/* (nie ruszać)
- autoload/Shared/Tpl/* (nie ruszać)
- config.php, libraries/* (nie ruszać)
## SCOPE LIMITS
- Tylko Email i Security — nie refaktoryzować innych metod Helpers
- Nie zmieniać callerów (admin/, front/) — oni nadal używają Helpers::
- Nie dodawać nowych zależności poza tym co już jest w libraries/
</boundaries>
<verification>
Before declaring plan complete:
- [ ] autoload/Shared/Email/Email.php istnieje z namespace Shared\Email
- [ ] autoload/Shared/Security/CsrfToken.php istnieje z namespace Shared\Security
- [ ] Helpers::send_email() deleguje do Email class
- [ ] Helpers::get_token() deleguje do CsrfToken::getToken()
- [ ] Helpers::is_token_valid() deleguje do CsrfToken::validate()
- [ ] Żadne inne metody w Helpers nie zostały zmienione
- All acceptance criteria met
</verification>
<success_criteria>
- Email i CsrfToken klasy utworzone z poprawnymi namespace'ami
- Wrappery w Helpers zachowują kompatybilność wsteczną
- Zero regresji — istniejący kod używający Helpers:: działa bez zmian
</success_criteria>
<output>
After completion, create `.paul/phases/02-shared-email-security/02-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,108 @@
---
phase: 02-shared-email-security
plan: 01
subsystem: infra
tags: [email, phpmailer, csrf, security, shared]
requires:
- phase: 01-infrastructure
provides: centralny autoloader PSR-4
provides:
- Shared\Email\Email — klasa email z PHPMailer
- Shared\Security\CsrfToken — CSRF z random_bytes + hash_equals
- Wrappery w Helpers dla kompatybilności wstecznej
affects: [phase-06 admin-base, phase-17 users-security]
tech-stack:
added: []
patterns: [wrapper delegation dla Helpers, static utility class dla CsrfToken]
key-files:
created: [autoload/Shared/Email/Email.php, autoload/Shared/Security/CsrfToken.php]
modified: [autoload/Shared/Helpers/Helpers.php]
key-decisions:
- "CsrfToken: single token per session (shopPRO pattern) zamiast multi-token array"
- "Email: PHPMailer require via __DIR__ absolute paths"
- "Helpers::get_token() wywołuje regenerate() + getToken() — zachowuje semantykę jednorazowego tokenu"
patterns-established:
- "Wrapper delegation: stara metoda w Helpers deleguje do nowej klasy"
- "Security: random_bytes(32) + hash_equals() jako standard"
duration: ~8min
completed: 2026-04-04
---
# Phase 2 Plan 01: Shared Email + Security Summary
**Shared\Email\Email z PHPMailer i Shared\Security\CsrfToken z kryptograficznie bezpiecznym tokenem, plus wrappery w Helpers.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~8min |
| Completed | 2026-04-04 |
| Tasks | 3 completed |
| Files modified | 3 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Email class | Pass | send(), email_check(), load_by_name(), PHPMailer |
| AC-2: CsrfToken class | Pass | random_bytes(32), hash_equals(), regenerate() |
| AC-3: Wrappery w Helpers | Pass | send_email(), get_token(), is_token_valid() delegują |
## Accomplishments
- Utworzono `Shared\Email\Email` z pełną obsługą PHPMailer, załączników, reply-to, regex URL fix
- Utworzono `Shared\Security\CsrfToken` z kryptograficznie bezpiecznym tokenem (upgrade z sha1/mt_rand)
- Wrappery w Helpers zachowują pełną kompatybilność wsteczną
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/Shared/Email/Email.php` | Created | OOP Email z PHPMailer |
| `autoload/Shared/Security/CsrfToken.php` | Created | CSRF token management |
| `autoload/Shared/Helpers/Helpers.php` | Modified | Wrappery delegujące do nowych klas |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Single token per session (CsrfToken) | Wzór shopPRO, prostsze, bezpieczniejsze | Legacy multi-token array zastąpiony |
| get_token() = regenerate() + getToken() | Zachowuje semantykę: każde wywołanie daje nowy token | Kompatybilność z kodem który zakłada jednorazowy token |
| PHPMailer require via __DIR__ | Absolute paths, działa z każdego entry pointa | Eliminuje problem relatywnych ścieżek |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Scope additions | 1 | Minimal — Email.send() ma $replay param z cmsPRO |
Email.send() w cmsPRO ma dodatkowy parametr `$replay` (reply-to) którego shopPRO nie ma. Zachowano dla kompatybilności z istniejącym kodem.
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Shared layer kompletny (Cache, Helpers, Html, Image, Tpl, Email, Security)
- Fazy 3-5 (Domain repositories) mogą startować
**Concerns:**
- None
**Blockers:**
- None
---
*Phase: 02-shared-email-security, Plan: 01*
*Completed: 2026-04-04*

View File

@@ -0,0 +1,198 @@
---
phase: 03-domain-scontainers-banners
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/Domain/Scontainers/ScontainersRepository.php
- autoload/Domain/Banners/BannersRepository.php
- autoload/admin/factory/class.Scontainers.php
- autoload/admin/factory/class.Banners.php
- autoload/front/factory/class.Scontainers.php
- autoload/front/factory/class.Banners.php
autonomous: true
delegation: auto
---
<objective>
## Goal
Create Domain\Scontainers\ScontainersRepository and Domain\Banners\BannersRepository, then convert legacy factory classes to wrapper delegation.
## Purpose
Continue DDD refactoring — migrate Scontainers and Banners data access from static factory methods (global $mdb) to injected-dependency Domain repositories. Establishes wrapper delegation pattern for the first time in the project.
## Output
- 2 new Domain repository files
- 4 legacy factory files converted to wrappers
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Source Files
@autoload/admin/factory/class.Scontainers.php
@autoload/admin/factory/class.Banners.php
@autoload/front/factory/class.Scontainers.php
@autoload/front/factory/class.Banners.php
@autoload/Domain/Languages/LanguagesRepository.php (pattern reference)
</context>
<acceptance_criteria>
## AC-1: ScontainersRepository exists with all methods
```gherkin
Given the autoloader is configured for Domain\ namespace
When ScontainersRepository is instantiated with $db (Medoo)
Then it provides containerDetails(), containerSave(), containerDelete(), scontainerByLang() methods
And all methods use $this->db instead of global $mdb
```
## AC-2: BannersRepository exists with all methods
```gherkin
Given the autoloader is configured for Domain\ namespace
When BannersRepository is instantiated with $db (Medoo)
Then it provides bannerDetails(), bannerSave(), bannerDelete(), activeBanners(), mainBanner() methods
And all methods use $this->db instead of global $mdb
```
## AC-3: Legacy admin factories delegate to repositories
```gherkin
Given admin\factory\Scontainers and admin\factory\Banners exist
When their static methods are called (e.g. container_save(), banner_delete())
Then they instantiate the Domain repository with global $mdb
And delegate the call to the corresponding repository method
And return the same result as before
```
## AC-4: Legacy front factories delegate to repositories
```gherkin
Given front\factory\Scontainers and front\factory\Banners exist
When their static methods are called (e.g. scontainer_details(), banners())
Then they delegate to the Domain repository
And caching behavior is preserved (Cache::fetch/store in repository)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Create ScontainersRepository and BannersRepository</name>
<files>autoload/Domain/Scontainers/ScontainersRepository.php, autoload/Domain/Banners/BannersRepository.php</files>
<action>
Create Domain\Scontainers\ScontainersRepository following LanguagesRepository pattern:
- namespace Domain\Scontainers
- Constructor: __construct($db) storing Medoo instance
- containerDetails($containerId): get from pp_scontainers + pp_scontainers_langs (all langs)
- containerSave($containerId, $title, $text, $status, $showTitle, $src, $html): insert/update pp_scontainers + pp_scontainers_langs with multi-language support. Handle single-lang vs multi-lang arrays exactly as current factory does. Call \S::delete_cache() after.
- containerDelete($containerId): delete from pp_scontainers, call \S::delete_cache()
- scontainerByLang($scontainerId, $langId): get container + single lang translation, use \Shared\Cache\CacheHandler::fetch/store (migrate from \Cache:: to \Shared\Cache\CacheHandler::)
Create Domain\Banners\BannersRepository following same pattern:
- namespace Domain\Banners
- Constructor: __construct($db)
- bannerDetails($bannerId): get from pp_banners + pp_banners_langs (all langs)
- bannerSave($bannerId, $name, $status, $dateStart, $dateEnd, $homePage, $src, $url, $html, $text): insert/update pp_banners + pp_banners_langs. Handle single/multi lang arrays. Call \S::delete_cache().
- bannerDelete($bannerId): delete from pp_banners, call \S::delete_cache()
- activeBanners($langId): active non-homepage banners with date filtering, use \Shared\Cache\CacheHandler for caching
- mainBanner($langId): single active homepage banner with date filtering, cached
IMPORTANT:
- PHP < 8.0 compatible (no match, no named args, no union types, no str_contains)
- Use $this->db->query() for complex SQL (date filtering in Banners) — keep raw SQL identical to current factory
- Multi-language save pattern: query pp_langs for active languages, loop and insert translations
- Status/checkbox conversion ('on' → 1, else 0) stays in repository methods
</action>
<verify>php -l autoload/Domain/Scontainers/ScontainersRepository.php && php -l autoload/Domain/Banners/BannersRepository.php</verify>
<done>AC-1 and AC-2 satisfied: Both repositories exist with all methods, use injected $db</done>
</task>
<task type="auto">
<name>Task 2: Convert legacy factories to wrapper delegation</name>
<files>autoload/admin/factory/class.Scontainers.php, autoload/admin/factory/class.Banners.php, autoload/front/factory/class.Scontainers.php, autoload/front/factory/class.Banners.php</files>
<action>
Convert all 4 factory files to thin wrappers that delegate to Domain repositories.
Pattern for each static method:
```php
public static function method_name($args)
{
global $mdb;
$repo = new \Domain\Scontainers\ScontainersRepository($mdb);
return $repo->methodName($args);
}
```
admin\factory\Scontainers:
- container_delete($id) → $repo->containerDelete($id)
- container_save(...) → $repo->containerSave(...)
- container_details($id) → $repo->containerDetails($id)
admin\factory\Banners:
- banner_delete($id) → $repo->bannerDelete($id)
- banner_save(...) → $repo->bannerSave(...)
- banner_details($id) → $repo->bannerDetails($id)
front\factory\Scontainers:
- scontainer_details($id) → $repo->scontainerByLang($id, $lang[0]) — note: use global $lang
front\factory\Banners:
- banners() → $repo->activeBanners($lang[0])
- main_banner() → $repo->mainBanner($lang[0])
IMPORTANT:
- Keep namespace declarations unchanged (admin\factory, front\factory)
- Keep method signatures identical (same parameter names and order)
- For front factories: pass $lang[0] explicitly to repository (repo does NOT use global $lang)
</action>
<verify>php -l autoload/admin/factory/class.Scontainers.php && php -l autoload/admin/factory/class.Banners.php && php -l autoload/front/factory/class.Scontainers.php && php -l autoload/front/factory/class.Banners.php</verify>
<done>AC-3 and AC-4 satisfied: All legacy factories delegate to Domain repositories, signatures unchanged</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- autoload/autoloader.php (autoloader stable)
- composer.json (PSR-4 mapping already includes Domain\)
- autoload/admin/controls/class.Scontainers.php (admin controllers — Phase 10)
- autoload/admin/controls/class.Banners.php (admin controllers — Phase 10)
- autoload/admin/view/ (admin views — later phases)
- autoload/front/view/ (front views — later phases)
- autoload/class.Scontainer.php (legacy ArrayAccess entity — separate concern)
- Any existing Domain\ repositories (Articles, Languages, Layouts, Pages, Settings, User)
## SCOPE LIMITS
- Only factory → repository migration, NOT admin controllers or views
- No new Composer dependencies
- No database schema changes
- Do not refactor the multi-language save pattern (keep it working as-is)
</boundaries>
<verification>
Before declaring plan complete:
- [ ] php -l passes for all 6 files (2 new + 4 modified)
- [ ] ScontainersRepository has: containerDetails, containerSave, containerDelete, scontainerByLang
- [ ] BannersRepository has: bannerDetails, bannerSave, bannerDelete, activeBanners, mainBanner
- [ ] All 4 factory files are thin wrappers (no direct $mdb usage, only delegation)
- [ ] No PHP 8.0+ syntax used
- [ ] \S::delete_cache() calls preserved in repository methods
- [ ] Caching (\Shared\Cache\CacheHandler) used in front-facing repository methods
</verification>
<success_criteria>
- All tasks completed
- All verification checks pass
- Zero regression — factory method signatures unchanged
- Domain repositories follow established pattern (constructor DI, $this->db)
</success_criteria>
<output>
After completion, create `.paul/phases/03-domain-scontainers-banners/03-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,115 @@
---
phase: 03-domain-scontainers-banners
plan: 01
subsystem: domain
tags: [medoo, repository, scontainers, banners, wrapper-delegation]
requires:
- phase: 01-infrastructure
provides: PSR-4 autoloader for Domain\ namespace
provides:
- Domain\Scontainers\ScontainersRepository
- Domain\Banners\BannersRepository
- Wrapper delegation pattern (first usage in project)
affects: [phase-10-admin-banners-authors-scontainers, phase-15-front-pages-menu-banners-scontainers]
tech-stack:
added: []
patterns: [wrapper-delegation, domain-repository-with-cache]
key-files:
created:
- autoload/Domain/Scontainers/ScontainersRepository.php
- autoload/Domain/Banners/BannersRepository.php
modified:
- autoload/admin/factory/class.Scontainers.php
- autoload/admin/factory/class.Banners.php
- autoload/front/factory/class.Scontainers.php
- autoload/front/factory/class.Banners.php
key-decisions:
- "Wrapper delegation pattern: factory static methods delegate to repo instances via global $mdb"
- "Front factories pass $lang[0] explicitly — repositories do not use global $lang"
- "Caching migrated from \\Cache:: to \\Shared\\Cache\\CacheHandler:: in repository layer"
patterns-established:
- "Wrapper delegation: global $mdb; $repo = new \\Domain\\X\\XRepository($mdb); return $repo->method()"
- "Front factory passes language ID explicitly to repository"
duration: ~2min
started: 2026-04-04T00:00:00Z
completed: 2026-04-04T00:00:00Z
---
# Phase 3 Plan 01: Scontainers + Banners Repositories Summary
**Domain repositories for Scontainers and Banners with wrapper delegation in all 4 legacy factories.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~2min |
| Tasks | 2 completed (delegated) |
| Files created | 2 |
| Files modified | 4 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: ScontainersRepository exists with all methods | Pass | 5 methods (incl. constructor), 110 lines |
| AC-2: BannersRepository exists with all methods | Pass | 6 methods (incl. constructor), 148 lines |
| AC-3: Legacy admin factories delegate to repositories | Pass | 6 static methods → thin wrappers |
| AC-4: Legacy front factories delegate to repositories | Pass | 3 static methods → thin wrappers, $lang[0] passed explicitly |
## Accomplishments
- Created ScontainersRepository with containerDetails, containerSave, containerDelete, scontainerByLang
- Created BannersRepository with bannerDetails, bannerSave, bannerDelete, activeBanners, mainBanner
- Established wrapper delegation pattern — first usage in the project, template for all future phases
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/Domain/Scontainers/ScontainersRepository.php` | Created | Domain repository for scontainers CRUD + cached front read |
| `autoload/Domain/Banners/BannersRepository.php` | Created | Domain repository for banners CRUD + cached active/main banner |
| `autoload/admin/factory/class.Scontainers.php` | Modified | Wrapper: 3 methods delegate to ScontainersRepository |
| `autoload/admin/factory/class.Banners.php` | Modified | Wrapper: 3 methods delegate to BannersRepository |
| `autoload/front/factory/class.Scontainers.php` | Modified | Wrapper: 1 method delegates with $lang[0] |
| `autoload/front/factory/class.Banners.php` | Modified | Wrapper: 2 methods delegate with $lang[0] |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Wrapper creates new repo instance per call | Matches static factory pattern, no singleton needed | Simple, no state leaks between calls |
| Front repos use CacheHandler, not \Cache | Aligns with Shared layer conventions | Consistent caching across Domain layer |
| $lang[0] passed as parameter, not global in repo | Repositories should not depend on globals | Cleaner, testable API |
## Deviations from Plan
None — plan executed exactly as written.
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Domain\Scontainers and Domain\Banners available for Admin controllers (Phase 10)
- Wrapper delegation pattern established for future Domain phases (4, 5)
**Concerns:**
- None
**Blockers:**
- None
---
*Phase: 03-domain-scontainers-banners, Plan: 01*
*Completed: 2026-04-04*

View File

@@ -0,0 +1,216 @@
---
phase: 04-domain-authors-newsletter
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/Domain/Authors/AuthorsRepository.php
- autoload/Domain/Newsletter/NewsletterRepository.php
- autoload/admin/factory/class.Authors.php
- autoload/admin/factory/class.Newsletter.php
- autoload/front/factory/class.Authors.php
- autoload/front/factory/class.Newsletter.php
autonomous: true
delegation: auto
---
<objective>
## Goal
Create Domain\Authors\AuthorsRepository and Domain\Newsletter\NewsletterRepository, then convert legacy factory classes to wrapper delegation.
## Purpose
Continue DDD refactoring — migrate Authors and Newsletter data access to Domain repositories using established wrapper delegation pattern from Phase 3.
## Output
- 2 new Domain repository files
- 4 legacy factory files converted to wrappers
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/03-domain-scontainers-banners/03-01-SUMMARY.md (wrapper delegation pattern reference)
## Source Files
@autoload/admin/factory/class.Authors.php
@autoload/admin/factory/class.Newsletter.php
@autoload/front/factory/class.Authors.php
@autoload/front/factory/class.Newsletter.php
@autoload/Domain/Languages/LanguagesRepository.php (pattern reference)
</context>
<acceptance_criteria>
## AC-1: AuthorsRepository exists with all methods
```gherkin
Given the autoloader is configured for Domain\ namespace
When AuthorsRepository is instantiated with $db (Medoo)
Then it provides simpleList(), authorDetails(), authorSave(), authorDelete(), authorByLang() methods
And all methods use $this->db instead of global $mdb
```
## AC-2: NewsletterRepository exists with all methods
```gherkin
Given the autoloader is configured for Domain\ namespace
When NewsletterRepository is instantiated with $db (Medoo)
Then it provides emailsImport(), isAdminTemplate(), templateDelete(), send(), templateDetails(), templateSave(), templatesList(), unsubscribe(), confirm(), newsletterSend(), getHash(), signin(), getTemplate(), signout() methods
And all methods use $this->db instead of global $mdb
```
## AC-3: Legacy admin factories delegate to repositories
```gherkin
Given admin\factory\Authors and admin\factory\Newsletter exist
When their static methods are called
Then they instantiate the Domain repository with global $mdb
And delegate the call to the corresponding repository method
And return the same result as before
```
## AC-4: Legacy front factories delegate to repositories
```gherkin
Given front\factory\Authors and front\factory\Newsletter exist
When their static methods are called
Then they delegate to the Domain repository
And caching behavior is preserved (in repository for Authors)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Create AuthorsRepository and NewsletterRepository</name>
<files>autoload/Domain/Authors/AuthorsRepository.php, autoload/Domain/Newsletter/NewsletterRepository.php</files>
<action>
Create Domain\Authors\AuthorsRepository following established pattern:
- namespace Domain\Authors
- Constructor: __construct($db) storing Medoo instance
- simpleList(): select from pp_authors, return array (from admin get_simple_list)
- authorDetails($authorId): get from pp_authors + select pp_authors_langs, return with ['languages'][$lang_id] sub-array
- authorSave($authorId, $author, $image, $description): insert/update pp_authors + pp_authors_langs with multi-language support. Same pattern as ScontainersRepository: query pp_langs for active languages, handle single vs multi lang arrays. Call \S::delete_cache() after.
- authorDelete($authorId): delete from pp_authors, call \S::delete_cache(), return result
- authorByLang($authorId, $langId): cached read using \Shared\Cache\CacheHandler::fetch("get_single_author:$authorId"). Get from pp_authors + pp_authors_langs for specific lang. Cache and return. Note: cache key does NOT include langId (matching original front factory).
Create Domain\Newsletter\NewsletterRepository following same pattern:
- namespace Domain\Newsletter
- Constructor: __construct($db)
- emailsImport($emails): parse comma/newline separated emails, validate with filter_var, insert unique into pp_newsletter. Return count of imported.
- isAdminTemplate($templateId): check if template exists in pp_newsletter_templates where id and admin=1. Return boolean.
- templateDelete($templateId): delete from pp_newsletter_templates where id. Return result.
- send($dates, $template, $onlyOnce): insert into pp_newsletter_send for each subscriber email from pp_newsletter. If $onlyOnce, check pp_newsletter_send for existing entries. Complex logic — replicate exactly from admin factory.
- templateDetails($templateId): get single template from pp_newsletter_templates.
- templateSave($id, $name, $text): insert/update pp_newsletter_templates. Call \S::delete_cache().
- templatesList(): select all from pp_newsletter_templates ordered.
- unsubscribe($hash): update pp_newsletter set status=0 where hash=$hash. Return result.
- confirm($hash): update pp_newsletter set status=1 where hash=$hash. Return result.
- newsletterSend($limit): select from pp_newsletter_send with limit, send emails via loop, delete sent entries. Replicate exactly from front factory.
- getHash($email): select hash from pp_newsletter where email. Return hash or false.
- signin($email): insert into pp_newsletter with email, hash (md5), status=0. Return result or hash.
- getTemplate($templateName): get template from pp_newsletter_templates where name=$templateName. Return template.
- signout($email): delete from pp_newsletter where email=$email. Return result.
IMPORTANT:
- PHP < 8.0 compatible
- Replicate logic EXACTLY from factory files — read them first
- Multi-language save pattern same as Phase 3 repos
- Keep all \S::delete_cache() calls where they exist in originals
- Newsletter send() and newsletterSend() are complex — read carefully and replicate precisely
</action>
<verify>php -l autoload/Domain/Authors/AuthorsRepository.php && php -l autoload/Domain/Newsletter/NewsletterRepository.php</verify>
<done>AC-1 and AC-2 satisfied: Both repositories exist with all methods, use injected $db</done>
</task>
<task type="auto">
<name>Task 2: Convert legacy factories to wrapper delegation</name>
<files>autoload/admin/factory/class.Authors.php, autoload/admin/factory/class.Newsletter.php, autoload/front/factory/class.Authors.php, autoload/front/factory/class.Newsletter.php</files>
<action>
Convert all 4 factory files to thin wrappers using Phase 3 pattern:
```php
public static function method_name($args)
{
global $mdb;
$repo = new \Domain\Authors\AuthorsRepository($mdb);
return $repo->methodName($args);
}
```
admin\factory\Authors:
- get_simple_list() → $repo->simpleList()
- delete_author($id_author) → $repo->authorDelete($id_author)
- save_author($id_author, $author, $image, $description) → $repo->authorSave($id_author, $author, $image, $description)
admin\factory\Newsletter:
- emails_import($emails) → $repo->emailsImport($emails)
- is_admin_template($template_id) → $repo->isAdminTemplate($template_id)
- newsletter_template_delete($template_id) → $repo->templateDelete($template_id)
- send($dates, $template, $only_once) → $repo->send($dates, $template, $only_once)
- email_template_detalis($id_template) → $repo->templateDetails($id_template)
- template_save($id, $name, $text) → $repo->templateSave($id, $name, $text)
- templates_list() → $repo->templatesList()
front\factory\Authors:
- get_single_author($id_author) → global $mdb; $repo = new \Domain\Authors\AuthorsRepository($mdb); return $repo->authorByLang($id_author, null);
Note: front factory uses global $lang but the cache key doesn't include lang — pass null or handle in repo. Check original carefully.
front\factory\Newsletter:
- newsletter_unsubscribe($hash) → $repo->unsubscribe($hash)
- newsletter_confirm($hash) → $repo->confirm($hash)
- newsletter_send($limit = 5) → $repo->newsletterSend($limit)
- get_hash($email) → $repo->getHash($email)
- newsletter_signin($email) → $repo->signin($email)
- get_template($template_name) → $repo->getTemplate($template_name)
- newsletter_signout($email) → $repo->signout($email)
IMPORTANT:
- Keep namespaces and method signatures IDENTICAL
- Read each file first before editing
- Each method = thin 3-line wrapper
</action>
<verify>php -l autoload/admin/factory/class.Authors.php && php -l autoload/admin/factory/class.Newsletter.php && php -l autoload/front/factory/class.Authors.php && php -l autoload/front/factory/class.Newsletter.php</verify>
<done>AC-3 and AC-4 satisfied: All legacy factories delegate to Domain repositories</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- autoload/autoloader.php
- composer.json
- autoload/admin/controls/ (admin controllers — later phases)
- autoload/admin/view/ (admin views — later phases)
- autoload/front/view/ (front views — later phases)
- Any existing Domain\ repositories (Articles, Languages, Layouts, Pages, Settings, User, Scontainers, Banners)
## SCOPE LIMITS
- Only factory → repository migration
- No new Composer dependencies
- No database schema changes
</boundaries>
<verification>
Before declaring plan complete:
- [ ] php -l passes for all 6 files (2 new + 4 modified)
- [ ] AuthorsRepository has: simpleList, authorDetails, authorSave, authorDelete, authorByLang
- [ ] NewsletterRepository has: emailsImport, isAdminTemplate, templateDelete, send, templateDetails, templateSave, templatesList, unsubscribe, confirm, newsletterSend, getHash, signin, getTemplate, signout
- [ ] All 4 factory files are thin wrappers (no direct $mdb usage)
- [ ] No PHP 8.0+ syntax used
- [ ] \S::delete_cache() calls preserved where originals had them
</verification>
<success_criteria>
- All tasks completed
- All verification checks pass
- Zero regression — factory method signatures unchanged
- Domain repositories follow established pattern
</success_criteria>
<output>
After completion, create `.paul/phases/04-domain-authors-newsletter/04-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,111 @@
---
phase: 04-domain-authors-newsletter
plan: 01
subsystem: domain
tags: [medoo, repository, authors, newsletter, wrapper-delegation]
requires:
- phase: 01-infrastructure
provides: PSR-4 autoloader for Domain\ namespace
provides:
- Domain\Authors\AuthorsRepository
- Domain\Newsletter\NewsletterRepository
affects: [phase-10-admin-banners-authors-scontainers, phase-11-admin-newsletter-emails-seoadditional]
tech-stack:
added: []
patterns: [wrapper-delegation, globals-to-parameters]
key-files:
created:
- autoload/Domain/Authors/AuthorsRepository.php
- autoload/Domain/Newsletter/NewsletterRepository.php
modified:
- autoload/admin/factory/class.Authors.php
- autoload/admin/factory/class.Newsletter.php
- autoload/front/factory/class.Authors.php
- autoload/front/factory/class.Newsletter.php
key-decisions:
- "Newsletter methods using global $settings/$lang now take them as explicit parameters"
- "authorByLang cache key preserved from original (no langId in key)"
patterns-established:
- "Globals-to-parameters: when repo method needs $settings or $lang, wrapper passes them explicitly"
duration: ~2min
started: 2026-04-04T00:00:00Z
completed: 2026-04-04T00:00:00Z
---
# Phase 4 Plan 01: Authors + Newsletter Repositories Summary
**Domain repositories for Authors (5 methods) and Newsletter (14 methods) with wrapper delegation and globals-to-parameters pattern.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~2min |
| Tasks | 2 completed (delegated) |
| Files created | 2 |
| Files modified | 4 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: AuthorsRepository exists with all methods | Pass | 5 methods, 156 lines |
| AC-2: NewsletterRepository exists with all methods | Pass | 14 methods, 281 lines |
| AC-3: Legacy admin factories delegate to repositories | Pass | 11 static methods → wrappers |
| AC-4: Legacy front factories delegate to repositories | Pass | 8 static methods → wrappers, globals passed as params |
## Accomplishments
- Created AuthorsRepository with simpleList, authorDetails, authorSave, authorDelete, authorByLang
- Created NewsletterRepository with full subscriber lifecycle + template CRUD + sending
- Established globals-to-parameters pattern for methods needing $settings/$lang
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/Domain/Authors/AuthorsRepository.php` | Created | Domain repository for authors CRUD + cached front read |
| `autoload/Domain/Newsletter/NewsletterRepository.php` | Created | Domain repository for newsletter subscriber lifecycle, templates, sending |
| `autoload/admin/factory/class.Authors.php` | Modified | Wrapper: 4 methods delegate to AuthorsRepository |
| `autoload/admin/factory/class.Newsletter.php` | Modified | Wrapper: 7 methods delegate to NewsletterRepository |
| `autoload/front/factory/class.Authors.php` | Modified | Wrapper: 1 method delegates |
| `autoload/front/factory/class.Newsletter.php` | Modified | Wrapper: 7 methods delegate, passing $settings/$lang |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Globals as parameters for newsletterSend/signin | Repos should not depend on globals | Front wrappers pass $settings, $lang explicitly |
| Preserve original cache key for authorByLang | Backward compatibility with existing cache | Cache key "get_single_author:$id" without langId |
## Deviations from Plan
None — plan executed exactly as written.
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Domain\Authors and Domain\Newsletter available for Admin controllers (Phases 10, 11)
- All Domain repos for phases 3-4 complete
**Concerns:**
- None
**Blockers:**
- None
---
*Phase: 04-domain-authors-newsletter, Plan: 01*
*Completed: 2026-04-04*

View File

@@ -1 +1 @@
{"version":2,"defects":{"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenDbReturnsNull":8,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsUsesCache":8},"times":{"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0.028,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsEmptyWhenNull":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsRowWhenFound":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsNullWhenNotFound":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesQueriesDbAndCaches":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsEmptyWhenNull":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testMaxOrderReturnsInteger":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsTrueOnSuccess":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsFalseOnFailure":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDetailsReturnsRowOrNull":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsMappedArray":0.001,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenDbReturnsNull":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsUsesCache":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testUpdateCallsDbUpdateWhenParamExists":0.001,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testUpdateCallsDbInsertWhenParamMissing":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testVisitCounterReturnsValue":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testVisitCounterReturnsNullWhenEmpty":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserArray":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindByLoginReturnsUser":0.002,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testAllReturnsArray":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testAllReturnsEmptyArrayWhenNull":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsTrueForAdminUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsTrueWhenPrivilegeExists":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsFalseWhenPrivilegeMissing":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroWhenUserNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsMinusOneWhenAccountBlocked":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsOneOnSuccess":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsTrueWhenExists":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsFalseWhenFree":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenUserNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenTooManyFailedAttempts":0.079,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenExpired":0.08,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueOnValidCode":0.159,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordTooShort":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordsMismatch":0}} {"version":2,"defects":{"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenDbReturnsNull":8,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsUsesCache":8},"times":{"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0.041,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsEmptyWhenNull":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsRowWhenFound":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsNullWhenNotFound":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesQueriesDbAndCaches":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsEmptyWhenNull":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testMaxOrderReturnsInteger":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsTrueOnSuccess":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsFalseOnFailure":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDetailsReturnsRowOrNull":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsMappedArray":0.001,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenDbReturnsNull":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsUsesCache":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testUpdateCallsDbUpdateWhenParamExists":0.001,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testUpdateCallsDbInsertWhenParamMissing":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testVisitCounterReturnsValue":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testVisitCounterReturnsNullWhenEmpty":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserArray":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindByLoginReturnsUser":0.002,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testAllReturnsArray":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testAllReturnsEmptyArrayWhenNull":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsTrueForAdminUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsTrueWhenPrivilegeExists":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsFalseWhenPrivilegeMissing":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroWhenUserNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsMinusOneWhenAccountBlocked":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsOneOnSuccess":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsTrueWhenExists":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsFalseWhenFree":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenUserNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenTooManyFailedAttempts":0.075,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenExpired":0.073,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueOnValidCode":0.148,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordTooShort":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordsMismatch":0}}

View File

@@ -45,7 +45,9 @@ ignored_paths: []
# Added on 2025-04-18 # Added on 2025-04-18
read_only: false read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. # list of tool names to exclude.
# This extends the existing exclusions (e.g. from the global configuration)
#
# Below is the complete list of tools for convenience. # Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions, # To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`. # execute `uv run scripts/print_tool_overview.py`.
@@ -86,7 +88,8 @@ read_only: false
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. # * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: [] excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
included_optional_tools: [] included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. # fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
@@ -112,8 +115,10 @@ default_modes:
# (contrary to the memories, which are loaded on demand). # (contrary to the memories, which are loaded on demand).
initial_prompt: "" initial_prompt: ""
# override of the corresponding setting in serena_config.yml, see the documentation there. # time budget (seconds) per tool call for the retrieval of additional symbol information
# If null or missing, the value from the global config is used. # such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget: symbol_info_budget:
# The language backend to use for this project. # The language backend to use for this project.
@@ -122,3 +127,26 @@ symbol_info_budget:
# Note: the backend is fixed at startup. If a project with a different backend # Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned. # is activated post-init, an error will be returned.
language_backend: language_backend:
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
# list of regex patterns for memories to completely ignore.
# Matching memories will not appear in list_memories or activate_project output
# and cannot be accessed via read_memory or write_memory.
# To access ignored memory files, use the read_file tool on the raw file path.
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
ls_specific_settings: {}

8
.vscode/ftp-kr.json vendored
View File

@@ -12,6 +12,12 @@
"ignoreRemoteModification": true, "ignoreRemoteModification": true,
"ignore": [ "ignore": [
".git", ".git",
"/.vscode" "/.vscode",
"/.claude",
"/.serena",
"/docs",
"AGENTS.md",
"CLAUDE.md",
"/.paul"
] ]
} }

41
AGENTS.md Normal file
View File

@@ -0,0 +1,41 @@
# Workflow
## Sposób pracy
- Pisz do mnie po polsku, zwięźle i krótko, ale merytorycznie
## Zasady pisania kodu
- Kod ma być czytelny „dla obcego”: jasne nazwy, mało magii
- Brak „skrótów na szybko” typu logika w widokach, copy-paste, losowe helpery bez spójności
- Każda funkcja/klasa ma mieć jedną odpowiedzialność, zwykle do 3050 linii (jeśli dłuższe dzielić)
- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod
- Nazewnictwo:
- klasy: PascalCase
- metody/zmienne: camelCase
- stałe: UPPER_SNAKE_CASE
- Zero „skrótologii” w nazwach (np. $d, $tmp, $x1) poza pętlami 23 linijki
- medoo + prepared statements bez wyjątków (żadnego sklejania SQL stringiem)
- XSS: escape w widokach (np. helper e())
- CSRF dla formularzy, sensowna obsługa sesji
- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego”, nie „co”
## Wprowadzanie zmian
- Przeanalizuj wprowadzone zadanie
- Jeżeli masz jakieś wątpliwości pytaj
- Przedstaw plan
- Po akceptacji wdróź plan
## KONIEC PRACY
Gdy użytkownik napisze `KONIEC PRACY`, wykonaj kolejno:
1. Przeprowadzenie testów.
2. Aktualizacja dokumentacji technicznej, jeśli zmiany tego wymagają:
- `docs/PROJECT_STRUCTURE.md`
- `docs/FORM_EDIT_SYSTEM.md`
3. Migracje SQL (jeśli były zmiany w bazie danych):
- Plik: `migrations/{version}.sql` (np. `migrations/0.304.sql`)
- **NIE** w `updates/` — build script sam wczyta z `migrations/`
- Sprawdź czy plik istnieje i jest poprawnie nazwany przed commitem
4. Commit.
5. Push.

View File

@@ -2,15 +2,4 @@
## KONIEC PRACY ## KONIEC PRACY
Gdy użytkownik napisze `KONIEC PRACY`, wykonaj kolejno: Gdy użytkownik napisze `KONIEC PRACY`, uruchom komendę `/koniec-pracy`.
1. Przeprowadzenie testów.
2. Aktualizacja dokumentacji technicznej, jeśli zmiany tego wymagają:
- `docs/PROJECT_STRUCTURE.md`
- `docs/FORM_EDIT_SYSTEM.md`
3. Migracje SQL (jeśli były zmiany w bazie danych):
- Plik: `migrations/{version}.sql` (np. `migrations/0.304.sql`)
- **NIE** w `updates/` — build script sam wczyta z `migrations/`
- Sprawdź czy plik istnieje i jest poprawnie nazwany przed commitem
4. Commit.
5. Push.

View File

@@ -1,25 +1,6 @@
<? <?
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED ); error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
function __autoload_my_classes( $classname ) require_once __DIR__ . '/../autoload/autoloader.php';
{
$q = explode( '\\' , $classname );
$c = array_pop( $q );
if ( $c == 'Savant3' )
{
require_once( '../autoload/Savant3.php' );
return true;
}
// 1. Legacy: class.ClassName.php
$f = '../autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = '../autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );
require_once '../config.php'; require_once '../config.php';
require_once '../libraries/medoo/medoo.php'; require_once '../libraries/medoo/medoo.php';

View File

@@ -12,20 +12,7 @@ if ( file_exists( 'ip.conf' ) )
} }
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED ); error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
function __autoload_my_classes( $classname ) require_once __DIR__ . '/../autoload/autoloader.php';
{
$q = explode( '\\' , $classname );
$c = array_pop( $q );
// 1. Legacy: class.ClassName.php
$f = '../autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = '../autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );
require_once '../config.php'; require_once '../config.php';
require_once '../libraries/medoo/medoo.php'; require_once '../libraries/medoo/medoo.php';

View File

@@ -1,19 +1,6 @@
<?php <?php
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED ); error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
function __autoload_my_classes( $classname ) require_once __DIR__ . '/autoload/autoloader.php';
{
$q = explode( '\\' , $classname );
$c = array_pop( $q );
// 1. Legacy: class.ClassName.php
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = 'autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );
date_default_timezone_set( 'Europe/Warsaw' ); date_default_timezone_set( 'Europe/Warsaw' );
require_once 'config.php'; require_once 'config.php';

17
api.php
View File

@@ -1,19 +1,6 @@
<?php <?php
error_reporting(E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED); error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
function __autoload_my_classes($classname) require_once __DIR__ . '/autoload/autoloader.php';
{
$q = explode('\\', $classname);
$c = array_pop($q);
// 1. Legacy: class.ClassName.php
$f = 'autoload/' . implode('/', $q) . '/class.' . $c . '.php';
if (file_exists($f)) { require_once($f); return; }
// 2. PSR-4: ClassName.php
$f = 'autoload/' . implode('/', $q) . '/' . $c . '.php';
if (file_exists($f)) require_once($f);
}
spl_autoload_register('__autoload_my_classes');
date_default_timezone_set('Europe/Warsaw'); date_default_timezone_set('Europe/Warsaw');
require_once 'config.php'; require_once 'config.php';

View File

@@ -0,0 +1,156 @@
<?php
namespace Domain\Authors;
class AuthorsRepository
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* Prosta lista autorow
* @return array|bool
*/
public function simpleList()
{
return $this->db->select('pp_authors', '*', ['ORDER' => ['author' => 'ASC']]);
}
/**
* Szczegoly autora z jezykami
* @param int $authorId
* @return array|bool
*/
public function authorDetails($authorId)
{
$author = $this->db->get('pp_authors', '*', ['id' => (int)$authorId]);
$results = $this->db->select('pp_authors_langs', '*', ['id_author' => (int)$authorId]);
if (is_array($results)) foreach ($results as $row)
$author['languages'][$row['id_lang']] = $row;
return $author;
}
/**
* Zapis autora (insert lub update)
* @param int $authorId
* @param string $author
* @param string $image
* @param string|array $description
* @return int|bool
*/
public function authorSave($authorId, $author, $image, $description)
{
if (!$authorId)
{
$this->db->insert('pp_authors', [
'author' => $author,
'image' => $image
]);
$id = $this->db->id();
if ($id)
{
$i = 0;
$results = $this->db->select('pp_langs', ['id'], ['status' => 1, 'ORDER' => ['o' => 'ASC']]);
if (is_array($results) and count($results) > 1) foreach ($results as $row)
{
$this->db->insert('pp_authors_langs', [
'id_author' => (int)$id,
'id_lang' => $row['id'],
'description' => $description[$i]
]);
$i++;
}
else if (is_array($results) and count($results) == 1) foreach ($results as $row)
{
$this->db->insert('pp_authors_langs', [
'id_author' => (int)$id,
'id_lang' => $row['id'],
'description' => $description
]);
}
\S::delete_cache();
return $id;
}
}
else
{
$this->db->update('pp_authors', [
'author' => $author,
'image' => $image
], [
'id' => (int)$authorId
]);
$this->db->delete('pp_authors_langs', ['id_author' => (int)$authorId]);
$i = 0;
$results = $this->db->select('pp_langs', ['id'], ['status' => 1, 'ORDER' => ['o' => 'ASC']]);
if (is_array($results) and count($results) > 1) foreach ($results as $row)
{
$this->db->insert('pp_authors_langs', [
'id_author' => (int)$authorId,
'id_lang' => $row['id'],
'description' => $description[$i]
]);
$i++;
}
else if (is_array($results) and count($results) == 1) foreach ($results as $row)
{
$this->db->insert('pp_authors_langs', [
'id_author' => (int)$authorId,
'id_lang' => $row['id'],
'description' => $description
]);
}
\S::delete_cache();
return $authorId;
}
return false;
}
/**
* Usuniecie autora
* @param int $authorId
* @return object|bool
*/
public function authorDelete($authorId)
{
$result = $this->db->delete('pp_authors', ['id' => (int)$authorId]);
\S::delete_cache();
return $result;
}
/**
* Szczegoly autora z cache (front)
* @param int $authorId
* @return array|bool
*/
public function authorByLang($authorId)
{
if (!$author = \Shared\Cache\CacheHandler::fetch("get_single_author:$authorId"))
{
$author = $this->db->get('pp_authors', '*', ['id' => (int)$authorId]);
$results = $this->db->select('pp_authors_langs', '*', ['id_author' => (int)$authorId]);
if (is_array($results)) foreach ($results as $row)
$author['languages'][$row['id_lang']] = $row;
\Shared\Cache\CacheHandler::store("get_single_author:$authorId", $author);
}
return $author;
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Domain\Banners;
class BannersRepository
{
private $db;
public function __construct( $db )
{
$this->db = $db;
}
// -------------------------------------------------------------------------
// Odczyt
// -------------------------------------------------------------------------
public function bannerDetails( $bannerId )
{
$banner = $this->db->get( 'pp_banners', '*', [ 'id' => $bannerId ] );
if ( !$banner ) return null;
$langs = $this->db->select( 'pp_banners_langs', '*', [ 'id_banner' => $bannerId ] );
$banner['languages'] = [];
if ( is_array( $langs ) )
foreach ( $langs as $lang )
$banner['languages'][ $lang['id_lang'] ] = $lang;
return $banner;
}
public function activeBanners( $langId )
{
if ( $banners = \Shared\Cache\CacheHandler::fetch( 'banners' ) )
return $banners;
$results = $this->db->query(
'SELECT id, name FROM pp_banners WHERE status = 1 AND ( date_start <= \'' . date( 'Y-m-d' ) . '\' OR date_start IS NULL ) AND ( date_end >= \'' . date( 'Y-m-d' ) . '\' OR date_end IS NULL ) AND home_page = 0'
)->fetchAll( \PDO::FETCH_ASSOC );
$banners = [];
if ( is_array( $results ) )
{
foreach ( $results as $row )
{
$langData = $this->db->get( 'pp_banners_langs', '*', [
'AND' => [ 'id_banner' => $row['id'], 'id_lang' => $langId ]
] );
$row['languages'] = $langData ?: [];
$banners[] = $row;
}
}
\Shared\Cache\CacheHandler::store( 'banners', $banners );
return $banners;
}
public function mainBanner( $langId )
{
$cacheKey = "main_banner:$langId";
if ( $banner = \Shared\Cache\CacheHandler::fetch( $cacheKey ) )
return $banner;
$results = $this->db->query(
'SELECT id, name FROM pp_banners WHERE status = 1 AND ( date_start <= \'' . date( 'Y-m-d' ) . '\' OR date_start IS NULL ) AND ( date_end >= \'' . date( 'Y-m-d' ) . '\' OR date_end IS NULL ) AND home_page = 1 ORDER BY date_end ASC LIMIT 1'
)->fetchAll( \PDO::FETCH_ASSOC );
if ( !is_array( $results ) || empty( $results ) ) return null;
$banner = $results[0];
$langData = $this->db->get( 'pp_banners_langs', '*', [
'AND' => [ 'id_banner' => $banner['id'], 'id_lang' => $langId ]
] );
$banner['languages'] = $langData ?: [];
\Shared\Cache\CacheHandler::store( $cacheKey, $banner );
return $banner;
}
// -------------------------------------------------------------------------
// Zapis / usuwanie
// -------------------------------------------------------------------------
public function bannerSave( $bannerId, $name, $status, $dateStart, $dateEnd, $homePage, $src, $url, $html, $text )
{
$languages = $this->db->select( 'pp_langs', '*', [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( !is_array( $languages ) ) $languages = [];
$langCount = count( $languages );
if ( !$bannerId )
{
$this->db->insert( 'pp_banners', [
'name' => $name,
'status' => $status == 'on' ? 1 : 0,
'date_start' => $dateStart ? $dateStart : null,
'date_end' => $dateEnd ? $dateEnd : null,
'home_page' => $homePage == 'on' ? 1 : 0,
] );
$bannerId = $this->db->id();
if ( !$bannerId ) return false;
foreach ( $languages as $i => $lang )
{
$this->db->insert( 'pp_banners_langs', [
'id_banner' => $bannerId,
'id_lang' => $lang['id'],
'src' => $langCount > 1 ? $src[ $i ] : $src,
'url' => $langCount > 1 ? $url[ $i ] : $url,
'html' => $langCount > 1 ? $html[ $i ] : $html,
'text' => $langCount > 1 ? $text[ $i ] : $text,
] );
}
}
else
{
$this->db->update( 'pp_banners', [
'name' => $name,
'status' => $status == 'on' ? 1 : 0,
'date_start' => $dateStart ? $dateStart : null,
'date_end' => $dateEnd ? $dateEnd : null,
'home_page' => $homePage == 'on' ? 1 : 0,
], [ 'id' => $bannerId ] );
$this->db->delete( 'pp_banners_langs', [ 'id_banner' => $bannerId ] );
foreach ( $languages as $i => $lang )
{
$this->db->insert( 'pp_banners_langs', [
'id_banner' => $bannerId,
'id_lang' => $lang['id'],
'src' => $langCount > 1 ? $src[ $i ] : $src,
'url' => $langCount > 1 ? $url[ $i ] : $url,
'html' => $langCount > 1 ? $html[ $i ] : $html,
'text' => $langCount > 1 ? $text[ $i ] : $text,
] );
}
}
\S::delete_cache();
return $bannerId;
}
public function bannerDelete( $bannerId )
{
$result = $this->db->delete( 'pp_banners', [ 'id' => $bannerId ] );
\S::delete_cache();
return $result;
}
}

View File

@@ -0,0 +1,281 @@
<?php
namespace Domain\Newsletter;
class NewsletterRepository
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* Import emaili do newslettera
* @param string $emails
* @return bool
*/
public function emailsImport($emails)
{
$emails = explode(PHP_EOL, $emails);
if (is_array($emails)) foreach ($emails as $email)
{
if (trim($email) and !$this->db->count('pp_newsletter', ['email' => trim($email)]))
$this->db->insert('pp_newsletter', [
'email' => trim($email),
'hash' => md5($email . time()),
'status' => 1
]);
}
return true;
}
/**
* Sprawdza czy szablon jest adminski
* @param int $templateId
* @return string|bool
*/
public function isAdminTemplate($templateId)
{
return $this->db->get('pp_newsletter_templates', 'is_admin', ['id' => (int)$templateId]);
}
/**
* Usuniecie szablonu newslettera
* @param int $templateId
* @return object|bool
*/
public function templateDelete($templateId)
{
return $this->db->delete('pp_newsletter_templates', ['id' => (int)$templateId]);
}
/**
* Wysylka newslettera - kolejkowanie
* @param string $dates
* @param int $template
* @param string $onlyOnce
* @return bool
*/
public function send($dates, $template, $onlyOnce)
{
$results = $this->db->select('pp_newsletter', 'email', ['status' => 1]);
if (is_array($results) and !empty($results)) foreach ($results as $row)
{
if ($template and $onlyOnce)
{
if (!$this->db->count('pp_newsletter_send', ['AND' => ['id_template' => $template, 'email' => $row]]))
$this->db->insert('pp_newsletter_send', [
'email' => $row,
'dates' => $dates,
'id_template' => $template ? $template : null,
'only_once' => ($onlyOnce == 'on' and $template) ? 1 : 0
]);
}
else
$this->db->insert('pp_newsletter_send', [
'email' => $row,
'dates' => $dates,
'id_template' => $template ? $template : null,
'only_once' => ($onlyOnce == 'on' and $template) ? 1 : 0
]);
}
return true;
}
/**
* Szczegoly szablonu email
* @param int $templateId
* @return array|bool
*/
public function templateDetails($templateId)
{
$result = $this->db->get('pp_newsletter_templates', '*', ['id' => (int)$templateId]);
return $result;
}
/**
* Zapis szablonu (insert lub update)
* @param int $id
* @param string $name
* @param string $text
* @return int|bool
*/
public function templateSave($id, $name, $text)
{
if (!$id)
{
if ($this->db->insert('pp_newsletter_templates', [
'name' => $name,
'text' => $text
]))
{
\S::delete_cache();
return $this->db->id();
}
}
else
{
$this->db->update('pp_newsletter_templates', [
'name' => $name,
'text' => $text
], [
'id' => (int)$id
]);
\S::delete_cache();
return $id;
}
}
/**
* Lista szablonow (nie-adminskich)
* @return array|bool
*/
public function templatesList()
{
return $this->db->select('pp_newsletter_templates', '*', ['is_admin' => 0, 'ORDER' => ['name' => 'ASC']]);
}
/**
* Wypisanie z newslettera po hashu
* @param string $hash
* @return object|bool
*/
public function unsubscribe($hash)
{
return $this->db->update('pp_newsletter', ['status' => 0], ['hash' => $hash]);
}
/**
* Potwierdzenie zapisu po hashu
* @param string $hash
* @return bool
*/
public function confirm($hash)
{
if (!$id = $this->db->get('pp_newsletter', 'id', ['AND' => ['hash' => $hash, 'status' => 0]]))
return false;
else
$this->db->update('pp_newsletter', ['status' => 1], ['id' => $id]);
return true;
}
/**
* Wysylka zakolejkowanych newsletterow (cron/front)
* @param int $limit
* @param array $settings
* @param array $lang
* @return bool
*/
public function newsletterSend($limit, $settings, $lang)
{
$results = $this->db->query('SELECT * FROM pp_newsletter_send WHERE mailed = 0 ORDER BY id ASC LIMIT ' . (int)$limit)->fetchAll();
if (is_array($results) and !empty($results))
{
foreach ($results as $row)
{
$dates = explode(' - ', $row['dates']);
$text = \admin\view\Newsletter::preview(
\admin\factory\Articles::articles_by_date_add($dates[0], $dates[1]),
\admin\factory\Settings::settings_details(),
\admin\factory\Newsletter::email_template_detalis($row['id_template'])
);
if ($settings['ssl']) $base = 'https'; else $base = 'http';
$link = $base . "://" . $_SERVER['SERVER_NAME'] . '/newsletter/unsubscribe/hash=' . $this->getHash($row['email']);
$text = str_replace('[WYPISZ_SIE]', $link, $text);
$regex = "-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|http(|s)://).)*)(['\"][^>]*>)-i";
$text = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text);
$regex = "-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|http(|s)://).)*)(['\"][^>]*>)-i";
$text = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text);
\S::send_email($row['email'], 'Newsletter ze strony: ' . $_SERVER['SERVER_NAME'], $text);
if ($row['only_once'])
$this->db->update('pp_newsletter_send', ['mailed' => 1], ['id' => $row['id']]);
else
$this->db->delete('pp_newsletter_send', ['id' => $row['id']]);
}
return true;
}
return false;
}
/**
* Pobranie hasha dla emaila
* @param string $email
* @return string|bool
*/
public function getHash($email)
{
return $this->db->get('pp_newsletter', 'hash', ['email' => $email]);
}
/**
* Zapis do newslettera z wysylka potwierdzenia
* @param string $email
* @param array $settings
* @param array $lang
* @return bool
*/
public function signin($email, $settings, $lang)
{
if (!\S::email_check($email))
return false;
if (!$this->db->get('pp_newsletter', 'id', ['email' => $email]))
{
$hash = md5(time() . $email);
$text = $settings['newsletter_header'];
$text .= $this->getTemplate('#potwierdzenie-zapisu-do-newslettera');
$text .= $settings['newsletter_footer_1'];
$settings['ssl'] ? $base = 'https' : $base = 'http';
$regex = "-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|http://).)*)(['\"][^>]*>)-i";
$text = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text);
$regex = "-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|http://).)*)(['\"][^>]*>)-i";
$text = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text);
$link = '/newsletter/confirm/hash=' . $hash;
$text = str_replace('[LINK]', $link, $text);
$send = \S::send_email($email, $lang['potwierdz-zapisanie-sie-do-newslettera'], $text);
$this->db->insert('pp_newsletter', ['email' => $email, 'hash' => $hash, 'status' => 0]);
return true;
}
return false;
}
/**
* Pobranie szablonu po nazwie
* @param string $templateName
* @return string|bool
*/
public function getTemplate($templateName)
{
return $this->db->get('pp_newsletter_templates', 'text', ['name' => $templateName]);
}
/**
* Wypisanie z newslettera po emailu
* @param string $email
* @return object|bool
*/
public function signout($email)
{
if ($this->db->get('pp_newsletter', 'id', ['email' => $email]))
return $this->db->delete('pp_newsletter', ['email' => $email]);
return false;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Domain\Scontainers;
class ScontainersRepository
{
private $db;
public function __construct( $db )
{
$this->db = $db;
}
// -------------------------------------------------------------------------
// Odczyt
// -------------------------------------------------------------------------
public function containerDetails( $containerId )
{
$container = $this->db->get( 'pp_scontainers', '*', [ 'id' => $containerId ] );
if ( !$container ) return null;
$langs = $this->db->select( 'pp_scontainers_langs', '*', [ 'container_id' => $containerId ] );
$container['languages'] = [];
if ( is_array( $langs ) )
foreach ( $langs as $lang )
$container['languages'][ $lang['lang_id'] ] = $lang;
return $container;
}
public function scontainerByLang( $scontainerId, $langId )
{
$cacheKey = "scontainer_details:$scontainerId:$langId";
if ( $scontainer = \Shared\Cache\CacheHandler::fetch( $cacheKey ) )
return $scontainer;
$scontainer = $this->db->get( 'pp_scontainers', '*', [ 'id' => $scontainerId ] );
if ( !$scontainer ) return null;
$langData = $this->db->select( 'pp_scontainers_langs', '*', [
'AND' => [ 'container_id' => $scontainerId, 'lang_id' => $langId ]
] );
$scontainer['languages'] = is_array( $langData ) ? $langData : [];
\Shared\Cache\CacheHandler::store( $cacheKey, $scontainer );
return $scontainer;
}
// -------------------------------------------------------------------------
// Zapis / usuwanie
// -------------------------------------------------------------------------
public function containerSave( $containerId, $title, $text, $status, $showTitle, $src, $html )
{
$languages = $this->db->select( 'pp_langs', '*', [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( !is_array( $languages ) ) $languages = [];
$langCount = count( $languages );
if ( !$containerId )
{
$this->db->insert( 'pp_scontainers', [
'status' => $status == 'on' ? 1 : 0,
'show_title' => $showTitle == 'on' ? 1 : 0,
'src' => $src,
] );
$containerId = $this->db->id();
if ( !$containerId ) return false;
foreach ( $languages as $i => $lang )
{
$this->db->insert( 'pp_scontainers_langs', [
'container_id' => $containerId,
'lang_id' => $lang['id'],
'title' => $langCount > 1 ? $title[ $i ] : $title,
'text' => $langCount > 1 ? $text[ $i ] : $text,
'html' => $langCount > 1 ? $html[ $i ] : $html,
] );
}
}
else
{
$this->db->update( 'pp_scontainers', [
'status' => $status == 'on' ? 1 : 0,
'show_title' => $showTitle == 'on' ? 1 : 0,
'src' => $src,
], [ 'id' => $containerId ] );
$this->db->delete( 'pp_scontainers_langs', [ 'container_id' => $containerId ] );
foreach ( $languages as $i => $lang )
{
$this->db->insert( 'pp_scontainers_langs', [
'container_id' => $containerId,
'lang_id' => $lang['id'],
'title' => $langCount > 1 ? $title[ $i ] : $title,
'text' => $langCount > 1 ? $text[ $i ] : $text,
'html' => $langCount > 1 ? $html[ $i ] : $html,
] );
}
}
\S::delete_cache();
return $containerId;
}
public function containerDelete( $containerId )
{
return $this->db->delete( 'pp_scontainers', [ 'id' => $containerId ] );
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Shared\Email;
class Email
{
public $table = 'pp_newsletter_templates';
public $text = '';
public function load_by_name( string $name )
{
global $mdb;
$result = $mdb->get( $this->table, '*', [ 'name' => $name ] );
if ( is_array( $result ) ) foreach ( $result as $key => $val )
$this->$key = $val;
}
public function email_check( $email )
{
return filter_var( $email, FILTER_VALIDATE_EMAIL );
}
public function send( string $email, string $subject, $replay = '', $file = '' )
{
global $settings;
$base = dirname( dirname( dirname( __DIR__ ) ) );
if ( file_exists( $base . '/libraries/phpmailer/class.phpmailer.php' ) )
require_once $base . '/libraries/phpmailer/class.phpmailer.php';
if ( file_exists( $base . '/libraries/phpmailer/class.smtp.php' ) )
require_once $base . '/libraries/phpmailer/class.smtp.php';
$text = $this->text;
$regex = "-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i";
$text = preg_replace( $regex, "$1https://" . $_SERVER['SERVER_NAME'] . "$2$4", $text );
$regex = "-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i";
$text = preg_replace( $regex, "$1https://" . $_SERVER['SERVER_NAME'] . "$2$4", $text );
if ( $this->email_check( $email ) and $subject )
{
$mail = new \PHPMailer();
$mail->IsSMTP();
$mail->SMTPAuth = true;
$mail->Host = $settings['email_host'];
$mail->Port = $settings['email_port'];
$mail->Username = $settings['email_login'];
$mail->Password = $settings['email_password'];
$mail->CharSet = "UTF-8";
$mail->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
)
);
if ( $this->email_check( $replay ) )
{
$mail->AddReplyTo( $replay, $replay );
$mail->SetFrom( $settings['contact_email'], $settings['contact_email'] );
}
else
{
$mail->AddReplyTo( $settings['contact_email'], $settings['firm_name'] );
$mail->SetFrom( $settings['contact_email'], $settings['firm_name'] );
}
$mail->AddAddress( $email, '' );
$mail->Subject = $subject;
$mail->Body = $text;
if ( is_array( $file ) )
{
foreach ( $file as $file_tmp )
{
if ( file_exists( $file_tmp ) )
$mail->AddAttachment( $file_tmp );
}
}
else
{
if ( file_exists( $file ) )
$mail->AddAttachment( $file );
}
$mail->IsHTML( true );
return $mail->Send();
}
return false;
}
}

View File

@@ -320,22 +320,13 @@ class Helpers
public static function is_token_valid($token) public static function is_token_valid($token)
{ {
if (!empty($_SESSION['tokens'][$token])) return \Shared\Security\CsrfToken::validate($token);
{
unset($_SESSION['tokens'][$token]);
return true;
}
return false;
} }
public static function get_token() public static function get_token()
{ {
$token = sha1(mt_rand()); \Shared\Security\CsrfToken::regenerate();
if (!isset($_SESSION['tokens'])) return \Shared\Security\CsrfToken::getToken();
$_SESSION['tokens'] = [$token => 1];
else
$_SESSION['tokens'][$token] = 1;
return $token;
} }
public static function get_domain($url) public static function get_domain($url)
@@ -1222,60 +1213,8 @@ class Helpers
public static function send_email( $email, $subject, $text, $replay = '', $file = '' ) public static function send_email( $email, $subject, $text, $replay = '', $file = '' )
{ {
global $settings; $emailObj = new \Shared\Email\Email();
$emailObj->text = $text;
if ( file_exists('libraries/phpmailer/class.phpmailer.php') ) require_once 'libraries/phpmailer/class.phpmailer.php'; return $emailObj->send( $email, $subject, $replay, $file );
if ( file_exists('libraries/phpmailer/class.smtp.php') ) require_once 'libraries/phpmailer/class.smtp.php';
if ( file_exists('../libraries/phpmailer/class.phpmailer.php') ) require_once '../libraries/phpmailer/class.phpmailer.php';
if ( file_exists('../libraries/phpmailer/class.smtp.php') ) require_once '../libraries/phpmailer/class.smtp.php';
if ( $email and $subject )
{
$mail = new \PHPMailer();
$mail->IsSMTP();
$mail->SMTPAuth = true;
$mail->Host = $settings['email_host'];
$mail->Port = $settings['email_port'];
$mail->Username = $settings['email_login'];
$mail->Password = $settings['email_password'];
$mail->CharSet = "UTF-8";
$mail->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
)
);
if (self::email_check($replay))
{
$mail->AddReplyTo($replay, $replay);
$mail->SetFrom($settings['contact_email'], $settings['contact_email']);
}
else
{
$mail->AddReplyTo($settings['contact_email'], $settings['firm_name']);
$mail->SetFrom($settings['contact_email'], $settings['firm_name']);
}
$mail->AddAddress($email, '');
$mail->Subject = $subject;
$mail->Body = $text;
if (is_array($file))
{
foreach ($file as $file_tmp)
{
if (file_exists($file_tmp))
$mail->AddAttachment($file_tmp);
}
}
else
{
if (file_exists($file))
$mail->AddAttachment($file);
}
$mail->IsHTML(true);
return $mail->Send();
}
return true;
} }
} }

View File

@@ -0,0 +1,28 @@
<?php
namespace Shared\Security;
class CsrfToken
{
const SESSION_KEY = 'csrf_token';
public static function getToken()
{
if ( empty( $_SESSION[self::SESSION_KEY] ) )
$_SESSION[self::SESSION_KEY] = bin2hex( random_bytes( 32 ) );
return $_SESSION[self::SESSION_KEY];
}
public static function validate( $token )
{
if ( empty( $_SESSION[self::SESSION_KEY] ) || empty( $token ) )
return false;
return hash_equals( $_SESSION[self::SESSION_KEY], $token );
}
public static function regenerate()
{
unset( $_SESSION[self::SESSION_KEY] );
}
}

View File

@@ -1,4 +1,4 @@
<? <?php
namespace admin\factory; namespace admin\factory;
class Authors class Authors
{ {
@@ -6,112 +6,31 @@ class Authors
static public function get_simple_list() static public function get_simple_list()
{ {
global $mdb; global $mdb;
return $mdb -> select( 'pp_authors', '*', [ 'ORDER' => [ 'author' => 'ASC' ] ] ); $repo = new \Domain\Authors\AuthorsRepository($mdb);
return $repo->simpleList();
} }
// usunięcie autora // usunięcie autora
static public function delete_author( $id_author ) static public function delete_author( $id_author )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Authors\AuthorsRepository($mdb);
$result = $mdb -> delete( 'pp_authors', [ 'id' => (int)$id_author ] ); return $repo->authorDelete($id_author);
\S::delete_cache();
return $result;
} }
// zapis autora // zapis autora
static public function save_author( $id_author, $author, $image, $description ) static public function save_author( $id_author, $author, $image, $description )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Authors\AuthorsRepository($mdb);
if ( !$id_author ) return $repo->authorSave($id_author, $author, $image, $description);
{
$mdb -> insert( 'pp_authors', [
'author' => $author,
'image' => $image
] );
$id = $mdb -> id();
if ( $id )
{
$i = 0;
$results = $mdb -> select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) and count( $results ) > 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_authors_langs', [
'id_author' => (int)$id,
'id_lang' => $row['id'],
'description' => $description[ $i ]
] );
$i++;
}
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_authors_langs', [
'id_author' => (int)$id,
'id_lang' => $row['id'],
'description' => $description
] );
}
\S::delete_cache();
return $id;
}
}
else
{
$mdb -> update( 'pp_authors', [
'author' => $author,
'image' => $image
], [
'id' => (int)$id_author
] );
$mdb -> delete( 'pp_authors_langs', [ 'id_author' => (int)$id_author ] );
$i = 0;
$results = $mdb -> select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) and count( $results ) > 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_authors_langs', [
'id_author' => (int)$id_author,
'id_lang' => $row['id'],
'description' => $description[ $i ]
] );
$i++;
}
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_authors_langs', [
'id_author' => (int)$id_author,
'id_lang' => $row['id'],
'description' => $description
] );
}
\S::delete_cache();
return $id_author;
}
return false;
} }
// szczególy autora // szczególy autora
static public function get_single_author( $id_author ) static public function get_single_author( $id_author )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Authors\AuthorsRepository($mdb);
$author = $mdb -> get( 'pp_authors', '*', [ 'id' => (int)$id_author ] ); return $repo->authorDetails($id_author);
$results = $mdb -> select( 'pp_authors_langs', '*', [ 'id_author' => (int)$id_author ] );
if ( is_array( $results ) ) foreach ( $results as $row )
$author['languages'][$row['id_lang']] = $row;
return $author;
} }
} }

View File

@@ -7,123 +7,21 @@ class Banners
public static function banner_delete( $banner_id ) public static function banner_delete( $banner_id )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Banners\BannersRepository($mdb);
$result = $mdb -> delete( 'pp_banners', [ 'id' => (int) $banner_id ] ); return $repo->bannerDelete($banner_id);
\S::delete_cache();
return $result;
} }
public static function banner_save( $banner_id, $name, $status, $date_start, $date_end, $home_page, $src, $url, $html, $text ) public static function banner_save( $banner_id, $name, $status, $date_start, $date_end, $home_page, $src, $url, $html, $text )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Banners\BannersRepository($mdb);
if ( !$banner_id ) return $repo->bannerSave($banner_id, $name, $status, $date_start, $date_end, $home_page, $src, $url, $html, $text);
{
$mdb -> insert( 'pp_banners', [
'name' => $name,
'status' => $status == 'on' ? 1 : 0,
'date_start' => $date_start != '' ? $date_start : null,
'date_end' => $date_end != '' ? $date_end : null,
'home_page' => $home_page == 'on' ? 1 : 0
] );
$id = $mdb -> id();
if ( $id )
{
$i = 0;
$results = $mdb -> select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) and count( $results ) > 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_banners_langs', [
'id_banner' => (int)$id,
'id_lang' => $row['id'],
'src' => $src[ $i ],
'url' => $url[ $i ],
'html' => $html[ $i ],
'text' => $text[ $i ]
] );
$i++;
}
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_banners_langs', [
'id_banner' => (int)$id,
'id_lang' => $row['id'],
'src' => $src,
'url' => $url,
'html' => $html,
'text' => $text
] );
}
\S::delete_cache();
return $id;
}
}
else
{
$mdb -> update( 'pp_banners',
[
'name' => $name,
'status' => $status == 'on' ? 1 : 0,
'date_start' => $date_start != '' ? $date_start : null,
'date_end' => $date_end != '' ? $date_end : null,
'home_page' => $home_page == 'on' ? 1 : 0
], [
'id' => (int) $banner_id
] );
$mdb -> delete( 'pp_banners_langs', [ 'id_banner' => (int)$banner_id ] );
$i = 0;
$results = $mdb -> select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) and count( $results ) > 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_banners_langs', [
'id_banner' => (int)$banner_id,
'id_lang' => $row['id'],
'src' => $src[ $i ],
'url' => $url[ $i ],
'html' => $html[ $i ],
'text' => $text[ $i ]
] );
$i++;
}
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_banners_langs', [
'id_banner' => (int)$banner_id,
'id_lang' => $row['id'],
'src' => $src,
'url' => $url,
'html' => $html,
'text' => $text
] );
}
\S::delete_cache();
return $banner_id;
}
return false;
} }
public static function banner_details( $id_banner ) public static function banner_details( $id_banner )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Banners\BannersRepository($mdb);
$banner = $mdb -> get( 'pp_banners', '*', [ 'id' => (int)$id_banner ] ); return $repo->bannerDetails($id_banner);
$results = $mdb -> select( 'pp_banners_langs', '*', [ 'id_banner' => (int)$id_banner ] );
if ( is_array( $results ) ) foreach ( $results as $row )
$banner['languages'][$row['id_lang']] = $row;
return $banner;
} }
} }
?>

View File

@@ -6,100 +6,49 @@ class Newsletter
public static function emails_import( $emails ) public static function emails_import( $emails )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
$emails = explode( PHP_EOL, $emails ); return $repo->emailsImport($emails);
if ( is_array( $emails ) ) foreach ( $emails as $email )
{
if ( trim( $email ) and !$mdb -> count( 'pp_newsletter', [ 'email' => trim( $email ) ] ) )
$mdb -> insert( 'pp_newsletter', [
'email' => trim( $email ),
'hash' => md5( $email . time() ),
'status' => 1
] );
}
return true;
} }
public static function is_admin_template( $template_id ) public static function is_admin_template( $template_id )
{ {
global $mdb; global $mdb;
return $mdb -> get( 'pp_newsletter_templates', 'is_admin', [ 'id' => (int)$template_id ] ); $repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->isAdminTemplate($template_id);
} }
public static function newsletter_template_delete( $template_id ) public static function newsletter_template_delete( $template_id )
{ {
global $mdb; global $mdb;
return $mdb -> delete( 'pp_newsletter_templates', [ 'id' => (int)$template_id ] ); $repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->templateDelete($template_id);
} }
public static function send( $dates, $template, $only_once ) public static function send( $dates, $template, $only_once )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
$results = $mdb -> select( 'pp_newsletter', 'email', [ 'status' => 1 ] ); return $repo->send($dates, $template, $only_once);
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
{
if ( $template and $only_once )
{
if ( !$mdb -> count( 'pp_newsletter_send', [ 'AND' => [ 'id_template' => $template, 'email' => $row ] ] ) )
$mdb -> insert( 'pp_newsletter_send', [
'email' => $row,
'dates' => $dates,
'id_template' => $template ? $template : null,
'only_once' => ( $only_once == 'on' and $template ) ? 1 : 0
] );
}
else
$mdb -> insert( 'pp_newsletter_send', [
'email' => $row,
'dates' => $dates,
'id_template' => $template ? $template : null,
'only_once' => ( $only_once == 'on' and $template ) ? 1 : 0
] );
}
return true;
} }
public static function email_template_detalis ($id_template) public static function email_template_detalis ($id_template)
{ {
global $mdb; global $mdb;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
$result = $mdb -> get ('pp_newsletter_templates', '*', [ 'id' => (int)$id_template ] ); return $repo->templateDetails($id_template);
return $result;
} }
public static function template_save($id, $name, $text) public static function template_save($id, $name, $text)
{ {
global $mdb; global $mdb;
if ( !$id ) $repo = new \Domain\Newsletter\NewsletterRepository($mdb);
{ return $repo->templateSave($id, $name, $text);
if ( $mdb -> insert( 'pp_newsletter_templates', [
'name' => $name,
'text' => $text
] ) )
{
\S::delete_cache();
return $mdb -> id();
}
}
else
{
$mdb -> update( 'pp_newsletter_templates', [
'name' => $name,
'text' => $text
], [
'id' => (int)$id
] );
\S::delete_cache();
return $id;
}
} }
public static function templates_list() public static function templates_list()
{ {
global $mdb; global $mdb;
return $mdb -> select( 'pp_newsletter_templates', '*', [ 'is_admin' => 0, 'ORDER' => [ 'name' => 'ASC' ] ] ); $repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->templatesList();
} }
} }

View File

@@ -7,115 +7,21 @@ class Scontainers
public static function container_delete( $container_id ) public static function container_delete( $container_id )
{ {
global $mdb; global $mdb;
return $mdb -> delete( 'pp_scontainers', [ 'id' => (int) $container_id ] ); $repo = new \Domain\Scontainers\ScontainersRepository($mdb);
return $repo->containerDelete($container_id);
} }
public static function container_save( $container_id, $title, $text, $status, $show_title, $src, $html ) public static function container_save( $container_id, $title, $text, $status, $show_title, $src, $html )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Scontainers\ScontainersRepository($mdb);
if ( !$container_id ) return $repo->containerSave($container_id, $title, $text, $status, $show_title, $src, $html);
{
$mdb -> insert( 'pp_scontainers',
[
'status' => $status == 'on' ? 1 : 0,
'show_title' => $show_title == 'on' ? 1 : 0,
'src' => $src
] );
$id = $mdb -> id();
if ( $id )
{
$i = 0;
$results = $mdb -> select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) and count( $results ) > 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_scontainers_langs',
[
'container_id' => (int) $id,
'lang_id' => $row['id'],
'title' => $title[$i],
'text' => $text[$i],
'html' => $html[$i]
] );
$i++;
}
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_scontainers_langs', [
'container_id' => (int) $id,
'lang_id' => $row['id'],
'title' => $title,
'text' => $text,
'html' => $html
] );
}
\S::delete_cache();
return $id;
}
}
else
{
$mdb -> update( 'pp_scontainers',
[
'status' => $status == 'on' ? 1 : 0,
'show_title' => $show_title == 'on' ? 1 : 0,
'src' => $src
],
[
'id' => (int) $container_id
] );
$mdb -> delete( 'pp_scontainers_langs',
[ 'container_id' => (int) $container_id ] );
$i = 0;
$results = $mdb -> select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) and count( $results ) > 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_scontainers_langs',
[
'container_id' => (int) $container_id,
'lang_id' => $row['id'],
'title' => $title[$i],
'text' => $text[$i],
'html' => $html[$i]
] );
$i++;
}
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
{
$mdb -> insert( 'pp_scontainers_langs',
[
'container_id' => (int) $container_id,
'lang_id' => $row['id'],
'title' => $title,
'text' => $text,
'html' => $html
] );
}
\S::delete_cache();
return $container_id;
}
} }
public static function container_details( $container_id ) public static function container_details( $container_id )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Scontainers\ScontainersRepository($mdb);
$container = $mdb -> get( 'pp_scontainers', '*', [ 'id' => (int) $container_id ] ); return $repo->containerDetails($container_id);
$results = $mdb -> select( 'pp_scontainers_langs', '*', [ 'container_id' => (int) $container_id ] );
if ( is_array( $results ) ) foreach ( $results as $row )
$container['languages'][$row['lang_id']] = $row;
return $container;
} }
} }

32
autoload/autoloader.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
/**
* Centralny autoloader — hybrydowy (PSR-4 + legacy class.*.php)
* Obsługuje namespace'y: Domain\, Shared\, Admin\, Frontend\, admin\, front\
*/
function __autoload_my_classes( $classname )
{
$base = __DIR__ . '/';
$q = explode( '\\', $classname );
$c = array_pop( $q );
// Savant3 — special case
if ( $c == 'Savant3' )
{
$f = $base . 'Savant3.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
}
$path = implode( '/', $q );
// 1. Legacy: class.ClassName.php
$f = $base . $path . '/class.' . $c . '.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = $base . $path . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );

View File

@@ -1,4 +1,4 @@
<? <?php
namespace front\factory; namespace front\factory;
class Authors class Authors
{ {
@@ -6,17 +6,7 @@ class Authors
static public function get_single_author( $id_author ) static public function get_single_author( $id_author )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Authors\AuthorsRepository($mdb);
if ( !$author = \Cache::fetch( "get_single_author:$id_author" ) ) return $repo->authorByLang($id_author);
{
$author = $mdb -> get( 'pp_authors', '*', [ 'id' => (int)$id_author ] );
$results = $mdb -> select( 'pp_authors_langs', '*', [ 'id_author' => (int)$id_author ] );
if ( is_array( $results ) ) foreach ( $results as $row )
$author['languages'][$row['id_lang']] = $row;
\Cache::store( "get_single_author:$id_author", $author );
}
return $author;
} }
} }

View File

@@ -6,58 +6,14 @@ class Banners
public static function banners() public static function banners()
{ {
global $mdb, $lang; global $mdb, $lang;
$repo = new \Domain\Banners\BannersRepository($mdb);
if ( !$banners = \Cache::fetch( 'banners' ) ) return $repo->activeBanners($lang[0]);
{
$results = $mdb -> query( 'SELECT '
. 'id, name '
. 'FROM '
. 'pp_banners '
. 'WHERE '
. 'status = 1 '
. 'AND '
. '( date_start <= \'' . date( 'Y-m-d' ) . '\' OR date_start IS NULL ) '
. 'AND '
. '( date_end >= \'' . date( 'Y-m-d' ) . '\' OR date_end IS NULL ) '
. 'AND '
. 'home_page = 0' ) -> fetchAll();
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
{
$row['languages'] = $mdb -> get( 'pp_banners_langs', '*', [ 'AND' => [ 'id_banner' => (int)$row['id'], 'id_lang' => $lang[0] ] ] );
$banners[] = $row;
}
\Cache::store( 'banners', $banners );
}
return $banners;
} }
public static function main_banner() public static function main_banner()
{ {
global $mdb, $lang; global $mdb, $lang;
$repo = new \Domain\Banners\BannersRepository($mdb);
if ( !$banner = \Cache::fetch( "main_banner:" . $lang[0] ) ) return $repo->mainBanner($lang[0]);
{
$banner = $mdb -> query( 'SELECT '
. '* '
. 'FROM '
. 'pp_banners '
. 'WHERE '
. 'status = 1 '
. 'AND '
. '( date_start <= \'' . date( 'Y-m-d' ) . '\' OR date_start IS NULL ) '
. 'AND '
. '( date_end >= \'' . date( 'Y-m-d' ) . '\' OR date_end IS NULL ) '
. 'AND '
. 'home_page = 1 '
. 'ORDER BY '
. 'date_end ASC '
. 'LIMIT 1' ) -> fetchAll();
$banner = $banner[0];
if ( $banner )
$banner['languages'] = $mdb -> get( 'pp_banners_langs', '*', [ 'AND' => [ 'id_banner' => (int)$banner['id'], 'id_lang' => $lang[0] ] ] );
\Cache::store( "main_banner:" . $lang[0], $banner );
}
return $banner;
} }
} }

View File

@@ -6,113 +6,49 @@ class Newsletter
public static function newsletter_unsubscribe( $hash ) public static function newsletter_unsubscribe( $hash )
{ {
global $mdb; global $mdb;
return $mdb -> update( 'pp_newsletter', [ 'status' => 0 ], [ 'hash' => $hash ] ); $repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->unsubscribe($hash);
} }
public static function newsletter_confirm( $hash ) public static function newsletter_confirm( $hash )
{ {
global $mdb; global $mdb;
if ( !$id = $mdb -> get( 'pp_newsletter', 'id', [ 'AND' => [ 'hash' => $hash, 'status' => 0 ] ] ) ) $repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return false; return $repo->confirm($hash);
else
$mdb -> update( 'pp_newsletter', [ 'status' => 1 ], [ 'id' => $id ] );
return true;
} }
public static function newsletter_send( $limit = 5 ) public static function newsletter_send( $limit = 5 )
{ {
global $mdb, $settings, $lang; global $mdb, $settings, $lang;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
$results = $mdb -> query( 'SELECT * FROM pp_newsletter_send WHERE mailed = 0 ORDER BY id ASC LIMIT ' . $limit ) -> fetchAll(); return $repo->newsletterSend($limit, $settings, $lang);
if ( is_array( $results ) and !empty( $results ) )
{
foreach ( $results as $row )
{
$dates = explode( ' - ', $row['dates'] );
$text = \admin\view\Newsletter::preview(
\admin\factory\Articles::articles_by_date_add( $dates[0], $dates[1] ),
\admin\factory\Settings::settings_details(),
\admin\factory\Newsletter::email_template_detalis($row['id_template'])
);
if ( $settings['ssl'] ) $base = 'https'; else $base = 'http';
$link = $base . "://" . $_SERVER['SERVER_NAME'] . '/newsletter/unsubscribe/hash=' . \front\factory\Newsletter::get_hash( $row['email'] );
$text = str_replace( '[WYPISZ_SIE]', $link, $text );
$regex = "-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|http(|s)://).)*)(['\"][^>]*>)-i";
$text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text );
$regex = "-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|http(|s)://).)*)(['\"][^>]*>)-i";
$text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text );
\S::send_email( $row['email'], 'Newsletter ze strony: ' . $_SERVER['SERVER_NAME'], $text );
if ( $row['only_once'] )
$mdb -> update( 'pp_newsletter_send', [ 'mailed' => 1 ], [ 'id' => $row['id'] ] );
else
$mdb -> delete( 'pp_newsletter_send', [ 'id' => $row['id'] ] );
}
return true;
}
return false;
} }
public static function get_hash( $email ) public static function get_hash( $email )
{ {
global $mdb; global $mdb;
return $mdb -> get( 'pp_newsletter', 'hash', [ 'email' => $email ] ); $repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->getHash($email);
} }
public static function newsletter_signin( $email ) public static function newsletter_signin( $email )
{ {
global $mdb, $lang, $settings; global $mdb, $lang, $settings;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
if ( !\S::email_check( $email ) ) return $repo->signin($email, $settings, $lang);
return false;
if ( !$mdb -> get( 'pp_newsletter', 'id', [ 'email' => $email ] ) )
{
$hash = md5( time() . $email );
$text = $settings['newsletter_header'];
$text .= \front\factory\Newsletter::get_template( '#potwierdzenie-zapisu-do-newslettera' );
$text .= $settings['newsletter_footer_1'];
$settings['ssl'] ? $base = 'https' : $base = 'http';
$regex = "-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|http://).)*)(['\"][^>]*>)-i";
$text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text );
$regex = "-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|http://).)*)(['\"][^>]*>)-i";
$text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text );
$link = '/newsletter/confirm/hash=' . $hash;
$text = str_replace( '[LINK]', $link, $text );
$send = \S::send_email( $email, $lang['potwierdz-zapisanie-sie-do-newslettera'], $text );
$mdb -> insert( 'pp_newsletter', [ 'email' => $email, 'hash' => $hash, 'status' => 0 ] );
return true;
}
return false;
} }
public static function get_template( $template_name ) public static function get_template( $template_name )
{ {
global $mdb; global $mdb;
return $mdb -> get( 'pp_newsletter_templates', 'text', [ 'name' => $template_name ] ); $repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->getTemplate($template_name);
} }
public static function newsletter_signout( $email ) public static function newsletter_signout( $email )
{ {
global $mdb; global $mdb;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
if ( $mdb -> get( 'pp_newsletter', 'id', [ 'email' => $email ] ) ) return $repo->signout($email);
return $mdb -> delete( 'pp_newsletter', [ 'email' => $email ] );
return false;
} }
} }

View File

@@ -6,18 +6,7 @@ class Scontainers
public static function scontainer_details( $scontainer_id ) public static function scontainer_details( $scontainer_id )
{ {
global $mdb, $lang; global $mdb, $lang;
$repo = new \Domain\Scontainers\ScontainersRepository($mdb);
if ( !$scontainer = \Cache::fetch( "scontainer_details:$scontainer_id:" . $lang[0] ) ) return $repo->scontainerByLang($scontainer_id, $lang[0]);
{
$scontainer = $mdb -> get( 'pp_scontainers', '*', [ 'id' => (int)$scontainer_id ] );
$results = $mdb -> select( 'pp_scontainers_langs', '*', [ 'AND' => [ 'container_id' => (int)$scontainer_id, 'lang_id' => $lang[0] ] ] );
if ( is_array( $results ) ) foreach ( $results as $row )
$scontainer['languages'] = $row;
\Cache::store( "scontainer_details:$scontainer_id:" . $lang[0], $scontainer );
}
return $scontainer;
} }
} }

View File

@@ -2,6 +2,14 @@
"require-dev": { "require-dev": {
"phpunit/phpunit": "^10.5" "phpunit/phpunit": "^10.5"
}, },
"autoload": {
"psr-4": {
"Domain\\": "autoload/Domain/",
"Shared\\": "autoload/Shared/",
"Admin\\": "autoload/Admin/",
"Frontend\\": "autoload/Frontend/"
}
},
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Tests\\": "tests/" "Tests\\": "tests/"

View File

@@ -1,19 +1,6 @@
<?php <?php
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ); error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING );
function __autoload_my_classes( $classname ) require_once __DIR__ . '/autoload/autoloader.php';
{
$q = explode( '\\' , $classname );
$c = array_pop( $q );
// 1. Legacy: class.ClassName.php
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = 'autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );
date_default_timezone_set( 'Europe/Warsaw' ); date_default_timezone_set( 'Europe/Warsaw' );
require_once 'config.php'; require_once 'config.php';

View File

@@ -12,10 +12,9 @@
| `cron.php` | Zadania cykliczne (newsletter) | | `cron.php` | Zadania cykliczne (newsletter) |
| `download.php` | Chronione pobieranie plików | | `download.php` | Chronione pobieranie plików |
Każdy punkt wejścia ładuje dwa autoloadery (PSR-4 + legacy): Każdy punkt wejścia ładuje centralny autoloader (hybrydowy PSR-4 + legacy):
```php ```php
spl_autoload_register(function($class) { /* PSR-4: src/ → autoload/ */ }); require_once __DIR__ . '/autoload/autoloader.php';
spl_autoload_register(function($class) { /* legacy: class.{Name}.php */ });
``` ```
--- ---
@@ -40,36 +39,40 @@ Projekt migruje stopniowo do architektury DDD. Stare klasy stają się
cienkimi wrapperami delegującymi do nowych klas w `Shared\` i `Domain\`. cienkimi wrapperami delegującymi do nowych klas w `Shared\` i `Domain\`.
### Faza 0 ✓ — Autoloader PSR-4 ### Faza 0 ✓ — Autoloader PSR-4
Dodany do wszystkich 6 punktów wejścia. Mapowanie: namespace → `autoload/`. Centralny autoloader w `autoload/autoloader.php` (hybrydowy: PSR-4 + legacy class.*.php).
Wszystkie 7 punktów wejścia używają jednego pliku. composer.json z PSR-4 mapowaniem:
Domain\, Shared\, Admin\, Frontend\ → autoload/.
### Faza 1 ✓ — Shared utilities (`autoload/Shared/`) ### Faza 1 ✓ — Shared utilities (`autoload/Shared/`)
``` ```
autoload/Shared/ autoload/Shared/
├── Cache/CacheHandler.php ← \Shared\Cache\CacheHandler ├── Cache/CacheHandler.php ← \Shared\Cache\CacheHandler
├── Email/ ← \Shared\Email\* ├── Email/Email.php ← \Shared\Email\Email
├── Helpers/Helpers.php ← \Shared\Helpers\Helpers ├── Helpers/Helpers.php ← \Shared\Helpers\Helpers
├── Html/Html.php ← \Shared\Html\Html ├── Html/Html.php ← \Shared\Html\Html
├── Image/ImageManipulator.php ← \Shared\Image\ImageManipulator ├── Image/ImageManipulator.php ← \Shared\Image\ImageManipulator
├── Security/CsrfToken.php ← \Shared\Security\CsrfToken
└── Tpl/Tpl.php ← \Shared\Tpl\Tpl └── Tpl/Tpl.php ← \Shared\Tpl\Tpl
``` ```
Stare klasy (`class.S.php`, `class.Cache.php`, itd.) są teraz cienkimi Stare klasy (`class.S.php`, `class.Cache.php`, itd.) są teraz cienkimi
wrapperami — zachowana pełna kompatybilność wsteczna. wrapperami — zachowana pełna kompatybilność wsteczna.
Helpers::send_email() → Email, Helpers::get_token()/is_token_valid() → CsrfToken.
### Faza 2 (w toku) - Domain Repositories (`autoload/Domain/`) ### Faza 2 (w toku) - Domain Repositories (`autoload/Domain/`)
``` ```
autoload/Domain/ autoload/Domain/
|- Languages/LanguagesRepository.php <- \Domain\Languages\LanguagesRepository OK ├── Languages/LanguagesRepository.php \Domain\Languages\LanguagesRepository
|- Settings/SettingsRepository.php <- \Domain\Settings\SettingsRepository OK ├── Settings/SettingsRepository.php \Domain\Settings\SettingsRepository
|- User/UserRepository.php <- \Domain\User\UserRepository OK ├── User/UserRepository.php \Domain\User\UserRepository
|- Pages/PagesRepository.php <- \Domain\Pages\PagesRepository OK ├── Pages/PagesRepository.php \Domain\Pages\PagesRepository
|- Layouts/LayoutsRepository.php <- \Domain\Layouts\LayoutsRepository OK ├── Layouts/LayoutsRepository.php \Domain\Layouts\LayoutsRepository
`- Articles/ArticlesRepository.php <- \Domain\Articles\ArticlesRepository OK (w toku) └── Articles/ArticlesRepository.php \Domain\Articles\ArticlesRepository
``` ```
Nastepne: `Domain\Banners`, `Domain\Authors`, `Domain\Newsletter`, ... Następne: `Domain\Scontainers`, `Domain\Banners`, `Domain\Authors`, `Domain\Newsletter`, ...
--- ---
## Katalogi ## Katalogi

View File

@@ -1,15 +1,6 @@
<?php <?php
error_reporting( E_ALL & ~E_NOTICE & ~E_WARNING ); error_reporting( E_ALL & ~E_NOTICE & ~E_WARNING );
function __autoload_my_classes( $classname ) require_once __DIR__ . '/autoload/autoloader.php';
{
$q = explode( '\\' , $classname );
$c = array_pop( $q );
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) )
require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );
date_default_timezone_set( 'Europe/Warsaw' ); date_default_timezone_set( 'Europe/Warsaw' );
require_once 'config.php'; require_once 'config.php';

View File

@@ -1,19 +1,6 @@
<?php <?php
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED ); error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
function __autoload_my_classes( $classname ) require_once __DIR__ . '/autoload/autoloader.php';
{
$q = explode( '\\', $classname );
$c = array_pop( $q );
// 1. Legacy: class.ClassName.php
$f = 'autoload/' . implode( '/', $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = 'autoload/' . implode( '/', $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );
date_default_timezone_set( 'Europe/Warsaw' ); date_default_timezone_set( 'Europe/Warsaw' );
require_once 'config.php'; require_once 'config.php';

BIN
updates/1.60/ver_1.693.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,28 @@
{
"changelog": "REF - migracja admin Pages/Layouts/Articles do Domain repositories",
"version": "1.693",
"files": {
"added": [
"autoload/Domain/Articles/ArticlesRepository.php",
"autoload/Domain/Layouts/LayoutsRepository.php",
"autoload/Domain/Pages/PagesRepository.php",
"composer.phar"
],
"deleted": [
],
"modified": [
"autoload/admin/factory/class.Articles.php",
"autoload/admin/factory/class.Layouts.php",
"autoload/admin/factory/class.Pages.php"
]
},
"checksum_zip": "sha256:3994561d9f3df8ed887f53c903b2a26ae6d17e6b10d98c7cb5cdc59132cef7b5",
"sql": [
],
"date": "2026-03-04",
"directories_deleted": [
]
}

BIN
updates/1.60/ver_1.694.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,33 @@
{
"changelog": "NEW - centralny autoloader, Shared\\Email, Shared\\Security\\CsrfToken",
"version": "1.694",
"files": {
"added": [
".mcp.json",
"autoload/Shared/Email/Email.php",
"autoload/Shared/Security/CsrfToken.php",
"autoload/autoloader.php"
],
"deleted": [
],
"modified": [
"admin/ajax.php",
"admin/index.php",
"ajax.php",
"api.php",
"autoload/Shared/Helpers/Helpers.php",
"cron.php",
"download.php",
"index.php"
]
},
"checksum_zip": "sha256:a21dc4a768bc7c9e71b8a319ff0e6a16bdd894330a5145d0278b220a3ccc4027",
"sql": [
],
"date": "2026-04-04",
"directories_deleted": [
]
}

BIN
updates/1.60/ver_1.695.zip Normal file

Binary file not shown.

View File

@@ -11,9 +11,9 @@ $mdb = new medoo( [
'charset' => 'utf8' 'charset' => 'utf8'
] ); ] );
$current_ver = 1692; // aktualizowane automatycznie przez build-update.ps1 $current_ver = 1695; // aktualizowane automatycznie przez build-update.ps1
// 1. Skan filesystem — lista istniejÄ…cych ZIPĂłw // 1. Skan filesystem — lista istniejÄ‚ââ¬ĹľÄ˘â‚¬Ă¦cych ZIPÄ„ââ¬ĹˇĂ„ąââ¬Ĺˇw
$versions = []; $versions = [];
for ( $i = 1; $i <= $current_ver; $i++ ) for ( $i = 1; $i <= $current_ver; $i++ )
{ {
@@ -34,7 +34,7 @@ $license = $mdb->get( 'pp_update_licenses', '*', [ 'key' => ( $_GET['key'] ?? ''
if ( !$license ) if ( !$license )
die(); die();
// 3. SprawdĹş waĹĽnoĹć daty // 3. SprawdĹş waĄąĄĂĹĄnoĄąĢ€şĂââ¬ĹľÄ˘â‚¬Ă‡ daty
if ( $license['valid_to_date'] && $license['valid_to_date'] < date( 'Y-m-d' ) ) if ( $license['valid_to_date'] && $license['valid_to_date'] < date( 'Y-m-d' ) )
die(); die();
@@ -53,11 +53,11 @@ foreach ( $versions as $ver )
} }
} }
// 5. Filtruj wersje wg kanału (beta widzi beta+stable, reszta tylko stable) // 5. Filtruj wersje wg kanaĹ‚u (beta widzi beta+stable, reszta tylko stable)
$channels = $license['beta'] ? [ 'beta', 'stable' ] : [ 'stable' ]; $channels = $license['beta'] ? [ 'beta', 'stable' ] : [ 'stable' ];
$allowed = array_flip( $mdb->select( 'pp_update_versions', 'version', [ 'channel' => $channels ] ) ?: [] ); $allowed = array_flip( $mdb->select( 'pp_update_versions', 'version', [ 'channel' => $channels ] ) ?: [] );
// 6. Wypisz dostępne wersje // 6. Wypisz dostÄ™pne wersje
$valid_to_version = $license['valid_to_version']; $valid_to_version = $license['valid_to_version'];
foreach ( $versions as $ver ) foreach ( $versions as $ver )
{ {