12 Commits

Author SHA1 Message Date
3b1084964a build(update): paczka 1.697 — Admin\ base infrastructure (Phase 6)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:33:19 +02:00
a3caeb9a9a feat(06-admin-base): Admin\ base infrastructure — Form Edit System + Support layer (Phase 6)
Phase 6 zamknięta po 2 planach. Pełny fundament dla Phase 7-13 (migracja
17 admin controllers do Admin\ namespace).

06-01 (Forms infrastructure):
- Admin\ViewModels\Forms\* — 5 ViewModeli (687 L)
- Admin\Validation\FormValidator (196 L)
- composer.json: php >=7.4, PSR-4 paths cross-platform safe
  (Admin\ → autoload/admin/, Frontend\ → autoload/front/)

06-02 (Support layer):
- Admin\Support\TableListRequestFactory (99 L) — parser list z $_GET
- Admin\Support\Forms\FormRequestHandler (159 L) — POST + CSRF + walidacja + persist
- Admin\Support\Forms\FormFieldRenderer (494 L) — renderer HTML pól

Decyzje:
- Brak BaseController — Phase 7+ kontrolery jako POJOs z DI (jak shopPRO)
- PSR-4 filename fix: TableListRequestFactory.php (bez shopPRO 'class.' prefix)
- PascalCase namespace (Admin\Support) na lowercase folder admin/
  ze względu na Windows fs case-insensitivity vs legacy admin/controls/

Pliki: 8 nowych klas, 1635 L kodu PHP 7.4-kompatybilnego, zero regresji.
Smoke test: walidacja e-maila zwraca PL komunikat, factory parsuje
?page=&per_page=&sort=&filter=, Domain/Shared nadal ładują się.

PHPUnit: 37/37 OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:32:26 +02:00
72cb5b8d1d update 2026-04-26 01:44:36 +02:00
cbc2462ea4 build(update): paczka 1.696 — Domain layer kompletny (Phase 5)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 00:58:59 +02:00
f7c7c0bb88 feat(05-domain-seoadditional-cron-releases): Domain layer kompletny — SeoAdditional + Cron + Releases
Phase 5 complete:
- Domain\SeoAdditional\SeoAdditionalRepository (elementDelete, elementSave, elementDetails)
- Domain\Cron\CronRepository (3 pub + 12 private helper methods)
- Domain\Releases\ReleasesRepository (9 metod: wersje, licencje, discover)
- Domain\Releases\UpdateRepository (auto-update, konstruktor($db, $settings))
- 4 legacy factory wrappers zaktualizowane do wrapper delegation

Domain layer: 13/13 repozytoriów kompletnych.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 00:57:13 +02:00
bf4b7c6429 docs(codebase): mapa kodu wygenerowana przez /paul:map-codebase
7 dokumentów w .paul/codebase/ — overview, stack, architecture,
conventions, testing, integrations, concerns (CRITICAL→LOW).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 00:46:01 +02:00
cfd2e5fb57 update 2026-04-04 18:29:10 +02:00
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
226 changed files with 9069 additions and 1369 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

@@ -0,0 +1 @@
{"sessionId":"67436f78-3aa8-4e1d-b1bd-475562c4b01e","pid":21052,"acquiredAt":1777156013131}

View File

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

View File

@@ -7,17 +7,19 @@ Autorski system CMS z panelem administracyjnym (17 modułów admin, 13 modułów
Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi.
## Already Completed
- Domain (6 repos): Articles, Languages, Layouts, Pages, Settings, User
- Shared (5 modules): Cache, Helpers, Html, Image, Tpl
- Form Edit System: FormEditViewModel, multi-tab, validation, persistence
- Domain (13 repos): Articles, Languages, Layouts, Pages, Settings, User, Scontainers, Banners, Authors, Newsletter, SeoAdditional, Cron, Releases+Update
- Shared (7 modules): Cache, Helpers, Html, Image, Tpl, Email, Security
- **Admin\ base infrastructure (Phase 6, 2026-04-30):** 5 Form ViewModels + FormValidator + TableListRequestFactory + FormRequestHandler + FormFieldRenderer (1635 L total) — pełny fundament dla migracji 17 admin controllers w Phase 7-13
- 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)
- Wszystkie Domain repositories — Phase 5 complete
- Shared\Email + Shared\Security CsrfToken — Phase 2 complete (HMAC-SHA256 → Phase 17)
- ✓ Admin\ base infrastructure — Phase 6 complete (controllers Phase 7-13)
- Admin\ namespace z DI dla wszystkich 17 modułów
- Frontend\ namespace dla wszystkich front modułów
- Bezpieczne cookies (HMAC-SHA256 zamiast hash w JSON)
@@ -45,3 +47,4 @@ Autorski system CMS umożliwiający zarządzanie treściami i stronami interneto
---
*Created: 2026-04-04*
*Last updated: 2026-04-30 after Phase 6*

View File

@@ -6,7 +6,7 @@ Pełna refaktoryzacja cmsPRO do architektury DDD wzorowanej na shopPRO. Wzorzec:
## Current Milestone
**v0.1 Refaktoryzacja** (v0.1.0)
Status: In progress
Phases: 2 of 19 complete
Phases: 6 of 19 complete
## Already Completed (before PAUL)
- **Domain (6 repos):** Articles, Languages, Layouts, Pages, Settings, User
@@ -20,10 +20,11 @@ Phases: 2 of 19 complete
|-------|------|-------|--------|-----------|
| 1 | Infrastructure & Autoloader | 1 | Complete | 2026-04-04 |
| 2 | Shared: Email + Security | 1 | Complete | 2026-04-04 |
| 3 | Domain: Scontainers + Banners | 1 | Not started | - |
| 4 | Domain: Authors + Newsletter | 1 | Not started | - |
| 5 | Domain: SeoAdditional + Cron + Releases | 1 | Not started | - |
| 6 | Admin: Base Infrastructure | 1 | Not started | - |
| 3 | Domain: Scontainers + Banners | 1 | Complete | 2026-04-04 |
| 4 | Domain: Authors + Newsletter | 1 | Complete | 2026-04-04 |
| 04h | **HOTFIX:** HTTPS update endpoint (out-of-roadmap) | 1 | Complete | 2026-04-26 |
| 5 | Domain: SeoAdditional + Cron + Releases | 1 | Complete | 2026-04-26 |
| 6 | Admin: Base Infrastructure | 2 | Complete | 2026-04-30 |
| 7 | Admin: Articles + ArticlesArchive | 1 | Not started | - |
| 8 | Admin: Pages + Layouts | 1 | Not started | - |
| 9 | Admin: Languages + Settings | 1 | Not started | - |
@@ -111,18 +112,20 @@ Phases: 2 of 19 complete
### 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)
**Goal:** Bazowe klasy Admin\ — Form Edit System + helpers do request handling i renderowania (wzór shopPRO). **Bez BaseController** — shopPRO używa POJOs z DI, kontrolery Phase 7+ będą zwykłymi klasami z Domain repos w konstruktorze.
**Depends on:** Phase 1 (autoloader)
**Research:** Done (06-01 SUMMARY)
**Scope:**
- Admin\Base\BaseController (lub abstrakcyjna klasa bazowa)
- Admin\Support\TableListRequestFactory
- Admin\Support\FormValidator
- Integracja z istniejącym FormEditViewModel
- Admin\ViewModels\Forms\* (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction) — done in 06-01
- Admin\Validation\FormValidator — done in 06-01
- Admin\Support\TableListRequestFactory — 06-02
- Admin\Support\Forms\FormRequestHandler — 06-02
- Admin\Support\Forms\FormFieldRenderer — 06-02
**Plans:**
- [ ] 06-01: Admin base infrastructure
- [x] 06-01: Forms infrastructure (Form ViewModels + FormValidator) — 2026-04-30
- [x] 06-02: Support layer (TableListRequestFactory + FormRequestHandler + FormFieldRenderer) — 2026-04-30
### Phase 7: Admin: Articles + ArticlesArchive

View File

@@ -2,35 +2,36 @@
## Project Reference
See: .paul/PROJECT.md (updated 2026-04-04)
See: .paul/PROJECT.md (updated 2026-04-26)
**Core value:** Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi.
**Current focus:** Phase 2 complete — ready for Phase 3
**Current focus:** Phase 6 — Plan 06-01 (Forms infrastructure) created, awaiting approval
## Current Position
Milestone: v0.1 Refaktoryzacja
Phase: 2 of 19 (Shared: Email + Security) — Complete
Plan: 02-01 complete
Status: Loop closed, ready for next PLAN
Last activity: 2026-04-04Phase 2 complete, UNIFY done
Phase: 6 of 19 (Admin: Base Infrastructure) — Complete
Plan: All plans complete (06-01, 06-02)
Status: Phase 6 closed ready to plan Phase 7
Last activity: 2026-04-30 — UNIFY 06-02 closed Phase 6 (Admin Base Infrastructure shipped)
Progress:
- Milestone: [▓░░░░░░░░░] 10%
- Milestone: [▓▓▓▓░░░░░░] 32% (6 of 19 phases)
- Phase 6: [▓▓▓▓▓▓▓▓▓▓] 100% complete
## Loop Position
Current loop state:
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [Loop complete - ready for next PLAN]
✓ ✓ ✓ [Phase 6 complete ready for Phase 7 PLAN]
```
## Performance Metrics
**Velocity:**
- Total plans completed: 2
- Total execution time: ~18min
- Total plans completed: 7
- Total execution time: ~39min
**By Phase:**
@@ -38,14 +39,30 @@ PLAN ──▶ APPLY ──▶ UNIFY
|-------|-------|------------|----------|
| 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 |
| 04h-hotfix-https-updates | 1/1 | ~90min | ~90min |
| 05-domain-seoadditional-cron-releases | 1/1 | ~5min | ~5min |
| 06-admin-base | 2/2 | ~12min | ~6min |
## Accumulated Context
### Decisions
- 2026-04-30: Phase 6/06-02 — Brak `BaseController`. Phase 7+ kontrolery będą POJOs z DI w konstruktorze (jak shopPRO BannerController). Powtórzony kod (np. `requirePrivilege()`) zostanie wyabstrahowany dopiero gdy zaobserwujemy realny pattern w 2-3 kontrolerach.
- 2026-04-30: Phase 6/06-02 — Filename `TableListRequestFactory.php` (PSR-4 czyste) zamiast shopPRO `class.TableListRequestFactory.php` (lamie PSR-4). Naprawia bug shopPRO.
- 2026-04-30: Phase 6/06-01 — composer.json PSR-4 mapuje `Admin\``autoload/admin/` (lowercase folder), bo Windows fs case-insensitive zlał `admin/` legacy z planowanym `Admin/`. Cross-platform safe na Linux. Bonus: `Frontend\``autoload/front/` (preventive dla Phase 14+).
- 2026-04-30: Phase 6/06-01 — Form VMs i FormValidator skopiowane 1:1 z shopPRO; jedyne zmiany to namespace `admin\``Admin\` (PascalCase per decyzja użytkownika). PROJECT.md mylnie deklarował, że Form Edit System już istnieje — w cmsPRO go nie było.
- 2026-04-26: Phase 5 — UpdateRepository przyjmuje ($db, $settings) w konstruktorze — settings potrzebny do update_key i wersji.
- 2026-04-26: Phase 5 — Cron helper methods (get_site_meta_*) stały się private w CronRepository — były wywoływane tylko wewnętrznie.
- 2026-04-26: Phase 5 — class.Cron.php zachowuje brak namespace (klasa globalna) — cron.php używa bezpośrednio.
- 2026-04-26: Hotfix 04h — full-patch wszystkich 121 paczek (zamiast minimal-patch). Powód: paczki nadpisują class.S.php w różnych wersjach, częściowy patch ryzykuje regresję podczas chain-update.
- 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
- Newsletter: globals ($settings, $lang) passed as explicit params to repo methods
### Deferred Issues
None.
@@ -55,10 +72,10 @@ None.
## Session Continuity
Last session: 2026-04-04
Stopped at: Phase 2 complete, loop closed
Next action: Run /paul:plan for Phase 3 (Domain: Scontainers + Banners)
Resume file: .paul/phases/02-shared-email-security/02-01-SUMMARY.md
Last session: 2026-04-30
Stopped at: Phase 6 complete (loop closed)
Next action: /paul:plan dla Phase 7 (Admin: Articles + ArticlesArchive)
Resume file: .paul/ROADMAP.md
---
*STATE.md — Updated after every significant action*

View File

@@ -0,0 +1,52 @@
# 2026-04-26
## Co zrobiono
- [Phase 04h, Plan 01] Hotfix HTTPS update endpoint: naprawa zablokowanego mechanizmu aktualizacji we wszystkich instancjach cmsPRO
- Patch http://www.cmspro.project-dc.pl -> https:// w kodzie zrodlowym (Helpers.php, factory/Update.php) i instancji testowej
- Audit 542 paczek aktualizacji - wykryto 121 z buggy http:// URL
- Patch 121 paczek (autoload/class.S.php / Helpers.php / factory/Update.php) z http -> https
- Patch cmsPro.zip (base install) z http -> https
- Wstrzykniecie kotwicy fixa do ver_1.519.zip (oryginalnie tylko class.Articles.php; dodano patched class.S.php + factory/Update.php) - SHA256: 14e5754c75884fcc...
- Odkrycie bug-a #2 podczas UAT: klucz licencji z `#` lamie URL przez fragment delimiter -> serwer dostaje pusty klucz -> brak nowych wersji
- Patch urlencode($settings['update_key']) w kodzie zrodlowym + 64 paczkach + kotwicy
- Generacja upload-checklist.md (124 pliki: cmsPro.zip + 121 ZIP + 2 manifest)
- Auto-deploy ftp-kr.json przeniosl pliki na serwer cmspro.project-dc.pl
- UAT confirmation: instancja testowa widzi i instaluje aktualizacje > 1.519
- Cleanup 1085 plikow .bak / .preurlencode.bak / .preanchor.bak (lokalnie + FTP) przez .NET FtpWebRequest
- [Phase 05, Plan 01] Domain layer kompletny: SeoAdditional + Cron + Releases + Update repositories
- Utworzono Domain\SeoAdditional\SeoAdditionalRepository (elementDelete, elementSave, elementDetails)
- Utworzono Domain\Cron\CronRepository (3 pub + 12 private helper methods, crawling stron)
- Utworzono Domain\Releases\ReleasesRepository (9 metod: wersje, licencje, discover)
- Utworzono Domain\Releases\UpdateRepository (auto-update mechanizm, przyjmuje $db + $settings)
- Zaktualizowano 4 legacy wrappery: class.SeoAdditional, class.Cron, class.Releases, class.Update
## Zmienione pliki
- `autoload/Shared/Helpers/Helpers.php`
- `autoload/admin/factory/class.Update.php`
- `updates/cmsPro.zip`
- `updates/**/ver_*.zip` (121 paczek)
- `updates/**/ver_*_manifest.json` (2 manifesty)
- `.paul/phases/04h-hotfix-https-updates/04h-01-PLAN.md`
- `.paul/phases/04h-hotfix-https-updates/04h-01-SUMMARY.md`
- `.paul/phases/04h-hotfix-https-updates/audit-report.md`
- `.paul/phases/04h-hotfix-https-updates/patch-log.md`
- `.paul/phases/04h-hotfix-https-updates/patch-urlencode-log.md`
- `.paul/phases/04h-hotfix-https-updates/upload-checklist.md`
- `.paul/phases/04h-hotfix-https-updates/scripts/audit-packages.ps1`
- `.paul/phases/04h-hotfix-https-updates/scripts/patch-packages.ps1`
- `.paul/phases/04h-hotfix-https-updates/scripts/patch-urlencode.ps1`
- `.paul/phases/04h-hotfix-https-updates/scripts/inject-anchor-1519.ps1`
- `.paul/phases/04h-hotfix-https-updates/scripts/cleanup-baks.ps1`
- `autoload/Domain/SeoAdditional/SeoAdditionalRepository.php`
- `autoload/Domain/Cron/CronRepository.php`
- `autoload/Domain/Releases/ReleasesRepository.php`
- `autoload/Domain/Releases/UpdateRepository.php`
- `autoload/admin/factory/class.SeoAdditional.php`
- `autoload/class.Cron.php`
- `autoload/admin/factory/class.Releases.php`
- `autoload/admin/factory/class.Update.php`
- `.paul/STATE.md`
- `.paul/ROADMAP.md`

View File

@@ -0,0 +1,35 @@
# 2026-04-30
## Co zrobiono
- [Phase 6, Plan 06-01] Forms infrastructure — przeniesienie warstwy Form Edit System z shopPRO do cmsPRO pod namespace `Admin\` (PascalCase)
- Skopiowano 5 ViewModeli (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction) do `Admin\ViewModels\Forms\`
- Skopiowano FormValidator do `Admin\Validation\`
- Dodano `"php": ">=7.4"` do composer.json
- Naprawiono PSR-4 mapowanie: `Admin\``autoload/admin/`, `Frontend\``autoload/front/` (cross-platform Linux safe)
- Smoke test runtime: walidacja e-maila zwraca PL komunikat ✓
- Zero regresji: Domain\ + Shared\ ładują się dalej
- [Phase 6, Plan 06-02] Support layer — TableListRequestFactory + FormRequestHandler + FormFieldRenderer (752 L) skopiowane z shopPRO
- Decyzja: brak BaseController — Phase 7+ kontrolery jako POJOs z DI (jak shopPRO)
- PSR-4 filename fix: `TableListRequestFactory.php` zamiast shopPRO `class.TableListRequestFactory.php`
- **Phase 6 zamknięta** — pełny fundament Admin\ namespace gotowy dla Phase 7+
## Zmienione pliki
- `autoload/admin/ViewModels/Forms/FormEditViewModel.php` (utworzony)
- `autoload/admin/ViewModels/Forms/FormField.php` (utworzony)
- `autoload/admin/ViewModels/Forms/FormFieldType.php` (utworzony)
- `autoload/admin/ViewModels/Forms/FormTab.php` (utworzony)
- `autoload/admin/ViewModels/Forms/FormAction.php` (utworzony)
- `autoload/admin/Validation/FormValidator.php` (utworzony)
- `composer.json`
- `vendor/composer/autoload_*` (regen)
- `.paul/STATE.md`, `.paul/ROADMAP.md`
- `.paul/phases/06-admin-base/06-01-PLAN.md` (utworzony)
- `.paul/phases/06-admin-base/06-01-SUMMARY.md` (utworzony)
- `autoload/admin/Support/TableListRequestFactory.php` (utworzony)
- `autoload/admin/Support/Forms/FormRequestHandler.php` (utworzony)
- `autoload/admin/Support/Forms/FormFieldRenderer.php` (utworzony)
- `.paul/phases/06-admin-base/06-02-PLAN.md` (utworzony)
- `.paul/phases/06-admin-base/06-02-SUMMARY.md` (utworzony)

View File

@@ -0,0 +1,170 @@
# Architecture
> Generated: 2026-04-26
## Overview
cmsPRO uses a **3-layer architecture** with clean admin/frontend separation:
```
Request
Controls (admin\controls\ or front\controls\) ← request handling
Factories (admin\factory\ or front\factory\) ← DEPRECATED wrappers → will be removed
Domain Repositories (Domain\*\*Repository) ← data access (new pattern)
Medoo ORM → MySQL
```
Views are rendered through `admin\view\*` / `front\view\*``Shared\Tpl\Tpl` → Savant3 templates.
## Directory Map
```
autoload/
├── autoloader.php Hybrid PSR-4 + legacy autoloader
├── class.S.php Global helper facade (deprecated wrapper)
├── class.Article.php Legacy entity (ArrayAccess)
├── class.Page.php Legacy entity
├── class.Scontainer.php Legacy entity
├── class.Cache.php / class.Cron.php / class.Image.php / class.Html.php
├── Domain/ NEW — Repository pattern, DDD
│ ├── Articles/ArticlesRepository.php (648 lines)
│ ├── Authors/AuthorsRepository.php (156 lines)
│ ├── Banners/BannersRepository.php (148 lines)
│ ├── Languages/LanguagesRepository.php (213 lines)
│ ├── Layouts/LayoutsRepository.php (123 lines)
│ ├── Newsletter/NewsletterRepository.php (281 lines)
│ ├── Pages/PagesRepository.php (451 lines)
│ ├── Scontainers/ScontainersRepository.php (110 lines)
│ ├── Settings/SettingsRepository.php (73 lines)
│ └── User/UserRepository.php (235 lines)
├── Shared/ Cross-cutting services
│ ├── Helpers/Helpers.php God class — 1220 lines (⚠ needs splitting)
│ ├── Tpl/Tpl.php Template renderer (checks templates_user/ first)
│ ├── Email/Email.php Email service (wraps PHPMailer)
│ ├── Cache/CacheHandler.php File-based cache (gzdeflate, TTL)
│ ├── Security/CsrfToken.php CSRF token generation + validation
│ ├── Html/Html.php HTML form element builder
│ └── Image/ImageManipulator.php Image processing
├── admin/ Mixed: legacy lowercase namespaces + new Admin\* PSR-4
│ │
│ │ NEW (Phase 6) — namespace Admin\* (PascalCase, PSR-4)
│ ├── ViewModels/Forms/ Admin\ViewModels\Forms\* — Form Edit System (5 VMs, 687 L)
│ ├── Validation/FormValidator.php
│ ├── Support/TableListRequestFactory.php Admin\Support — list parser
│ ├── Support/Forms/FormRequestHandler.php POST + CSRF + validate + persist
│ ├── Support/Forms/FormFieldRenderer.php HTML renderer (delegates Shared\Html\Html)
│ │
│ │ LEGACY — namespace admin\* (lowercase, hybrid loader)
│ ├── class.Site.php Admin routing + 2FA
│ ├── controls/class.*.php 18 request handler classes (static methods)
│ ├── factory/class.*.php 18 @deprecated wrappers
│ └── view/class.*.php 14 template renderer classes
└── front/
├── controls/class.Site.php Main frontend router
├── controls/class.*.php 4 frontend controllers
├── factory/class.*.php 17 frontend factories
└── view/class.*.php View renderers
admin/
├── index.php Admin entry point (IP check, session, routing)
├── ajax.php Admin AJAX dispatcher → admin/ajax/*.php
└── templates/ Admin Savant3 templates (per module)
templates/ Frontend Savant3 templates
templates_user/ User-overridable template overrides
plugins/
├── special-actions.php Hook: pre-routing
├── special-actions-middle.php Hook: mid-request
└── special-actions-end.php Hook: post-rendering
```
## Namespace Convention
| Namespace | Path | Convention |
|-----------|------|-----------|
| `admin\controls\` | `autoload/admin/controls/class.*.php` | Legacy lowercase |
| `admin\factory\` | `autoload/admin/factory/class.*.php` | Legacy, @deprecated |
| `admin\view\` | `autoload/admin/view/class.*.php` | Legacy lowercase |
| `front\controls\` | `autoload/front/controls/class.*.php` | Legacy lowercase |
| `front\factory\` | `autoload/front/factory/class.*.php` | Legacy lowercase |
| `Domain\*\` | `autoload/Domain/*/ClassName.php` | PSR-4 PascalCase |
| `Shared\*\` | `autoload/Shared/*/ClassName.php` | PSR-4 PascalCase |
| `Admin\*\` | `autoload/admin/*/ClassName.php` | PSR-4 PascalCase namespace, lowercase folder (Win fs collision with legacy) |
## Key Patterns
### Repository Pattern (Domain layer)
```php
class ArticlesRepository {
public function __construct($db) { $this->db = $db; }
public function find(int $id): ?array { ... }
public function save(...): int { ... }
}
```
### Factory Wrapper (deprecated bridge)
```php
/** @deprecated Używaj Domain\Articles\ArticlesRepository przez DI */
class Articles {
private static function repo(): ArticlesRepository {
global $mdb;
return new ArticlesRepository($mdb);
}
public static function article_delete($id): bool {
return self::repo()->deleteArticle($id);
}
}
```
### Controls (request handler)
```php
class Articles {
public static function article_delete() {
global $user;
if (!admin\factory\Users::check_privileges('articles', $user['id']))
return \S::alert('Brak uprawnień');
// delegate to factory → repository
}
}
```
### Global Helper Facade
```php
// class.S.php — calls Shared\Helpers\Helpers via __callStatic
\S::get('param') // → Helpers::get()
\S::delete_cache() // → Helpers::delete_cache()
```
## Admin Routing
`GET /admin/?a=articles&action=view_list``admin\controls\Articles::view_list()`
Routing in `admin/index.php`: reads `$_GET['a']` → dynamically loads control class → calls action method.
## Frontend Routing
`index.php``front\controls\Site::route()` — checks `\S::get('search')`, `\S::get('tag')`, `\S::get('article')`, then falls through to page rendering by `page_type`.
## Caching Strategy
| Cache Type | Location | Engine |
|-----------|----------|--------|
| Page cache | `cache/` | Full HTML output |
| Object cache | `temp/md5[0]/md5[1]/` | gzdeflate + serialize, TTL |
| WebP images | `cache/` | Filesystem |
| Language strings | `$_SESSION` | PHP session |
## Plugin System
3 hook points in frontend lifecycle (files in `plugins/` directory, included if they exist):
1. `special-actions.php` — after language init, before routing
2. `special-actions-middle.php` — before cache check
3. `special-actions-end.php` — before final output

149
.paul/codebase/concerns.md Normal file
View File

@@ -0,0 +1,149 @@
# Technical Debt & Concerns
> Generated: 2026-04-26 | Prioritized by severity
## CRITICAL
### C1 — Unserialize on User-Controlled Cookies
**File**: `admin/ajax/pages.php` lines 36, 49
**Code**: `$array = unserialize($_COOKIE['cookie_menus']);`
**Risk**: Object injection / RCE — classic PHP vulnerability.
**Fix**: Replace with `json_decode($_COOKIE['cookie_menus'] ?? '{}', true)`.
### C2 — Path Traversal in Update File Deletion
**File**: `autoload/admin/factory/class.Update.php` lines 76-80, 119-128
**Code**: `unlink('../' . $filePath)``$filePath` from JSON manifest, not validated.
**Risk**: Attacker-controlled manifest could delete arbitrary files.
**Fix**:
```php
$full = realpath('../' . $filePath);
$base = realpath('../');
if (strpos($full, $base) !== 0) throw new \Exception('Path traversal');
unlink($full);
```
### C3 — God Class: Helpers.php (1220 lines, 75+ static methods)
**File**: `autoload/Shared/Helpers/Helpers.php`
**Risk**: Unmaintainable, untestable, global state dependency (`global $mdb, $settings, $lang`).
**Domains mixed**: image processing, HTML DOM, caching, SEO, authentication, dates, session.
**Fix**: Extract into focused service classes (`ImageService`, `SeoHelper`, `DateHelper`, etc.).
---
## HIGH
### H1 — Direct Superglobal Access Without Validation
**File**: `autoload/Shared/Helpers/Helpers.php` lines 25-26
**Code**: `$crop_w = $_GET['c_w'];` — no isset, no type check.
**Also**: `admin/ajax/pages.php` lines 36, 49 — `\S::get()` passed directly to queries.
**Fix**: Centralized request wrapper with typed getters.
### H2 — SQL String Concatenation (String Values)
**File**: `autoload/Domain/Articles/ArticlesRepository.php` lines 53, 68, 87 and others.
**Code**: `"... WHERE article_id = " . (int)$id` — integer cast OK, but pattern is dangerous for string params.
**Fix**: Use Medoo parameterized methods exclusively. Audit and replace all raw `query()` calls.
### H3 — No Input Validation / Sanitization Layer
**All entry points** — no `Validator` or `Sanitizer` class. Values flow from `$_GET`/`$_POST` → repository without validation.
**Fix**: Add validation at control layer before delegation to factory/repository.
### H4 — Password Hash in Cookie
**File**: `admin/index.php` lines 59-61
**Code**: `$obj = json_decode($_COOKIE[$cookie_name]); $password = $obj->{'hash'};`
**Risk**: Cookie exposure leaks credential hash, no HMAC signing.
**Fix**: Use signed JWT or HMAC-signed remember-me token, never store hashes in cookies.
### H5 — Update Download Without Signature Verification
**File**: `autoload/admin/factory/class.Update.php` lines 12, 25, 28
**Code**: `file_get_contents('https://www.cmspro.project-dc.pl/updates/...')`
**Risk**: MITM, supply chain — ZIP extracted without verifying integrity beyond SHA256 (if present).
**Fix**: Verify SHA256 checksum server-side before extraction; use curl with `CURLOPT_SSL_VERIFYPEER`.
### H6 — Deprecated `mime_content_type()` Removed in PHP 8.1
**File**: `autoload/Shared/Helpers/Helpers.php` line 39
**Fix**:
```php
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $file);
finfo_close($finfo);
```
---
## MEDIUM
### M1 — Global Variables as Dependency Injection
**Files**: Factory classes (`global $mdb`, `global $user`), Helpers (`global $settings, $lang`).
**Risk**: Untestable, tightly coupled, order-dependent initialization.
**Fix**: Pass `$mdb` to factories/repositories directly; remove `global` from repository code.
### M2 — Repository Classes Contain Business Logic and Side Effects
**File**: `autoload/Domain/Articles/ArticlesRepository.php` line 45, 59
**Code**: `\S::delete_cache()` and `\S::seo()` called inside repository methods.
**Fix**: Repositories should only do DB operations; call side effects in factories/services.
### M3 — Mixed Procedural + OOP AJAX Handlers
**Files**: `admin/ajax/pages.php`, `admin/ajax/articles.php`, `admin/ajax/users.php`
**Pattern**: 50-90 line `if ($a == '...')` chains, no routing abstraction.
**Fix**: Create `AjaxRouter` + controller base class.
### M4 — No Request/Response Abstraction
**All entry points** — `$_GET`/`$_POST` accessed directly everywhere.
**Fix**: `Request` class (typed getters) + `JsonResponse` class.
### M5 — Error Suppression with `@` Operator
**Files**: `admin/index.php` lines 2, 14; Helpers.php lines 40, 98, 111, 1188-1200
**Code**: `@file_get_contents(...)`, `@unlink(...)`.
**Fix**: Use `if (file_exists())` guards and proper try/catch.
### M6 — Uninitialized Variables
**File**: `autoload/Domain/Articles/ArticlesRepository.php` line 72
**Code**: `if ($out == '')``$out` never declared.
**Fix**: `$out = '';` before the loop.
### M7 — No Interface Contracts for Repositories
All 10 repositories share identical method signatures but no shared interface.
**Fix**: Define `RepositoryInterface` with `find()`, `all()`, `save()`, `delete()`.
### M8 — Hardcoded Values
- Update base URL: `'https://www.cmspro.project-dc.pl/updates/'` in 3 files
- File permissions: `chmod(..., 0755)` in 25 places
- Cookie expiry: `time() + 3600 * 24 * 365` as magic number
**Fix**: Extract to constants in a config class.
---
## LOW
### L1 — Backup Files in Repository
`libraries/medoo/medoo.bck.php` (973 lines), `libraries/grid/gdb.min.bck.php` (957 lines).
**Fix**: Delete; use Git for history.
### L2 — `test.php` in Project Root (700 lines)
Production benchmark/test script accessible via HTTP. Contains DB credentials in lines 15-17.
**Fix**: Remove or move to `tests/` with `.htaccess` protection.
### L3 — Legacy `class.S.php` Wrapper
200+ calls to `\S::*` throughout codebase — double indirection through `__callStatic`.
**Fix**: Gradual rename campaign to `\Shared\Helpers\Helpers::*`.
### L4 — Legacy SQL Update Fallback Format
`class.Update.php` lines 97-132 — parses old `_sql.txt` format alongside new JSON manifest.
**Fix**: Deprecate and remove once all deployments are on manifest format.
### L5 — Update Process Without Rollback
SQL runs before file extraction. If extraction fails, DB is inconsistent. No transaction wrapping.
**Fix**: Wrap SQL in transaction; extract files first, then run SQL; add rollback on failure.
---
## Files Needing Immediate Attention
| File | Lines | Issue |
|------|-------|-------|
| `autoload/Shared/Helpers/Helpers.php` | 1220 | God class (C3) |
| `autoload/admin/factory/class.Update.php` | 157 | Path traversal (C2), supply chain (H5) |
| `admin/ajax/pages.php` | ~90 | Unserialize (C1), missing validation (H1) |
| `admin/index.php` | — | Password hash in cookie (H4) |
| `autoload/Domain/Articles/ArticlesRepository.php` | 648 | Side effects in repo (M2), raw SQL (H2) |
| `test.php` | 700 | Remove from root (L2) |

View File

@@ -0,0 +1,161 @@
# Coding Conventions
> Generated: 2026-04-26
## File Naming
| Layer | Convention | Example |
|-------|-----------|---------|
| Legacy (admin/front) | `class.{ClassName}.php` | `class.Articles.php` |
| Domain repositories | `{ClassName}.php` (PSR-4) | `ArticlesRepository.php` |
| Shared services | `{ClassName}.php` (PSR-4) | `CacheHandler.php` |
| Templates | `{feature-name}.php` | `articles/list.php` |
## Naming Conventions
| Element | Legacy code | New Domain code |
|---------|------------|-----------------|
| Methods | `snake_case` | `camelCase` |
| Classes | `PascalCase` | `PascalCase` |
| Properties | `$camelCase` | `$camelCase` |
| Constants | `UPPER_CASE` | `UPPER_CASE` |
| Namespaces | lowercase (`admin\`, `front\`) | PascalCase (`Domain\`, `Shared\`) |
## Class Patterns
### Controls (request handlers) — static methods only
```php
namespace admin\controls;
class Articles {
public static function article_delete() {
global $user;
if (!admin\factory\Users::check_privileges('articles', $user['id']))
return \S::alert('Brak uprawnień');
admin\factory\Articles::article_delete(\S::get('article_id'));
}
}
```
### Factories — @deprecated wrappers, static methods, delegate to repo
```php
namespace admin\factory;
/** @deprecated Wrapper — używaj \Domain\Articles\ArticlesRepository przez DI */
class Articles {
private static function repo(): \Domain\Articles\ArticlesRepository {
global $mdb;
return new \Domain\Articles\ArticlesRepository($mdb);
}
public static function article_delete($id): bool {
return self::repo()->deleteArticle((int)$id);
}
}
```
### Domain Repositories — constructor DI, camelCase, typed returns
```php
namespace Domain\Articles;
class ArticlesRepository {
private $db;
public function __construct($db) { $this->db = $db; }
// -------------------------------------------------------------------------
// Odczyt (Read)
// -------------------------------------------------------------------------
public function find(int $id): ?array {
return $this->db->get('pp_articles', '*', ['id' => $id]) ?: null;
}
// -------------------------------------------------------------------------
// Zapis / usuwanie (Write / Delete)
// -------------------------------------------------------------------------
public function deleteArticle(int $id): bool {
$this->db->delete('pp_articles', ['id' => $id]);
return true;
}
}
```
### View classes — static rendering
```php
namespace admin\view;
class Articles {
public static function list($articles) {
$tpl = new \Tpl;
$tpl->articles = $articles;
return $tpl->render('articles/list');
}
}
```
## PHPDoc Style
Polish-language descriptions are standard in this project:
```php
/**
* Prosta lista autorów
* @return array|bool
*/
public function authorsList() { ... }
/**
* Zapis autora (insert lub update)
* @param int $authorId
* @param string $author
* @return object|bool
*/
public function authorSave(int $authorId, string $author) { ... }
```
Section separators in larger classes:
```php
// -------------------------------------------------------------------------
// Odczyt (Read operations)
// -------------------------------------------------------------------------
```
## Return Patterns
| Pattern | Usage |
|---------|-------|
| `?array` | Single record lookup (null = not found) |
| `array` (possibly `[]`) | List queries — `?: []` fallback |
| `bool` | Write/delete operations |
| `int` | Codes: `1 = OK`, `0 = bad credentials`, `-1 = blocked` |
| `void` | Side-effect-only writes |
| `['status' => 'ok'/'error', 'msg' => '...']` | AJAX JSON responses |
## Error Handling
- Repositories return `null`/`false`/`[]` for "not found", don't throw
- `ImageManipulator` uses typed exceptions (`\InvalidArgumentException`, `\RuntimeException`)
- AJAX endpoints: `json_encode(['status' => 'ok/error', 'msg' => '...'])`
- Error suppression with `@` is used in legacy code (avoid in new code)
## Database Access via Medoo
Always use parameterized Medoo methods — never string concatenation with string values:
```php
// Good
$this->db->get('pp_articles', '*', ['id' => $id]);
$this->db->select('pp_articles', '*', ['ORDER' => ['created' => 'DESC']]);
$this->db->update('pp_articles', ['status' => 1], ['id' => $id]);
$this->db->insert('pp_articles', ['title' => $title, 'slug' => $slug]);
// Acceptable (integer cast only)
$this->db->query("SELECT ... WHERE id = " . (int)$id)->fetchAll();
// Never
$this->db->query("SELECT ... WHERE slug = '" . $slug . "'"); // SQL injection risk
```
## Global Helper Facade (`\S::`)
Legacy code uses `\S::method()` — new code should use `\Shared\Helpers\Helpers::method()` directly or inject the dependency. Migrate `\S::` calls opportunistically but don't block on it.
## Template Rendering
```php
$tpl = new \Tpl; // or: new \Shared\Tpl\Tpl
$tpl->variable = $value; // assign template variables
return $tpl->render('module/template-name'); // checks templates_user/ first, then templates/
```

View File

@@ -0,0 +1,12 @@
# Database Schema
> Schemat bazy danych — tabele, kolumny, FK, indeksy.
> Generowany automatycznie przez /paul:map-codebase, aktualizowany przez apply-phase.
## Engine
MySQL — konfiguracja w `config.php`, abstrakcja Medoo 1.7.3 (`libraries/medoo/medoo.php`), prefix tabel: `pp_`
## Tables
> Uruchom `/paul:map-codebase` aby wypełnić ten plik szczegółowym schematem tabel.

View File

@@ -0,0 +1,63 @@
# External Integrations
> Generated: 2026-04-26
## Email — PHPMailer + SMTP
- **Library**: PHPMailer (`libraries/phpmailer/class.phpmailer.php`)
- **Service class**: `autoload/Shared/Email/Email.php`
- **Configuration**: stored in `pp_settings` table
- Keys: `email_host`, `email_port`, `email_login`, `email_password`, `contact_email`, `firm_name`
- **Features**: SSL/TLS, self-signed cert support, HTML email, attachments, relative URL conversion
- **Used by**: Newsletter cron, contact forms, 2FA code sending
## Geolocation — geoPlugin
- **Provider**: geoPlugin (http://www.geoplugin.net/)
- **Class**: `autoload/class.geoplugin.php`
- **Features**: IP-to-country, currency detection, exchange rates
- **Integration**: loaded in frontend via autoloader, used for localization hints
## Analytics
- **Type**: configurable (any script tag)
- **Storage**: `pp_settings.statistic_code` field
- **Injection**: `index.php` lines ~121-122 — injected into HTML `<head>` via string replacement
- **Default**: empty (disabled until configured in admin Settings)
## Updates — cmspro.project-dc.pl
- **Factory**: `autoload/admin/factory/class.Update.php`
- **Base URL**: `https://www.cmspro.project-dc.pl/updates/` (hardcoded)
- **Endpoints used**:
- `versions.php?key={update_key}` — fetch available versions list
- `{dir}/ver_{version}.zip` — download update ZIP
- `{dir}/ver_{version}_sql.txt` — legacy SQL migration fallback
- **Auth**: `update_key` from `pp_settings`, validated on server
- **License**: `pp_update_licenses` table — `valid_to_date`, `valid_to_version`, `beta` flag
- **Channels**: stable / beta
**Security note**: `file_get_contents()` over HTTPS, no signature verification, path not sanitized.
See `concerns.md` for details.
## File Manager
- **Library**: FileManager 9.14.1 (`libraries/filemanager-9.14.1/`)
- **API endpoint**: `upload/filemanager/api/`
- **Features**: file upload, deletion, browsing via AJAX
- **MIME validation**: JPEG, PNG, GIF, WebP allowed
- **Organization**: files stored by article ID under `upload/`
## Mobile Detection
- **Library**: Mobile_Detect 2.8.16 (`autoload/class.Mobile_Detect.php`)
- **Usage**: UA-based device detection for mobile/tablet
- **Integration**: used in frontend factory to adapt output
## No Payment Integration
No PayPal, Stripe, or other payment processor code detected.
## No CDN
Images served locally. WebP conversion cached in `cache/` directory.

80
.paul/codebase/stack.md Normal file
View File

@@ -0,0 +1,80 @@
# Technology Stack
> Generated: 2026-04-26
## PHP Runtime
- **Required**: PHP 7.4+ (nikic/php-parser constraint), PHP 7.1+ / 8.0+ (deep-copy)
- **Composer**: `composer.json` at project root
- **Dev dependency**: `phpunit/phpunit: ^10.5`
- **No runtime Composer packages** — all libraries are vendored manually in `libraries/`
## Database
| Item | Value |
|------|-------|
| Engine | MySQL |
| Config | `config.php` (plain-text credentials) |
| Abstraction | Medoo 1.7.3 (`libraries/medoo/medoo.php`) |
| Table prefix | `pp_` |
| Remote host | `host117523.hostido.net.pl` (hostido.net.pl hosting) |
Key tables: `pp_articles`, `pp_articles_langs`, `pp_pages`, `pp_layouts`, `pp_users`, `pp_users_privileges`, `pp_newsletter`, `pp_newsletter_templates`, `pp_banners`, `pp_scontainers`, `pp_authors`, `pp_languages`, `pp_settings`, `pp_tags`, `pp_update_versions`, `pp_update_licenses`
## Frontend Libraries (all vendored in `libraries/`)
| Library | Version | Purpose |
|---------|---------|---------|
| jQuery | 2.1.3 | JavaScript DOM |
| Bootstrap | 4.1.3 | CSS/JS framework |
| Font Awesome | 4.7.0 | Icons |
| jQuery UI | — | UI widgets |
| CKEditor | — | WYSIWYG editor |
| Leaflet | — | Maps (in CKEditor plugin) |
| Plupload | 3.1.2 | File upload |
| jQuery Confirm | — | Confirmation dialogs |
| FancyBox | — | Lightbox/modal |
| CodeMirror | — | Code editor |
| Lozad.js | — | Lazy loading |
| MotionCAPTCHA | — | CAPTCHA |
| FileManager | 9.14.1 | File browse/upload UI |
**No build tools** — no webpack, vite, or gulp. Raw JS/CSS files.
Custom JS: `libraries/functions.js`, `libraries/functions-front.js`, `libraries/jquery/javascript.js`
## PHP Libraries (vendored)
| Library | Location | Purpose |
|---------|----------|---------|
| PHPMailer | `libraries/phpmailer/` | SMTP email (class.phpmailer.php, class.smtp.php) |
| Medoo | `libraries/medoo/medoo.php` | Database abstraction |
| MySQLDump | `libraries/MySQLDump.php` | SQL dump utility |
| Savant3 | `autoload/Savant3.php` | Template engine |
| Mobile_Detect | `autoload/class.Mobile_Detect.php` | 2.8.16, device detection |
| geoPlugin | `autoload/class.geoplugin.php` | IP geolocation |
## Server
- **Apache** with mod_rewrite, mod_deflate, mod_expires
- Config: `.htaccess` — HTTPS redirect, www enforcement, trailing slash, gzip, 1-year browser cache
- Optional admin IP whitelist: `admin/ip.conf`
- Session: PHP native sessions with IP validation and regeneration
- Cache: File-based in `cache/` and `temp/` directories
## External Services
| Service | Purpose | Integration |
|---------|---------|-------------|
| SMTP (configurable) | Email delivery | PHPMailer, settings in `pp_settings` |
| geoPlugin (geoplugin.net) | IP geolocation | `class.geoplugin.php` |
| cmspro.project-dc.pl | Update downloads | `autoload/admin/factory/class.Update.php` line 12, 25 |
| Analytics (configurable) | Stats injection | `pp_settings.statistic_code` → injected in `<head>` |
## Autoloading
Hybrid custom autoloader at `autoload/autoloader.php`:
1. Tries `autoload/{namespace}/class.{ClassName}.php` (legacy)
2. Falls back to `autoload/{namespace}/{ClassName}.php` (PSR-4)
Composer PSR-4 mappings: `Domain\``autoload/Domain/`, `Shared\``autoload/Shared/`

124
.paul/codebase/testing.md Normal file
View File

@@ -0,0 +1,124 @@
# Testing
> Generated: 2026-04-26
## Framework
- **PHPUnit 10.5+** (`phpunit/phpunit` in `composer.json` dev)
- Config: `phpunit.xml` at project root
- Bootstrap: `tests/bootstrap.php`
## Structure
```
tests/
├── bootstrap.php Test bootstrap (PSR-4 autoload for Domain\)
├── stubs/
│ ├── CacheHandler.php In-memory stub (replaces file-based cache)
│ └── S.php Helper facade stub
└── Unit/
└── Domain/
├── Languages/LanguagesRepositoryTest.php
├── Settings/SettingsRepositoryTest.php
└── User/UserRepositoryTest.php
```
## Bootstrap Setup
`tests/bootstrap.php`:
- Loads Medoo ORM (`libraries/medoo/medoo.php`)
- Loads stubs **before** autoloader (to override `Shared\Cache\CacheHandler`)
- Registers PSR-4 autoloader for `Domain\` namespace only
**Critical**: Stubs must be loaded before autoloader. CacheHandler stub provides `reset()` method for test isolation.
## Test Pattern
All tests follow **AAA (Arrange-Act-Assert)** with Medoo mocked:
```php
namespace Tests\Unit\Domain\Languages;
use Domain\Languages\LanguagesRepository;
use PHPUnit\Framework\TestCase;
class LanguagesRepositoryTest extends TestCase {
private function mockDb(): object {
return $this->createMock(\medoo::class);
}
protected function setUp(): void {
\Shared\Cache\CacheHandler::reset(); // clear in-memory cache
}
public function testLanguagesListReturnsArray(): void {
$db = $this->mockDb();
$db->method('select')->willReturn([['id' => 'pl', 'name' => 'Polski']]);
$repo = new LanguagesRepository($db);
$result = $repo->languagesList();
$this->assertSame([['id' => 'pl', 'name' => 'Polski']], $result);
}
public function testLanguagesListReturnsEmptyWhenNull(): void {
$db = $this->mockDb();
$db->method('select')->willReturn(null);
$this->assertSame([], (new LanguagesRepository($db))->languagesList());
}
public function testActiveLanguagesQueriesDbAndCaches(): void {
$expected = [['id' => 'pl', 'name' => 'Polski', 'domain' => null]];
$db = $this->mockDb();
$db->expects($this->once())->method('select')->willReturn($expected);
$repo = new LanguagesRepository($db);
$this->assertSame($expected, $repo->activeLanguages());
$this->assertSame($expected, $repo->activeLanguages()); // 2nd call hits cache
}
}
```
## Stubs
### `tests/stubs/CacheHandler.php`
In-memory replacement for `Shared\Cache\CacheHandler`:
- `static::$store` — array key-value store
- `reset()` — clear all stored values (call in `setUp()`)
- `fetch($key)` — return stored value or `false`
- `store($key, $value, $ttl)` — store value (TTL ignored)
- `delete($key)` — remove value
### `tests/stubs/S.php`
Stub for the `\S` global helper facade — prevents tests from hitting real filesystem/session code.
## Coverage
Currently tested: **Domain layer only**
- `Domain\Languages\LanguagesRepository`
- `Domain\Settings\SettingsRepository`
- `Domain\User\UserRepository`
- All other Domain repositories: **no tests yet**
Not tested:
- `admin\controls\*` — static controllers
- `admin\factory\*` — deprecated wrappers
- `front\*` — frontend layer
- `Shared\*` — utilities
- AJAX handlers
## Running Tests
```bash
composer test
# or
./vendor/bin/phpunit
```
## Adding Tests for New Repositories
When adding a new `Domain\{Entity}\{Entity}Repository`:
1. Create `tests/Unit/Domain/{Entity}/{Entity}RepositoryTest.php`
2. Call `\Shared\Cache\CacheHandler::reset()` in `setUp()` if the repo uses caching
3. Mock `\medoo` via `$this->createMock(\medoo::class)`
4. Test: null-to-empty-array coercion, cache hit (expects `once()`), write returns expected type

4
.paul/codebase/todo.md Normal file
View File

@@ -0,0 +1,4 @@
# TODO
> Luźny parking pomysłów, rzeczy do sprawdzenia, rzeczy które mogą nigdy nie być wdrożone.
> Nie wymaga formalności — wrzucaj co chcesz.

3
.paul/docs/API.md Normal file
View File

@@ -0,0 +1,3 @@
# API
> Endpointy, kontrakty request/response, autentykacja.

View File

@@ -0,0 +1,3 @@
# ARCHITECTURE
> Struktura klas, modulow, przeplywow i zaleznosci w projekcie.

3
.paul/docs/DB_SCHEMA.md Normal file
View File

@@ -0,0 +1,3 @@
# DB_SCHEMA
> Schemat bazy danych — tabele, kolumny, FK, indeksy.

3
.paul/docs/DECISIONS.md Normal file
View File

@@ -0,0 +1,3 @@
# DECISIONS
> Kluczowe decyzje techniczne i ich uzasadnienia.

3
.paul/docs/STACK.md Normal file
View File

@@ -0,0 +1,3 @@
# STACK
> Stack technologiczny, wersje, zaleznosci, srodowisko.

View File

@@ -0,0 +1,3 @@
# TECH_CHANGELOG
> Chronologiczny log zmian technicznych — co i dlaczego.

3
.paul/docs/TODO.md Normal file
View File

@@ -0,0 +1,3 @@
# TODO
> Luzny parking pomyslow, rzeczy do sprawdzenia.

View File

@@ -0,0 +1,2 @@
{"ts":"2026-04-25T21:59:21Z","tool":"Bash","cmd":"diff -q \"C:/visual studio code/projekty/cmsPRO/autoload/admin/controls/class.Update.php\" \"C:/visual studio code/projekty/cmstest.pagedev.pl/autoload/admin/controls/class.Update.php\" 2>&1; echo \"-","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T21:59:48Z","tool":"Bash","cmd":"diff -q \"C:/visual studio code/projekty/cmsPRO/autoload/Shared/Helpers/Helpers.php\" \"C:/visual studio code/projekty/cmstest.pagedev.pl/autoload/Shared/Helpers/Helpers.php\" 2>&1; echo \"---\"; diff","cwd":"/c/visual studio code/projekty/cmsPRO"}

View File

@@ -0,0 +1,84 @@
{"ts":"2026-04-25T22:00:08Z","tool":"Bash","cmd":"curl -sS \"http://www.cmspro.project-dc.pl/updates/versions.php\" 2>&1 | head -20; echo \"===NO KEY===\"; curl -sS \"http://www.cmspro.project-dc.pl/updates/versions.php?key=test123\" 2>&1 | head -20\","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:00:20Z","tool":"Bash","cmd":"curl -sSI \"http://www.cmspro.project-dc.pl/updates/versions.php?key=test\" 2>&1 | grep -i -E \"location|HTTP/\"; echo \"---FOLLOW---\"; curl -sSL \"http://www.cmspro.project-dc.pl/updates/versions.ph","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:39:39Z","tool":"Bash","cmd":"wc -l \"C:\\\\visual studio code\\\\projekty\\\\cmsPRO/autoload/Domain/Articles/ArticlesRepository.php\" \"C:\\\\visual studio code\\\\projekty\\\\cmsPRO/autoload/Domain/Pages/PagesRepository.php\" \"","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:40:05Z","tool":"Bash","cmd":"for file in /c/visual\\\\ studio\\\\ code/projekty/cmsPRO/autoload/Domain/*/*.php; do wc -l \"$file\"; done\"},\"tool_response\":{\"stdout\":\"648 /c/visual studio code/projekty/cmsPRO/autoload/Domain","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:40:27Z","tool":"Bash","cmd":"wc -l \"C:\\\\visual studio code\\\\projekty\\\\cmsPRO/admin/ajax.php\" \"C:\\\\visual studio code\\\\projekty\\\\cmsPRO/admin/ajax/articles.php\" \"C:\\\\visual studio code\\\\projekty\\\\cmsPRO/admi","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:42:47Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/cmsPRO/.paul/codebase\"\",\"description\":\"Create .paul/codebase directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isIm","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:43:05Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\overview.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:43:22Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\stack.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:43:51Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\architecture.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:44:20Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\conventions.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:44:39Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\testing.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:44:55Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\integrations.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:45:39Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\concerns.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:45:56Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\README.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:46:01Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && git add .paul/codebase/ && git commit -m \"$(cat <<'EOF'\\ndocs(codebase): mapa kodu wygenerowana przez /paul:map-codebase\\n\\n7 dokumentów w .paul/co","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:49:08Z","tool":"Bash","cmd":"mkdir -p \"/c/visual studio code/projekty/cmsPRO/.paul/phases/05-domain-seoadditional-cron-releases\"\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"no","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:50:01Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\05-domain-seoadditional-cron-releases\\\\05-01-PLAN.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:50:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:50:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:50:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:51:44Z","tool":"Bash","cmd":"mkdir -p \"/c/visual studio code/projekty/cmsPRO/autoload/Domain/SeoAdditional\"\",\"description\":\"Create SeoAdditional domain directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"inter","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:51:54Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Domain\\\\SeoAdditional\\\\SeoAdditionalRepository.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:51:59Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/cmsPRO/autoload/Domain/Cron\"\",\"description\":\"Create Domain/Cron directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"i","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:52:04Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\admin\\\\factory\\\\class.SeoAdditional.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:52:11Z","tool":"Bash","cmd":"php -l \"/c/visual studio code/projekty/cmsPRO/autoload/Domain/SeoAdditional/SeoAdditionalRepository.php\" && php -l \"/c/visual studio code/projekty/cmsPRO/autoload/admin/factory/class.SeoAdditional.","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:52:31Z","tool":"Bash","cmd":"mkdir -p '/c/visual studio code/projekty/cmsPRO/autoload/Domain/Releases'\",\"description\":\"Create Domain\\\\Releases directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:52:47Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Domain\\\\Releases\\\\ReleasesRepository.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:53:04Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Domain\\\\Cron\\\\CronRepository.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:53:10Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\class.Cron.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:53:11Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Domain\\\\Releases\\\\UpdateRepository.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:53:16Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/cmsPRO/autoload/Domain/Cron/CronRepository.php\" && php -l \"C:/visual studio code/projekty/cmsPRO/autoload/class.Cron.php\"\",\"description\":\"PHP syntax chec","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:53:20Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\admin\\\\factory\\\\class.Releases.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:53:24Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\admin\\\\factory\\\\class.Update.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:53:31Z","tool":"Bash","cmd":"php -l '/c/visual studio code/projekty/cmsPRO/autoload/Domain/Releases/ReleasesRepository.php' && php -l '/c/visual studio code/projekty/cmsPRO/autoload/Domain/Releases/UpdateRepository.php' && php -l","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:54:02Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/cmsPRO/autoload/Domain/SeoAdditional/SeoAdditionalRepository.php\" && php -l \"C:/visual studio code/projekty/cmsPRO/autoload/Domain/Cron/CronRepository.php\" &","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:54:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:54:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:55:41Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\05-domain-seoadditional-cron-releases\\\\05-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:56:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\changelog\\\\2026-04-26.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:56:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\changelog\\\\2026-04-26.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:56:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:56:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:56:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:56:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:56:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:56:58Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:57:05Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && git add autoload/Domain/SeoAdditional/SeoAdditionalRepository.php autoload/Domain/Cron/CronRepository.php autoload/Domain/Releases/ReleasesRepository.ph","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:57:14Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && git commit -m \"$(cat <<'EOF'\\nfeat(05-domain-seoadditional-cron-releases): Domain layer kompletny — SeoAdditional + Cron + Releases\\n\\nPhase 5 com","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:57:45Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && php vendor/bin/phpunit 2>&1\",\"description\":\"Run PHPUnit tests\"},\"tool_response\":{\"stdout\":\"PHPUnit 10.5.63 by Sebastian Bergmann and contribut","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:57:47Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && git log --oneline -5\",\"description\":\"Recent commits for convention reference\"},\"tool_response\":{\"stdout\":\"f7c7c0b feat(05-domain-seoadditional","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:57:56Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && head -60 docs/PROJECT_STRUCTURE.md\",\"description\":\"Check PROJECT_STRUCTURE.md for Domain repos section\"},\"tool_response\":{\"stdout\":\"# Struktur","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:58:03Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && grep -n \"Domain\\\\|Faza\\\\|fase\\\\|Phase\" docs/PROJECT_STRUCTURE.md | head -40\",\"description\":\"Find Domain repos section in PROJECT_STRUCTURE.m","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:58:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\docs\\\\PROJECT_STRUCTURE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:58:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\updates\\\\versions.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:58:35Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && git add docs/PROJECT_STRUCTURE.md updates/versions.php && git commit -m \"$(cat <<'EOF'\\nbuild(update): paczka 1.696 — Domain layer kompletny (Phase ","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:58:43Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && git tag v1.696\",\"description\":\"Create version tag\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"no","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:58:51Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && powershell -ExecutionPolicy Bypass -File ./build-update.ps1 -FromTag v1.695 -ToTag v1.696 -ChangelogEntry \"NEW - Domain layer kompletny: SeoAdditional,","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:59:00Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && git add \"updates/1.60/ver_1.696.zip\" \"updates/1.60/ver_1.696_manifest.json\" && git commit --amend --no-edit\",\"description\":\"Amend commit to incl","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T22:59:10Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/cmsPRO\" && git push && git push --tags 2>&1\",\"timeout\":30000,\"description\":\"Push commits and tags\"},\"tool_response\":{\"stdout\":\"remote: . Processing 1 r","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:13:47Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\init-project.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:13:53Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\init-project.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:13:59Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:14:14Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:14:23Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:14:27Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:14:38Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:15:07Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:15:19Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:15:26Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:15:44Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:15:49Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:16:01Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:16:12Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\map-codebase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:16:25Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\apply-phase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:16:39Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\apply-phase.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:16:44Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\templates\\\\CLAUDE-TEMPLATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:16:49Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\templates\\\\CLAUDE-TEMPLATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:16:59Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\todo.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:17:03Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\db_schema.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:17:08Z","tool":"Bash","cmd":"rm -rf \"C:/visual studio code/projekty/cmsPRO/.paul/docs\" && rm -f \"C:/visual studio code/projekty/cmsPRO/.paul/codebase/overview.md\" \"C:/visual studio code/projekty/cmsPRO/.paul/codebase/README.","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:17:27Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\paul-framework\\\\workflows\\\\init-project.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:24:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Domain\\\\Releases\\\\UpdateRepository.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:33:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\updates\\\\1.60\\\\ver_1.696_manifest.json","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-25T23:43:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\build-update.ps1","cwd":"/c/visual studio code/projekty/cmsPRO"}

View File

@@ -0,0 +1,66 @@
{"ts":"2026-04-30T20:55:35Z","tool":"Bash","cmd":"for f in FormEditViewModel FormField FormFieldType FormTab FormAction; do\\n echo \"===== $f =====\"\\n head -50 \"C:/visual studio code/projekty/shopPRO/autoload/Admin/ViewModels/Forms/${f}.php\"\\","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T20:55:37Z","tool":"Bash","cmd":"wc -l \"C:/visual studio code/projekty/shopPRO/autoload/Admin/ViewModels/Forms/\"*.php \"C:/visual studio code/projekty/shopPRO/autoload/Admin/Validation/FormValidator.php\" 2>&1; echo \"---enum/match","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T20:55:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T20:55:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T20:57:14Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\06-admin-base\\\\06-01-PLAN.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T20:57:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T20:57:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T20:59:21Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/cmsPRO/autoload/Admin/ViewModels/Forms\" \"C:/visual studio code/projekty/cmsPRO/autoload/Admin/Validation\" && echo OK\",\"description\":\"Create Admin/ViewM","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T20:59:48Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormEditViewModel.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:00:16Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormField.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:00:19Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormFieldType.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:00:21Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormTab.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:00:29Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormAction.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:00:48Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\Validation\\\\FormValidator.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:00:56Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\composer.json","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:01:00Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && (composer dump-autoload 2>&1 || php composer.phar dump-autoload 2>&1) | tail -10\",\"description\":\"Regenerate composer autoloader\"},\"tool_response\"","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:01:06Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && for f in autoload/Admin/ViewModels/Forms/FormEditViewModel.php autoload/Admin/ViewModels/Forms/FormField.php autoload/Admin/ViewModels/Forms/FormFieldTy","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:01:11Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php -r \"require 'vendor/autoload.php';\\nforeach (['Admin\\\\\\\\ViewModels\\\\\\\\Forms\\\\\\\\FormEditViewModel','Admin\\\\\\\\ViewModels\\\\\\\\Form","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:01:17Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && git status --short && echo \"---DOMAIN SANITY---\" && php -r \"require 'vendor/autoload.php'; var_dump(class_exists('Domain\\\\\\\\\\\\\\\\Articles\\\\\","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:01:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\composer.json","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:02:06Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php composer.phar dump-autoload 2>&1 | tail -5 && echo \"---RETEST---\" && php -r \"require 'vendor/autoload.php';\\n\\\\$f = new Admin\\\\\\\\ViewModel","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:02:54Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\06-admin-base\\\\06-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:03:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:03:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:04:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:04:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:04:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:04:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:04:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:04:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:04:30Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\changelog\\\\2026-04-30.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:08:22Z","tool":"Bash","cmd":"wc -l \"C:/visual studio code/projekty/shopPRO/autoload/Admin/Support/class.TableListRequestFactory.php\" \"C:/visual studio code/projekty/shopPRO/autoload/Admin/Support/Forms/\"*.php 2>&1; echo \"---","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:11:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:11:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:12:48Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\06-admin-base\\\\06-02-PLAN.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:12:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:12:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:25:29Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/cmsPRO/autoload/admin/Support/Forms\" && echo OK\",\"description\":\"Create Support and Support/Forms directories\"},\"tool_response\":{\"stdout\":\"OK\",\"st","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:25:47Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\admin\\\\Support\\\\TableListRequestFactory.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:26:04Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\admin\\\\Support\\\\Forms\\\\FormRequestHandler.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:26:55Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\admin\\\\Support\\\\Forms\\\\FormFieldRenderer.php","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:27:04Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && for f in autoload/admin/Support/TableListRequestFactory.php autoload/admin/Support/Forms/*.php; do php -l \"$f\"; done && echo \"---DUMP---\" && php com","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:27:20Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php -r \"require 'vendor/autoload.php';\\necho '=== A) AUTOLOAD ==='.PHP_EOL;\\nforeach (['Admin\\\\\\\\Support\\\\\\\\TableListRequestFactory','Admin\\","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:27:30Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php -r \"require 'vendor/autoload.php';\\n\\\\$f = new Admin\\\\\\\\ViewModels\\\\\\\\Forms\\\\\\\\FormField('email', Admin\\\\\\\\ViewModels\\\\\\\\For","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:28:20Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && git status --short | grep -v vendor | grep -v \"\\\\.serena\" | grep -v governance\",\"description\":\"Check git status excluding vendor/serena/governan","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:29:13Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\06-admin-base\\\\06-02-SUMMARY.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:29:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:29:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:29:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:29:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\changelog\\\\2026-04-30.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\changelog\\\\2026-04-30.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\architecture.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:30:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\architecture.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:31:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:31:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:31:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:31:34Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php vendor/bin/phpunit 2>&1 | tail -30\",\"description\":\"Run PHPUnit test suite\"},\"tool_response\":{\"stdout\":\"PHPUnit 10.5.63 by Sebastian Bergma","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:32:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\docs\\\\PROJECT_STRUCTURE.md","cwd":"/c/visual studio code/projekty/cmsPRO"}
{"ts":"2026-04-30T21:32:10Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && git log --oneline -5 && echo \"---STATUS---\" && git status --short\",\"description\":\"Check git log convention and status\"},\"tool_response\":{\"stdo","cwd":"/c/visual studio code/projekty/cmsPRO"}

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

@@ -0,0 +1,379 @@
---
phase: 04h-hotfix-https-updates
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/Shared/Helpers/Helpers.php
- autoload/admin/factory/class.Update.php
- C:/visual studio code/projekty/cmstest.pagedev.pl/autoload/class.S.php
- C:/visual studio code/projekty/cmstest.pagedev.pl/autoload/admin/factory/class.Update.php
- updates/cmsPro.zip
- updates/1.50/ver_1.519.zip
- updates/**/ver_*.zip (audit + warunkowy patch)
- updates/**/ver_*_manifest.json (regeneracja checksum_zip jeśli paczka patchowana)
autonomous: false
delegation: off
---
<objective>
## Goal
Naprawić mechanizm wykrywania nowych wersji aktualizacji (HTTP 301 → HTTPS bug) w trzech warstwach:
1. Bieżący kod cmsPRO (źródło)
2. Instancja testowa cmstest.pagedev.pl (działający bug)
3. Wszystkie dystrybuowane paczki (cmsPro.zip baseline + ver_*.zip), aby nowe instalacje cmsPRO nigdy nie utknęły z tym samym problemem
## Purpose
Serwer cmspro.project-dc.pl został przeniesiony z HTTP na HTTPS i odpowiada `301 Moved Permanently`.
PHP-owy `file_get_contents()` bez kontekstu nie podąża za redirectem, więc zwraca HTML 301 zamiast listy wersji.
Skutkiem `(float)max($html_lines)` daje 0 i `S::get_new_version()` nigdy nie pokazuje nowszej wersji niż zainstalowana.
Aktualnie WSZYSTKIE instancje cmsPRO zainstalowane przed migracją serwera są zablokowane.
## Output
- Patch HTTP→HTTPS w kodzie źródłowym i w instancji testowej
- Audit-report listujący każdą paczkę aktualizacji zawierającą `http://www.cmspro.project-dc.pl`
- Patchowane paczki ZIP + zaktualizowane manifesty (SHA256)
- ver_1.519.zip rozszerzony o poprawione class.S.php i class.Update.php — "kotwica fixa" dla instancji przechodzących przez tę wersję
- Lista plików do uploadu na cmspro.project-dc.pl (dla użytkownika)
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/STATE.md
@.paul/ROADMAP.md
## Source Files
@autoload/Shared/Helpers/Helpers.php
@autoload/admin/factory/class.Update.php
@build-update.ps1
</context>
<acceptance_criteria>
## AC-1: Kod źródłowy cmsPRO używa HTTPS
```gherkin
Given pliki autoload/Shared/Helpers/Helpers.php oraz autoload/admin/factory/class.Update.php
When zostają zaktualizowane
Then nie zawierają już ciągu "http://www.cmspro.project-dc.pl"
And zawierają "https://www.cmspro.project-dc.pl"
```
## AC-2: Instancja testowa jest odblokowana
```gherkin
Given instancja w C:\visual studio code\projekty\cmstest.pagedev.pl\ na wersji 1.519
When pliki autoload/class.S.php i autoload/admin/factory/class.Update.php zostają poprawione (httphttps)
And sesja "new-version" zostaje wyczyszczona (przez wylogowanie/restart sesji)
Then panel admina pokazuje nową wersję > 1.519
And klik "Aktualizuj" pobiera kolejną paczkę bez błędu
```
## AC-3: Wszystkie paczki dystrybucyjne wolne od bug-a
```gherkin
Given katalog updates/ z plikami cmsPro.zip oraz updates/**/ver_*.zip
When skrypt audit przeskanuje każdą paczkę
Then żadna paczka nie zawiera pliku z ciągiem "http://www.cmspro.project-dc.pl"
And paczki które zawierały bug zostały sparowane z patched-wersjami
And manifesty (jeśli istnieją) mają zaktualizowane checksum_zip (SHA256) zgodne z nowym ZIP
```
## AC-4: ver_1.519.zip jest "kotwicą fixa"
```gherkin
Given oryginalny ver_1.519.zip zawiera tylko class.Articles.php
When zostaje rozszerzony o poprawione autoload/class.S.php i autoload/admin/factory/class.Update.php
Then każda nowa instancja cmsPRO przechodząca update do 1.519 dostaje plik z https://
And tym samym może wykryć i pobrać kolejne aktualizacje
```
## AC-5: Audit report dostarczony
```gherkin
Given wykonany skrypt audit
When skanowanie się kończy
Then powstaje plik .paul/phases/04h-hotfix-https-updates/audit-report.md
Z listą: każda paczka, jakie zawiera podejrzane pliki, czy zawiera http://, akcja podjęta (patched|skipped|N/A)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Patch kodu źródłowego cmsPRO (HTTP → HTTPS)</name>
<files>
autoload/Shared/Helpers/Helpers.php,
autoload/admin/factory/class.Update.php
</files>
<action>
Zamień wszystkie wystąpienia `http://www.cmspro.project-dc.pl``https://www.cmspro.project-dc.pl`
w obu plikach (Edit replace_all).
Plików łącznie ~6 wystąpień:
- Helpers.php:456 (versions.php)
- factory/class.Update.php: versions.php, .zip, _manifest.json, _sql.txt, _files.txt
NIE dodawaj kontekstu stream (follow_location=1) — wystarczy zmiana protokołu, bo
serwer obsługuje HTTPS bezpośrednio i bez redirectu.
NIE zmieniaj logiki parsowania, kontroli wersji, manifestów — tylko URL.
NIE ruszaj index.php:92 (`\S::get( 'hash' ) == $settings['update_key']`) — to inna sprawa.
</action>
<verify>
grep -c "http://www.cmspro.project-dc.pl" autoload/Shared/Helpers/Helpers.php autoload/admin/factory/class.Update.php
→ wynik 0:0 (zero wystąpień)
grep -c "https://www.cmspro.project-dc.pl" → wynik &gt;=1:&gt;=4
</verify>
<done>AC-1 satisfied</done>
</task>
<task type="auto">
<name>Task 2: Hotfix instancji testowej cmstest.pagedev.pl</name>
<files>
C:/visual studio code/projekty/cmstest.pagedev.pl/autoload/class.S.php,
C:/visual studio code/projekty/cmstest.pagedev.pl/autoload/admin/factory/class.Update.php
</files>
<action>
W instancji testowej zamień `http://www.cmspro.project-dc.pl``https://www.cmspro.project-dc.pl`
w obu plikach (Edit replace_all).
Pliki w instancji testowej mają STARSZĄ strukturę (przed refaktoringiem) — class.S.php
zawiera get_new_version(), class.Update.php (factory) zawiera 4 wystąpienia URL
(versions.php, .zip, _sql.txt, _files.txt — bez manifest, bez SHA256).
Po patchu trzeba poinstruować użytkownika, żeby:
- wylogował się z panelu (wyczyści sesję `new-version`),
- lub w przeglądarce otworzył dowolny link admina po wylogowaniu i ponownym zalogowaniu.
NIE modyfikuj innych plików w instancji testowej — to środowisko UAT, ma odzwierciedlać
stan produkcyjny.
</action>
<verify>
grep -c "http://www.cmspro.project-dc.pl" w obu plikach → 0:0
</verify>
<done>AC-2 satisfied (część kodowa, weryfikacja UAT w Task 8)</done>
</task>
<task type="auto">
<name>Task 3: Skrypt audit paczek aktualizacji</name>
<files>
.paul/phases/04h-hotfix-https-updates/scripts/audit-packages.ps1,
.paul/phases/04h-hotfix-https-updates/audit-report.md
</files>
<action>
Napisz PowerShell-owy skrypt audytujący wszystkie ZIP-y w `updates/` (rekurencyjnie),
który dla każdego ZIP:
1. Wymienia pliki kandydujące: `autoload/class.S.php`, `autoload/Shared/Helpers/Helpers.php`,
`autoload/admin/factory/class.Update.php` (i ich starsze warianty jak `class.Update.php`).
2. Jeśli któryś istnieje — rozpakowuje go do tempu, sprawdza zawartość `Select-String -Pattern 'http://www\.cmspro\.project-dc\.pl'`.
3. Wynik dla każdej paczki: { package, files_present, has_buggy_url, action }.
Skrypt MUSI obsłużyć też `updates/cmsPro.zip` (base install).
Skrypt NIE patchuje paczek — tylko raportuje. Patch w Task 4.
Output do `audit-report.md` jako tabela markdown sortowana wersjami.
Skrypt nie wymaga uprawnień do zapisu w paczkach.
</action>
<verify>
pwsh -File .paul/phases/04h-hotfix-https-updates/scripts/audit-packages.ps1
→ audit-report.md powstaje i zawiera wpis dla cmsPro.zip + co najmniej ver_1.518, ver_1.620
</verify>
<done>AC-5 satisfied: pełna lista dotkniętych paczek</done>
</task>
<task type="checkpoint:decision" gate="blocking">
<decision>
Czy patchować WSZYSTKIE paczki z bugiem, czy tylko baseline cmsPro.zip + ver_1.519 jako kotwica?
</decision>
<context>
Po audycie znamy listę dotkniętych paczek. Możliwe strategie:
- Patch wszystkich (pełna sanityzacja, ale każda zmieniona paczka wymaga regeneracji manifestu+checksum
i upload na serwer).
- Patch minimum (cmsPro.zip + ver_1.519 jako kotwica) — nowe instalacje od zera są OK,
a każdy upgrade dochodząc do 1.519 dostaje fix.
Decyzja zależy od liczby dotkniętych paczek z audit-report.md.
</context>
<options>
<option id="full-patch">
<name>Patch wszystkich dotkniętych paczek</name>
<pros>Spójność, brak buggy artefaktów na serwerze, każda ścieżka aktualizacji bezpieczna</pros>
<cons>Więcej pracy + uploadu, ryzyko zepsucia checksum jeśli manifesty istnieją</cons>
</option>
<option id="minimal-patch">
<name>Patch tylko cmsPro.zip + ver_1.519 (kotwica)</name>
<pros>Najmniejsza ingerencja, jasny "punkt zaczepienia"</pros>
<cons>Stare paczki nadal zawierają zepsuty kod — ryzyko regresji jeśli ktoś użyje pojedynczej paczki ręcznie</cons>
</option>
</options>
<resume-signal>Select: full-patch lub minimal-patch</resume-signal>
</task>
<task type="auto">
<name>Task 4: Patch dotkniętych paczek (zgodnie z decyzją z Task 3.5)</name>
<files>
updates/cmsPro.zip,
updates/1.50/ver_1.518.zip (jeśli buggy),
updates/1.60/ver_1.620.zip (jeśli buggy),
updates/**/ver_*.zip (każda inna z audit-report.md jeśli wybrano full-patch),
updates/**/ver_*_manifest.json (regeneracja checksum_zip dla patched paczek z manifestem)
</files>
<action>
Dla każdej paczki do patcha:
1. Skopiuj ZIP do tempu (backup)
2. Wyciągnij plik(i) zawierające http://www.cmspro.project-dc.pl
3. Zamień http:// → https:// (zachowaj BOM, encoding UTF-8 bez BOM zgodnie z konwencją projektu)
4. Wsadź spowrotem do ZIP-a (Compress-Archive -Update lub System.IO.Compression.ZipArchive)
5. Jeśli istnieje `ver_X.YYY_manifest.json` w tym samym katalogu — przelicz SHA256 całego ZIP
i zaktualizuj `checksum_zip` w manifeście (format: `sha256:HEX`)
6. Zachowaj oryginał jako `.bak` w tym samym katalogu
Dla cmsPro.zip nie ma manifestu (to base install) — sam ZIP wystarczy.
NIE ruszaj paczek, które audit zaznaczył jako N/A (brak buggy plików).
NIE zmieniaj struktury katalogów wewnątrz ZIP-ów.
</action>
<verify>
Powtórz audit z Task 3 — każda zmieniona paczka pokazuje `has_buggy_url: false`.
Dla każdej paczki z manifestem: `(Get-FileHash -Algorithm SHA256 paczka.zip).Hash.ToLower()`
równe wartości checksum_zip w manifeście.
</verify>
<done>AC-3 satisfied</done>
</task>
<task type="auto">
<name>Task 5: Wstrzyknięcie kotwicy fixa do ver_1.519.zip</name>
<files>
updates/1.50/ver_1.519.zip
</files>
<action>
ver_1.519.zip oryginalnie zawiera tylko `autoload/admin/controls/class.Articles.php`.
Dodać do niego (Compress-Archive -Update lub równoważne):
- `autoload/class.S.php` — wersja z https:// (taka sama jaka ląduje w cmsPro.zip post-patch)
- `autoload/admin/factory/class.Update.php` — wersja z https://
- `autoload/admin/controls/class.Update.php` — bez zmian (kontrola, nie ma URL)
- `autoload/admin/view/class.Update.php` — bez zmian
Pliki muszą mieć strukturę pasującą do legacy układu (PRZED refaktoringiem do Shared\Helpers),
czyli class.S.php zawiera get_new_version() inline. Skopiuj odpowiednie wersje z post-patched
`cmsPro.zip` lub z wcześniejszej paczki (np. patched ver_1.518.zip jeśli była buggy).
Cel: każda instancja cmsPRO doczołgająca się do 1.519 dostaje WORKING klient HTTPS,
nawet jeśli wcześniejsze paczki były buggy.
Jeśli istnieje ver_1.519_manifest.json — dodaj wpisy `files.added` z nowo wstrzykniętymi
plikami i przelicz checksum_zip.
Jeśli nie istnieje — opcjonalnie wygeneruj go (zgodnie ze schematem z build-update.ps1).
</action>
<verify>
unzip -l updates/1.50/ver_1.519.zip → zawiera autoload/class.S.php oraz autoload/admin/factory/class.Update.php
unzip -p updates/1.50/ver_1.519.zip autoload/class.S.php | grep -c "https://" → &gt;=1
unzip -p updates/1.50/ver_1.519.zip autoload/class.S.php | grep -c "http://www.cmspro" → 0
</verify>
<done>AC-4 satisfied</done>
</task>
<task type="auto">
<name>Task 6: Generacja listy plików do uploadu na serwer</name>
<files>
.paul/phases/04h-hotfix-https-updates/upload-checklist.md
</files>
<action>
Wygeneruj plik z listą paczek wymagających uploadu na cmspro.project-dc.pl/updates/:
- cmsPro.zip (jeśli zmieniony)
- każda zmieniona ver_*.zip wraz ze swoim manifestem (jeśli istnieje)
- ver_1.519.zip + ewentualny nowy ver_1.519_manifest.json
Format: tabela markdown { plik, ścieżka źródłowa, ścieżka docelowa na serwerze, SHA256 }.
Dodaj sekcję "Po uploadzie" z krokami: smoke-test (curl https://.../updates/versions.php?key=KLUCZ),
weryfikacja że versions.php nadal zwraca poprawną listę.
</action>
<verify>
Plik istnieje i zawiera co najmniej cmsPro.zip + ver_1.519.zip + smoke-test command
</verify>
<done>Dostarczona instrukcja uploadu dla użytkownika</done>
</task>
<task type="checkpoint:human-action" gate="blocking">
<what-built>
Zestaw spatchowanych paczek + cmsPro.zip + ver_1.519.zip kotwica fixa, gotowe do uploadu.
</what-built>
<how-to-verify>
1. Otwórz `.paul/phases/04h-hotfix-https-updates/upload-checklist.md`
2. Wgraj wskazane pliki na serwer cmspro.project-dc.pl do katalogu updates/
(FTP/SSH/panel hostingu — wedle Twojej procedury)
3. Uruchom smoke-test:
curl -sS "https://www.cmspro.project-dc.pl/updates/versions.php?key=DOWOLNY_VALID_KEY"
→ powinno zwrócić listę wersji bez 301
4. Pobierz testowo jedną z paczek:
curl -sS -o /tmp/test.zip "https://www.cmspro.project-dc.pl/updates/1.50/ver_1.519.zip"
file /tmp/test.zip → powinno być "Zip archive"
</how-to-verify>
<resume-signal>Wpisz "uploaded" gdy paczki są na serwerze, lub opisz problem</resume-signal>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Hotfix instancji testowej: pliki zaktualizowane lokalnie + paczki na serwerze.
</what-built>
<how-to-verify>
1. W instancji testowej (cmstest.pagedev.pl) wyloguj się i zaloguj ponownie do panelu
(czyści sesję `new-version`).
2. Wejdź w sekcję aktualizacji (admin → Aktualizacja).
3. Sprawdź:
- Pokazuje wersję 1.519 jako bieżącą
- Pokazuje "Dostępna nowa wersja: X.YYY" gdzie X.YYY > 1.519
- Klik "Aktualizuj" przebiega bez błędu i zwiększa wersję
4. Powtórz update kilka razy aż dojdziesz do najnowszej dostępnej wersji.
5. Sprawdź `libraries/version.ini` w instancji — odzwierciedla najnowszą wersję.
</how-to-verify>
<resume-signal>Wpisz "approved" gdy aktualizacje działają, lub opisz problem</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- index.php:92 (`\S::get('hash') == $settings['update_key']`) — to oddzielna funkcja (front-devel mode), nie URL
- Logika parsowania wersji, manifestów, SQL — tylko URL ma się zmienić
- Struktura katalogowa updates/ na serwerze — tylko zawartość plików
- Inne pliki w instancji testowej cmstest.pagedev.pl — to UAT, ma odzwierciedlać prod
- Roadmap Phase 5+ — to hotfix poza roadmapą, nie blokuje Phase 5
## SCOPE LIMITS
- Plan NIE refaktoryzuje update systemu do nowej architektury (to robota dla Phase 5/13)
- Plan NIE wprowadza retry/cURL/stream-context — wystarczy zmiana protokołu, serwer wspiera HTTPS bezpośrednio
- Plan NIE patchuje pojedynczych instancji produkcyjnych poza testową — użytkownicy końcowi dostaną fix przez kotwicę 1.519 lub baseline cmsPro.zip
- Plan NIE rozwiązuje problemu "stare instancje na <1.519 zablokowane przed migracją serwera" — wymagałyby manualnego patcha (out of scope)
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] Task 1 verify (grep w kodzie cmsPRO)
- [ ] Task 2 verify (grep w instancji testowej)
- [ ] Task 3: audit-report.md istnieje i jest kompletny
- [ ] Task 4: powtórzony audit pokazuje 0 buggy paczek (dla full-patch) lub tylko nie-patched paczki świadomie pominięte
- [ ] Task 5: ver_1.519.zip zawiera fix files
- [ ] Task 6: upload-checklist.md istnieje
- [ ] Checkpoint upload: użytkownik potwierdził upload + smoke-test OK
- [ ] Checkpoint UAT: instancja testowa aktualizuje się do najnowszej wersji bez błędu
- [ ] Wszystkie AC spełnione
</verification>
<success_criteria>
- Bieżąca instancja testowa odblokowana — widzi i instaluje aktualizacje > 1.519
- Bazowy install (cmsPro.zip) na serwerze nie zawiera buggy http://
- ver_1.519.zip jest "kotwicą fixa" dla każdej nowej instalacji
- Audit report dokumentuje stan wszystkich paczek
- Brak regresji w innych modułach (URL change to jedyna ingerencja)
</success_criteria>
<output>
Po zamknięciu utwórz `.paul/phases/04h-hotfix-https-updates/04h-01-SUMMARY.md`
zawierający:
- Co zmieniono (kod + paczki)
- Listę paczek patched z SHA256 przed/po
- Decyzję checkpoint (full-patch vs minimal-patch) i uzasadnienie
- Lessons learned (np. "zawsze używać HTTPS w endpointach update", "rozważyć stream context z follow_location jako defense-in-depth")
- Sugestię dla Phase 5/13 Releases+Update: rozważyć cURL z verify SSL i obsługą redirectów jako trwałe rozwiązanie
</output>

View File

@@ -0,0 +1,170 @@
---
phase: 04h-hotfix-https-updates
plan: 01
subsystem: infra
tags: [https, urlencode, ftp-cleanup, update-mechanism, license-key]
requires:
- phase: pre-paul (legacy update mechanism)
provides: file_get_contents-based update polling
provides:
- HTTPS-only update endpoint (kod + 121 paczek + cmsPro.zip + kotwica 1.519)
- urlencode($settings['update_key']) wrap (kod + 64 paczek + kotwica)
- Audit/patcher PowerShell scripts dla updates/ ZIP-ow
- FTP cleanup script (.bak removal lokalnie + zdalnie)
affects: [phase-5 (Releases+Update Domain repo), phase-13 (Admin: Update controller)]
tech-stack:
added: []
patterns:
- "Audit-then-patch script pair: oddzielne audyt vs mutating skrypty (idempotent verify)"
- "Anchor-package strategy: wstrzykniecie fixa w istniejaca paczke aktualizacji jako ratunkowy punkt zaczepienia"
key-files:
created:
- .paul/phases/04h-hotfix-https-updates/scripts/audit-packages.ps1
- .paul/phases/04h-hotfix-https-updates/scripts/patch-packages.ps1
- .paul/phases/04h-hotfix-https-updates/scripts/patch-urlencode.ps1
- .paul/phases/04h-hotfix-https-updates/scripts/inject-anchor-1519.ps1
- .paul/phases/04h-hotfix-https-updates/scripts/cleanup-baks.ps1
- .paul/phases/04h-hotfix-https-updates/audit-report.md
- .paul/phases/04h-hotfix-https-updates/patch-log.md
- .paul/phases/04h-hotfix-https-updates/patch-urlencode-log.md
- .paul/phases/04h-hotfix-https-updates/upload-checklist.md
modified:
- autoload/Shared/Helpers/Helpers.php
- autoload/admin/factory/class.Update.php
- updates/cmsPro.zip
- updates/**/ver_*.zip (121 paczek http->https + 64 urlencode)
- 2x ver_*_manifest.json (przeliczone checksum_zip SHA256)
key-decisions:
- "full-patch zamiast minimal-patch: 121 paczek > ryzyko regresji przy chain-update"
- "urlencode jako oddzielny patch po wykryciu bug-a #2 podczas UAT (klucz licencji z #)"
- ".NET FtpWebRequest zamiast curl --quote dla cleanup (PowerShell 5.1 traktuje stderr curl jako fatal)"
patterns-established:
- "Reverse-engineering bug w produkcji: curl probe serwera wyzwolil odkrycie HTTP 301 redirect"
- "Bug discovery podczas UAT: drugi bug (urlencode) ujawniony dopiero po naprawieniu pierwszego"
duration: ~90min
started: 2026-04-25T22:00:00Z
completed: 2026-04-26T00:35:00Z
---
# Phase 04h Plan 01: Hotfix HTTPS Update Endpoint Summary
**Naprawa zablokowanego mechanizmu aktualizacji w cmsPRO: HTTP->HTTPS w kodzie + 121 paczkach, urlencode klucza licencji w 64 paczkach, kotwica fixa w ver_1.519.zip, cleanup 1085 backupow z FTP+lokalnie. Test instance odblokowana i potwierdzona przez user-a.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~90min |
| Started | 2026-04-25 22:00 |
| Completed | 2026-04-26 00:35 |
| Tasks | 8 zaplanowane + 3 dodatkowe (urlencode patch, urlencode w paczkach, cleanup .bak) |
| Files modified | 4 PHP source + 124 paczki (cmsPro + 121 ZIP + 2 manifest) + 1085 .bak removed |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Kod cmsPRO uzywa HTTPS | Pass | Helpers.php + factory/Update.php; 0 wystapien http:// |
| AC-2: Instancja testowa odblokowana | Pass | User potwierdzil "Zadzialalo" po dodaniu urlencode |
| AC-3: Wszystkie paczki wolne od bug-a | Pass | Audit po patchu: 0 paczek z http://, 0 paczek z raw $settings['update_key'] w URL |
| AC-4: ver_1.519.zip jako kotwica fixa | Pass | Zawiera class.S.php (37958B) + class.Update.php (2886B), oba z https + urlencode. SHA256: 14e5754c75884fcc... |
| AC-5: Audit report dostarczony | Pass | audit-report.md + patch-log.md + patch-urlencode-log.md |
## Accomplishments
- **Odblokowanie WSZYSTKICH instancji cmsPRO**: kazda instancja ktora dotrze do 1.519 dostaje wbudowana kotwice z dzialajacym klientem update (https + urlencode)
- **Identyfikacja DWOCH bugow** (HTTP 301 redirect i URL fragment-cutting na #) podczas jednej sesji
- **Idempotentne skrypty audit+patch+cleanup** mozliwe do reuse gdyby pojawila sie kolejna fala buggy paczek
- **Zero regresji**: zachowane oryginalne backupy podczas patcha (.bak), usuniete dopiero po UAT confirmation
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/Shared/Helpers/Helpers.php` | Modified | http->https + urlencode w get_new_version() |
| `autoload/admin/factory/class.Update.php` | Modified | http->https + urlencode w update() |
| `cmstest.../autoload/class.S.php` | Modified | UAT instance hotfix |
| `cmstest.../autoload/admin/factory/class.Update.php` | Modified | UAT instance hotfix |
| `updates/cmsPro.zip` | Modified | Base install patched (legacy nie uzywal klucza, tylko http->https) |
| `updates/**/ver_*.zip` (121) | Modified | http->https w autoload/class.S.php / Helpers.php / factory/Update.php |
| `updates/**/ver_*.zip` (64) | Modified | urlencode wrap (subset 121) |
| `updates/1.50/ver_1.519.zip` | Modified | + anchor injection: class.S.php + factory/Update.php |
| `updates/**/ver_*_manifest.json` (2) | Modified | Przeliczony checksum_zip SHA256 |
| `.paul/phases/04h-hotfix-https-updates/scripts/*.ps1` (5) | Created | Audit, patch, anchor inject, urlencode patch, FTP cleanup |
| `.paul/phases/04h-hotfix-https-updates/*.md` (4) | Created | audit-report, patch-log, patch-urlencode-log, upload-checklist |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| full-patch wszystkich 121 paczek | Czesciowy patch ryzykowal regresje gdy stara buggy paczka nadpisywala swiezo zaktualizowany class.S.php | 124 pliki do uploadu (vs 3 minimum), ale kazda chain-update sciezka bezpieczna |
| Anchor injection do ver_1.519.zip | Oryginalna paczka (class.Articles.php only) jest "darmowa" wersja na granicy licencji - idealny punkt by gwarantowac dzialajacy klient HTTPS dla wszystkich nowych instancji | Wszystkie nowe instancje od zera dostaja fix przy upgrade do 1.519 |
| .NET FtpWebRequest zamiast curl dla cleanup | curl stderr w PowerShell 5.1 wyrzuca NativeCommandError przy 550, blokujac batch | Cleanup ukonczony bez bledow (1085 .bak) |
| urlencode jako oddzielny etap | Bug ujawniony dopiero podczas UAT (user dodal klucz z #) - zorganizowany jako "rozszerzenie planu" zamiast restart | Czysta sciezka audytowa, oba bugi udokumentowane oddzielnie |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 1 | urlencode bug discovered during UAT, dodatkowe 3 zadania (patch kodu, patch paczek, cleanup) |
| Scope additions | 1 | FTP cleanup (.bak files na serwerze) - nie planowane, user zlecil po UAT |
| Deferred | 0 | - |
**Total impact:** Plan ukonczony + 2 niezbedne rozszerzenia odkryte podczas wykonania.
### Auto-fixed Issues
**1. URL encoding klucza licencji (krytyczny bug #2)**
- **Found during:** UAT post-Task 8 (user testowal z kluczem `#e@1tUVvZDP:$7dL`)
- **Issue:** Klucz licencji wstrzykiwany raw w URL, znak `#` w kluczu byl traktowany jako fragment delimiter -> serwer dostawal pusty klucz -> zwracal wersje tylko do 1.519 (free tier)
- **Fix:** `urlencode($settings['update_key'])` w kodzie + 64 paczkach + kotwicy
- **Files:** Helpers.php, factory/Update.php (cmsPRO + test instance), 64x ver_*.zip, ver_1.519.zip
- **Verification:** PowerShell scan wszystkich plikow w paczkach: 0/165 bez urlencode wrap
### Scope Additions
**1. FTP cleanup .bak files**
- **Trigger:** User polecil usuniecie backupow po UAT confirmation
- **Discovered:** Auto-deploy ftp-kr.json (autoUpload=true, autoDelete=false) wyslal 1085 .bak na serwer
- **Action:** Skrypt cleanup-baks.ps1 (.NET FtpWebRequest) - 355 zdalnych usuniec + lokalne removal
- **Verification:** sample listing /updates/1.50/ - 0 .bak remote, 0 .bak lokalnie
### Deferred Items
None.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| PowerShell 5.1 mis-reads UTF-8 file bez BOM (audit-packages.ps1 z em-dash) | Zamieniono "—" na "--", plik czysto ASCII |
| `Substring(0,12)` na "(dry-run)" string (length 9) | Dodany length-guard if |
| curl -Q "DELE name" w katalogu wymaga CWD; bez CWD szuka w root (550) | Zmiana na pelna sciezka w DELE + URL ftp://host/ |
| curl stderr w PowerShell 5.1 = NativeCommandError przy 550 -> ErrorActionPreference Stop blokuje batch | Przepisanie na .NET FtpWebRequest z try/catch na 550 |
## Next Phase Readiness
**Ready:**
- Phase 5 (Domain: SeoAdditional + Cron + Releases) moze ruszyc bez zaleznosci od 04h
- Mechanizm update na produkcji odblokowany - nowe wersje moga byc dystrybuowane normalnie
- Skrypty audit/patch w `.paul/phases/04h-hotfix-https-updates/scripts/` dostepne jako template przy podobnych operacjach
**Concerns:**
- Mechanizm update nadal uzywa `file_get_contents()` bez stream context z verify_peer/follow_location - rekomendacja: w Phase 5 lub Phase 13 przepisac na cURL z proper error handling i SSL verification
- Klucze licencji powinny byc walidowane przy zapisie w settings (max length, allowed chars) - obecnie polegamy na urlencode jako fix
- Brak retry logic przy chwilowym network failure podczas update - obecny kod silnie failuje
**Blockers:**
- None. Phase 5 moze ruszyc.
---
*Phase: 04h-hotfix-https-updates, Plan: 01*
*Completed: 2026-04-26*

View File

@@ -0,0 +1,722 @@
# Audit Report: paczki aktualizacji cmsPRO (HTTP -> HTTPS bug)
**Data:** 2026-04-26 00:14
**Katalog:** `C:\visual studio code\projekty\cmsPRO\updates`
**Wzorzec buggy:** `http://www.cmspro.project-dc.pl`
## Podsumowanie
| Kategoria | Liczba |
|-----------|--------|
| Paczek przeskanowanych | 542 |
| **PATCH (buggy http://)** | **0** |
| OK (juz https albo brak URL) | 147 |
| N/A (brak plikow podejrzanych) | 395 |
| Bledy | 0 |
## Paczki wymagajace patcha (HasBuggyUrl=true)
_Brak paczek do patcha._
## Paczki zawierajace pliki ale juz na https (OK)
| # | Paczka | Pliki obecne |
|---|--------|--------------|
| 1 | `cmsPro.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 2 | `0.00\ver_0.03.zip` | autoload/admin/factory/class.Update.php |
| 3 | `0.00\ver_0.07.zip` | autoload/class.S.php |
| 4 | `0.10\ver_0.17.zip` | autoload/class.S.php |
| 5 | `0.10\ver_0.19.zip` | autoload/class.S.php |
| 6 | `0.20\ver_0.22.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 7 | `0.20\ver_0.26.zip` | autoload/admin/factory/class.Update.php |
| 8 | `0.30\ver_0.38.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 9 | `0.40\ver_0.47.zip` | autoload/class.S.php |
| 10 | `0.50\ver_0.54.zip` | autoload/class.S.php |
| 11 | `0.50\ver_0.55.zip` | autoload/class.S.php |
| 12 | `0.50\ver_0.59.zip` | autoload/class.S.php |
| 13 | `0.70\ver_0.71.zip` | autoload/class.S.php |
| 14 | `0.70\ver_0.73.zip` | autoload/class.S.php |
| 15 | `0.70\ver_0.77.zip` | autoload/class.S.php |
| 16 | `0.80\ver_0.82.zip` | autoload/class.S.php |
| 17 | `0.80\ver_0.83.zip` | autoload/class.S.php |
| 18 | `0.80\ver_0.84.zip` | autoload/class.S.php |
| 19 | `0.80\ver_0.86.zip` | autoload/class.S.php |
| 20 | `0.90\ver_0.90.zip` | autoload/class.S.php |
| 21 | `0.90\ver_0.93.zip` | autoload/admin/factory/class.Update.php |
| 22 | `0.90\ver_0.94.zip` | autoload/class.S.php |
| 23 | `1.00\ver_1.09.zip` | autoload/class.S.php |
| 24 | `1.10\ver_1.12.zip` | autoload/class.S.php |
| 25 | `1.20\ver_1.27.zip` | autoload/admin/factory/class.Update.php |
| 26 | `1.20\ver_1.271.zip` | autoload/admin/factory/class.Update.php |
| 27 | `1.20\ver_1.274.zip` | autoload/class.S.php |
| 28 | `1.20\ver_1.279.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 29 | `1.20\ver_1.280.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 30 | `1.20\ver_1.284.zip` | autoload/class.S.php |
| 31 | `1.20\ver_1.285.zip` | autoload/class.S.php |
| 32 | `1.20\ver_1.288.zip` | autoload/class.S.php |
| 33 | `1.20\ver_1.293.zip` | autoload/admin/factory/class.Update.php |
| 34 | `1.20\ver_1.294.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 35 | `1.20\ver_1.295.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 36 | `1.20\ver_1.297.zip` | autoload/class.S.php |
| 37 | `1.20\ver_1.298.zip` | autoload/class.S.php |
| 38 | `1.30\ver_1.300.zip` | autoload/class.S.php |
| 39 | `1.30\ver_1.306.zip` | autoload/class.S.php |
| 40 | `1.30\ver_1.308.zip` | autoload/class.S.php |
| 41 | `1.30\ver_1.318.zip` | autoload/class.S.php |
| 42 | `1.30\ver_1.323.zip` | autoload/class.S.php |
| 43 | `1.30\ver_1.326.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 44 | `1.30\ver_1.334.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 45 | `1.30\ver_1.335.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 46 | `1.30\ver_1.339.zip` | autoload/class.S.php |
| 47 | `1.30\ver_1.346.zip` | autoload/class.S.php |
| 48 | `1.30\ver_1.350.zip` | autoload/class.S.php |
| 49 | `1.30\ver_1.352.zip` | autoload/class.S.php |
| 50 | `1.30\ver_1.354.zip` | autoload/class.S.php |
| 51 | `1.30\ver_1.355.zip` | autoload/class.S.php |
| 52 | `1.30\ver_1.356.zip` | autoload/class.S.php |
| 53 | `1.30\ver_1.357.zip` | autoload/class.S.php |
| 54 | `1.30\ver_1.359.zip` | autoload/class.S.php |
| 55 | `1.30\ver_1.360.zip` | autoload/class.S.php |
| 56 | `1.30\ver_1.370.zip` | autoload/class.S.php |
| 57 | `1.30\ver_1.371.zip` | autoload/class.S.php |
| 58 | `1.30\ver_1.372.zip` | autoload/class.S.php |
| 59 | `1.30\ver_1.375.zip` | autoload/class.S.php |
| 60 | `1.30\ver_1.387.zip` | autoload/class.S.php |
| 61 | `1.30\ver_1.396.zip` | autoload/class.S.php |
| 62 | `1.30\ver_1.397.zip` | autoload/class.S.php |
| 63 | `1.40\ver_1.402.zip` | autoload/class.S.php |
| 64 | `1.40\ver_1.406.zip` | autoload/class.S.php |
| 65 | `1.40\ver_1.408.zip` | autoload/class.S.php |
| 66 | `1.40\ver_1.409.zip` | autoload/class.S.php |
| 67 | `1.40\ver_1.410.zip` | autoload/class.S.php |
| 68 | `1.40\ver_1.411.zip` | autoload/class.S.php |
| 69 | `1.40\ver_1.412.zip` | autoload/class.S.php |
| 70 | `1.40\ver_1.426.zip` | autoload/class.S.php |
| 71 | `1.40\ver_1.432.zip` | autoload/class.S.php |
| 72 | `1.40\ver_1.449.zip` | autoload/class.S.php |
| 73 | `1.40\ver_1.456.zip` | autoload/class.S.php |
| 74 | `1.40\ver_1.462.zip` | autoload/class.S.php |
| 75 | `1.40\ver_1.463.zip` | autoload/class.S.php |
| 76 | `1.40\ver_1.464.zip` | autoload/class.S.php |
| 77 | `1.40\ver_1.482.zip` | autoload/class.S.php |
| 78 | `1.40\ver_1.485.zip` | autoload/class.S.php |
| 79 | `1.40\ver_1.490.zip` | autoload/class.S.php |
| 80 | `1.40\ver_1.496.zip` | autoload/class.S.php |
| 81 | `1.40\ver_1.498.zip` | autoload/class.S.php |
| 82 | `1.50\ver_1.504.zip` | autoload/class.S.php |
| 83 | `1.50\ver_1.507.zip` | autoload/class.S.php |
| 84 | `1.50\ver_1.511.zip` | autoload/class.S.php |
| 85 | `1.50\ver_1.513.zip` | autoload/class.S.php |
| 86 | `1.50\ver_1.515.zip` | autoload/admin/factory/class.Update.php |
| 87 | `1.50\ver_1.517.zip` | autoload/admin/factory/class.Update.php |
| 88 | `1.50\ver_1.518.zip` | autoload/class.S.php |
| 89 | `1.50\ver_1.529.zip` | autoload/class.S.php |
| 90 | `1.50\ver_1.532.zip` | autoload/class.S.php |
| 91 | `1.50\ver_1.533.zip` | autoload/class.S.php |
| 92 | `1.50\ver_1.534.zip` | autoload/class.S.php |
| 93 | `1.50\ver_1.535.zip` | autoload/class.S.php |
| 94 | `1.50\ver_1.536.zip` | autoload/class.S.php |
| 95 | `1.50\ver_1.539.zip` | autoload/class.S.php |
| 96 | `1.50\ver_1.541.zip` | autoload/class.S.php |
| 97 | `1.50\ver_1.546.zip` | autoload/class.S.php |
| 98 | `1.50\ver_1.551.zip` | autoload/class.S.php |
| 99 | `1.50\ver_1.552.zip` | autoload/class.S.php |
| 100 | `1.50\ver_1.554.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 101 | `1.50\ver_1.555.zip` | autoload/class.S.php |
| 102 | `1.50\ver_1.556.zip` | autoload/class.S.php |
| 103 | `1.50\ver_1.557.zip` | autoload/class.S.php |
| 104 | `1.50\ver_1.561.zip` | autoload/class.S.php |
| 105 | `1.50\ver_1.568.zip` | autoload/class.S.php |
| 106 | `1.50\ver_1.572.zip` | autoload/class.S.php |
| 107 | `1.50\ver_1.583.zip` | autoload/class.S.php |
| 108 | `1.50\ver_1.587.zip` | autoload/class.S.php |
| 109 | `1.50\ver_1.589.zip` | autoload/class.S.php |
| 110 | `1.50\ver_1.591.zip` | autoload/class.S.php |
| 111 | `1.60\ver_1.602.zip` | autoload/class.S.php |
| 112 | `1.60\ver_1.610.zip` | autoload/class.S.php |
| 113 | `1.60\ver_1.613.zip` | autoload/class.S.php |
| 114 | `1.60\ver_1.617.zip` | autoload/class.S.php |
| 115 | `1.60\ver_1.618.zip` | autoload/class.S.php |
| 116 | `1.60\ver_1.620.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 117 | `1.60\ver_1.625.zip` | autoload/class.S.php |
| 118 | `1.60\ver_1.626.zip` | autoload/class.S.php |
| 119 | `1.60\ver_1.627.zip` | autoload/class.S.php |
| 120 | `1.60\ver_1.628.zip` | autoload/class.S.php |
| 121 | `1.60\ver_1.633.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 122 | `1.60\ver_1.634.zip` | autoload/class.S.php |
| 123 | `1.60\ver_1.638.zip` | autoload/class.S.php |
| 124 | `1.60\ver_1.642.zip` | autoload/class.S.php |
| 125 | `1.60\ver_1.643.zip` | autoload/class.S.php |
| 126 | `1.60\ver_1.644.zip` | autoload/class.S.php |
| 127 | `1.60\ver_1.645.zip` | autoload/class.S.php |
| 128 | `1.60\ver_1.649.zip` | autoload/class.S.php |
| 129 | `1.60\ver_1.650.zip` | autoload/class.S.php |
| 130 | `1.60\ver_1.651.zip` | autoload/class.S.php |
| 131 | `1.60\ver_1.654.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 132 | `1.60\ver_1.656.zip` | autoload/class.S.php |
| 133 | `1.60\ver_1.660.zip` | autoload/class.S.php |
| 134 | `1.60\ver_1.665.zip` | autoload/class.S.php |
| 135 | `1.60\ver_1.666.zip` | autoload/class.S.php |
| 136 | `1.60\ver_1.669.zip` | autoload/admin/factory/class.Update.php |
| 137 | `1.60\ver_1.674.zip` | autoload/class.S.php |
| 138 | `1.60\ver_1.675.zip` | autoload/class.S.php |
| 139 | `1.60\ver_1.678.zip` | autoload/class.S.php |
| 140 | `1.60\ver_1.680.zip` | autoload/class.S.php |
| 141 | `1.60\ver_1.681.zip` | autoload/class.S.php |
| 142 | `1.60\ver_1.684.zip` | autoload/class.S.php |
| 143 | `1.60\ver_1.687.zip` | autoload/admin/factory/class.Update.php, autoload/class.S.php |
| 144 | `1.60\ver_1.688.zip` | autoload/admin/factory/class.Update.php |
| 145 | `1.60\ver_1.690.zip` | autoload/admin/factory/class.Update.php |
| 146 | `1.60\ver_1.691.zip` | autoload/class.S.php, autoload/Shared/Helpers/Helpers.php |
| 147 | `1.60\ver_1.694.zip` | autoload/Shared/Helpers/Helpers.php |
## Bledy odczytu
_Brak._
## Pelna lista (raw)
| Paczka | FilesPresent | HasBuggyUrl | Action |
|--------|--------------|-------------|--------|
| `cmsPro.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `0.00\ver_0.01.zip` | - | False | N/A (no suspect files) |
| `0.00\ver_0.02.zip` | - | False | N/A (no suspect files) |
| `0.00\ver_0.03.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `0.00\ver_0.04.zip` | - | False | N/A (no suspect files) |
| `0.00\ver_0.05.zip` | - | False | N/A (no suspect files) |
| `0.00\ver_0.06.zip` | - | False | N/A (no suspect files) |
| `0.00\ver_0.07.zip` | autoload/class.S.php | False | OK (already https) |
| `0.00\ver_0.08.zip` | - | False | N/A (no suspect files) |
| `0.00\ver_0.09.zip` | - | False | N/A (no suspect files) |
| `0.10\ver_0.10.zip` | - | False | N/A (no suspect files) |
| `0.10\ver_0.11.zip` | - | False | N/A (no suspect files) |
| `0.10\ver_0.12.zip` | - | False | N/A (no suspect files) |
| `0.10\ver_0.13.zip` | - | False | N/A (no suspect files) |
| `0.10\ver_0.14.zip` | - | False | N/A (no suspect files) |
| `0.10\ver_0.15.zip` | - | False | N/A (no suspect files) |
| `0.10\ver_0.16.zip` | - | False | N/A (no suspect files) |
| `0.10\ver_0.17.zip` | autoload/class.S.php | False | OK (already https) |
| `0.10\ver_0.18.zip` | - | False | N/A (no suspect files) |
| `0.10\ver_0.19.zip` | autoload/class.S.php | False | OK (already https) |
| `0.20\ver_0.20.zip` | - | False | N/A (no suspect files) |
| `0.20\ver_0.21.zip` | - | False | N/A (no suspect files) |
| `0.20\ver_0.22.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `0.20\ver_0.23.zip` | - | False | N/A (no suspect files) |
| `0.20\ver_0.24.zip` | - | False | N/A (no suspect files) |
| `0.20\ver_0.25.zip` | - | False | N/A (no suspect files) |
| `0.20\ver_0.26.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `0.30\ver_0.38.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `0.30\ver_0.39.zip` | - | False | N/A (no suspect files) |
| `0.40\ver_0.40.zip` | - | False | N/A (no suspect files) |
| `0.40\ver_0.41.zip` | - | False | N/A (no suspect files) |
| `0.40\ver_0.42.zip` | - | False | N/A (no suspect files) |
| `0.40\ver_0.43.zip` | - | False | N/A (no suspect files) |
| `0.40\ver_0.44.zip` | - | False | N/A (no suspect files) |
| `0.40\ver_0.45.zip` | - | False | N/A (no suspect files) |
| `0.40\ver_0.46.zip` | - | False | N/A (no suspect files) |
| `0.40\ver_0.47.zip` | autoload/class.S.php | False | OK (already https) |
| `0.40\ver_0.48.zip` | - | False | N/A (no suspect files) |
| `0.40\ver_0.49.zip` | - | False | N/A (no suspect files) |
| `0.50\ver_0.50.zip` | - | False | N/A (no suspect files) |
| `0.50\ver_0.51.zip` | - | False | N/A (no suspect files) |
| `0.50\ver_0.52.zip` | - | False | N/A (no suspect files) |
| `0.50\ver_0.53.zip` | - | False | N/A (no suspect files) |
| `0.50\ver_0.54.zip` | autoload/class.S.php | False | OK (already https) |
| `0.50\ver_0.55.zip` | autoload/class.S.php | False | OK (already https) |
| `0.50\ver_0.56.zip` | - | False | N/A (no suspect files) |
| `0.50\ver_0.57.zip` | - | False | N/A (no suspect files) |
| `0.50\ver_0.58.zip` | - | False | N/A (no suspect files) |
| `0.50\ver_0.59.zip` | autoload/class.S.php | False | OK (already https) |
| `0.60\ver_0.60.zip` | - | False | N/A (no suspect files) |
| `0.60\ver_0.61.zip` | - | False | N/A (no suspect files) |
| `0.60\ver_0.62.zip` | - | False | N/A (no suspect files) |
| `0.60\ver_0.63.zip` | - | False | N/A (no suspect files) |
| `0.60\ver_0.64.zip` | - | False | N/A (no suspect files) |
| `0.60\ver_0.65.zip` | - | False | N/A (no suspect files) |
| `0.60\ver_0.66.zip` | - | False | N/A (no suspect files) |
| `0.60\ver_0.67.zip` | - | False | N/A (no suspect files) |
| `0.60\ver_0.68.zip` | - | False | N/A (no suspect files) |
| `0.60\ver_0.69.zip` | - | False | N/A (no suspect files) |
| `0.70\ver_0.70.zip` | - | False | N/A (no suspect files) |
| `0.70\ver_0.71.zip` | autoload/class.S.php | False | OK (already https) |
| `0.70\ver_0.72.zip` | - | False | N/A (no suspect files) |
| `0.70\ver_0.73.zip` | autoload/class.S.php | False | OK (already https) |
| `0.70\ver_0.74.zip` | - | False | N/A (no suspect files) |
| `0.70\ver_0.75.zip` | - | False | N/A (no suspect files) |
| `0.70\ver_0.76.zip` | - | False | N/A (no suspect files) |
| `0.70\ver_0.77.zip` | autoload/class.S.php | False | OK (already https) |
| `0.70\ver_0.78.zip` | - | False | N/A (no suspect files) |
| `0.70\ver_0.79.zip` | - | False | N/A (no suspect files) |
| `0.80\ver_0.80.zip` | - | False | N/A (no suspect files) |
| `0.80\ver_0.81.zip` | - | False | N/A (no suspect files) |
| `0.80\ver_0.82.zip` | autoload/class.S.php | False | OK (already https) |
| `0.80\ver_0.83.zip` | autoload/class.S.php | False | OK (already https) |
| `0.80\ver_0.84.zip` | autoload/class.S.php | False | OK (already https) |
| `0.80\ver_0.85.zip` | - | False | N/A (no suspect files) |
| `0.80\ver_0.86.zip` | autoload/class.S.php | False | OK (already https) |
| `0.80\ver_0.87.zip` | - | False | N/A (no suspect files) |
| `0.80\ver_0.88.zip` | - | False | N/A (no suspect files) |
| `0.80\ver_0.89.zip` | - | False | N/A (no suspect files) |
| `0.90\ver_0.90.zip` | autoload/class.S.php | False | OK (already https) |
| `0.90\ver_0.91.zip` | - | False | N/A (no suspect files) |
| `0.90\ver_0.92.zip` | - | False | N/A (no suspect files) |
| `0.90\ver_0.93.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `0.90\ver_0.94.zip` | autoload/class.S.php | False | OK (already https) |
| `0.90\ver_0.95.zip` | - | False | N/A (no suspect files) |
| `0.90\ver_0.96.zip` | - | False | N/A (no suspect files) |
| `0.90\ver_0.97.zip` | - | False | N/A (no suspect files) |
| `0.90\ver_0.98.zip` | - | False | N/A (no suspect files) |
| `0.90\ver_0.99.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.00.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.01.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.02.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.03.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.04.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.05.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.06.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.07.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.08.zip` | - | False | N/A (no suspect files) |
| `1.00\ver_1.09.zip` | autoload/class.S.php | False | OK (already https) |
| `1.10\ver_1.10.zip` | - | False | N/A (no suspect files) |
| `1.10\ver_1.11.zip` | - | False | N/A (no suspect files) |
| `1.10\ver_1.12.zip` | autoload/class.S.php | False | OK (already https) |
| `1.10\ver_1.13.zip` | - | False | N/A (no suspect files) |
| `1.10\ver_1.14.zip` | - | False | N/A (no suspect files) |
| `1.10\ver_1.15.zip` | - | False | N/A (no suspect files) |
| `1.10\ver_1.16.zip` | - | False | N/A (no suspect files) |
| `1.10\ver_1.17.zip` | - | False | N/A (no suspect files) |
| `1.10\ver_1.18.zip` | - | False | N/A (no suspect files) |
| `1.10\ver_1.19.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.20.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.21.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.22.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.23.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.24.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.25.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.26.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.27.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `1.20\ver_1.271.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `1.20\ver_1.272.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.273.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.274.zip` | autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.275.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.276.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.277.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.278.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.279.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.280.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.281.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.282.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.283.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.284.zip` | autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.285.zip` | autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.286.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.287.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.288.zip` | autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.289.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.290.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.291.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.292.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.293.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `1.20\ver_1.294.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.295.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.296.zip` | - | False | N/A (no suspect files) |
| `1.20\ver_1.297.zip` | autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.298.zip` | autoload/class.S.php | False | OK (already https) |
| `1.20\ver_1.299.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.300.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.301.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.302.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.303.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.304.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.305.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.306.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.307.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.308.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.309.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.310.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.311.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.312.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.313.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.314.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.315.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.316.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.317.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.318.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.319.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.320.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.321.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.322.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.323.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.324.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.325.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.326.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.327.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.328.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.329.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.330.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.331.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.332.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.333.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.334.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.335.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.336.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.337.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.338.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.339.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.340.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.341.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.342.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.343.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.344.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.345.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.346.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.347.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.348.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.349.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.350.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.351.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.352.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.353.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.354.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.355.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.356.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.357.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.358.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.359.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.360.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.361.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.362.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.363.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.364.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.365.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.366.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.367.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.368.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.369.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.370.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.371.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.372.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.373.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.374.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.375.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.376.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.377.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.378.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.379.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.380.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.381.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.382.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.383.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.384.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.385.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.386.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.387.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.388.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.389.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.390.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.391.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.392.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.393.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.394.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.395.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.396.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.397.zip` | autoload/class.S.php | False | OK (already https) |
| `1.30\ver_1.398.zip` | - | False | N/A (no suspect files) |
| `1.30\ver_1.399.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.400.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.401.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.402.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.403.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.404.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.405.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.406.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.407.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.408.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.409.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.410.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.411.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.412.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.413.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.414.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.415.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.416.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.417.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.418.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.419.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.420.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.421.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.422.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.423.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.424.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.425.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.426.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.427.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.428.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.429.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.430.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.431.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.432.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.433.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.434.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.435.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.436.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.437.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.438.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.439.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.440.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.441.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.442.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.443.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.444.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.445.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.446.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.447.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.448.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.449.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.450.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.451.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.452.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.453.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.454.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.455.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.456.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.457.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.458.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.459.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.460.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.461.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.462.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.463.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.464.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.465.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.466.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.467.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.468.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.469.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.470.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.471.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.472.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.473.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.474.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.475.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.476.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.477.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.478.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.479.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.480.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.481.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.482.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.483.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.484.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.485.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.486.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.487.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.488.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.489.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.490.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.491.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.492.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.493.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.494.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.495.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.496.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.497.zip` | - | False | N/A (no suspect files) |
| `1.40\ver_1.498.zip` | autoload/class.S.php | False | OK (already https) |
| `1.40\ver_1.499.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.500.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.501.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.502.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.503.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.504.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.505.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.506.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.507.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.508.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.509.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.510.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.511.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.512.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.513.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.514.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.515.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `1.50\ver_1.516.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.517.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `1.50\ver_1.518.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.519.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.520.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.521.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.522.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.523.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.524.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.525.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.526.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.527.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.528.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.529.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.530.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.531.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.532.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.533.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.534.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.535.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.536.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.537.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.538.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.539.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.540.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.541.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.542.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.543.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.544.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.545.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.546.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.547.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.548.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.549.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.550.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.551.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.552.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.553.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.554.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.555.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.556.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.557.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.558.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.559.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.560.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.561.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.562.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.563.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.564.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.565.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.566.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.567.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.568.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.569.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.570.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.571.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.572.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.573.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.574.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.575.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.576.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.577.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.578.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.579.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.580.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.581.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.582.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.583.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.584.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.585.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.586.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.587.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.588.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.589.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.590.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.591.zip` | autoload/class.S.php | False | OK (already https) |
| `1.50\ver_1.592.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.593.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.594.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.595.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.596.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.597.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.598.zip` | - | False | N/A (no suspect files) |
| `1.50\ver_1.599.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.600.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.601.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.602.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.603.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.604.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.605.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.606.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.607.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.608.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.609.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.610.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.611.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.612.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.613.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.614.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.615.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.616.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.617.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.618.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.619.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.620.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.621.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.622.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.623.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.624.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.625.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.626.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.627.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.628.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.629.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.630.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.631.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.632.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.633.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.634.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.635.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.636.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.637.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.638.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.639.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.640.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.641.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.642.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.643.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.644.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.645.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.646.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.647.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.648.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.649.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.650.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.651.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.652.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.653.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.654.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.655.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.656.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.657.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.658.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.659.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.660.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.661.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.662.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.663.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.664.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.665.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.666.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.667.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.668.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.669.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `1.60\ver_1.670.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.671.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.672.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.673.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.674.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.675.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.676.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.677.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.678.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.679.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.680.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.681.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.682.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.683.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.684.zip` | autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.685.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.686.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.687.zip` | autoload/admin/factory/class.Update.php; autoload/class.S.php | False | OK (already https) |
| `1.60\ver_1.688.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `1.60\ver_1.689.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.690.zip` | autoload/admin/factory/class.Update.php | False | OK (already https) |
| `1.60\ver_1.691.zip` | autoload/class.S.php; autoload/Shared/Helpers/Helpers.php | False | OK (already https) |
| `1.60\ver_1.692.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.693.zip` | - | False | N/A (no suspect files) |
| `1.60\ver_1.694.zip` | autoload/Shared/Helpers/Helpers.php | False | OK (already https) |
| `1.60\ver_1.695.zip` | - | False | N/A (no suspect files) |

View File

@@ -0,0 +1,144 @@
# Patch Log: paczki aktualizacji cmsPRO (HTTP -> HTTPS)
**Data:** 2026-04-26 00:14
**DryRun:** False
## Podsumowanie
| Kategoria | Liczba |
|-----------|--------|
| Paczek przeskanowanych | 542 |
| **Spatchowanych** | **121** |
| Pominietych (juz OK / brak buggy) | 421 |
| Manifestow zaktualizowanych | 2 |
| Bledow | 0 |
## Spatchowane paczki
| # | Paczka | Pliki | SHA256 (przed) | SHA256 (po) | Manifest |
|---|--------|-------|----------------|-------------|----------|
| 1 | `cmsPro.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `04bae6b928e3` | `34f3efaee333` | - |
| 2 | `1.20\ver_1.279.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `383c5ffa3d54` | `4b2fb3dfc445` | - |
| 3 | `1.20\ver_1.280.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `383c5ffa3d54` | `4b2fb3dfc445` | - |
| 4 | `1.20\ver_1.284.zip` | autoload/class.S.php | `341663d2657e` | `bec02c70641f` | - |
| 5 | `1.20\ver_1.285.zip` | autoload/class.S.php | `ec1a21ebe839` | `bd3d14f0db85` | - |
| 6 | `1.20\ver_1.288.zip` | autoload/class.S.php | `f436e004c786` | `b1e5485ce02b` | - |
| 7 | `1.20\ver_1.293.zip` | autoload/admin/factory/class.Update.php | `5d252576ec2b` | `a90a58d4eac0` | - |
| 8 | `1.20\ver_1.294.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `6ca587ed550b` | `dcbfc2f89315` | - |
| 9 | `1.20\ver_1.295.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `6ca587ed550b` | `dcbfc2f89315` | - |
| 10 | `1.20\ver_1.297.zip` | autoload/class.S.php | `a16402bfd885` | `9adf6ec503b6` | - |
| 11 | `1.20\ver_1.298.zip` | autoload/class.S.php | `30a2c3850523` | `dafec72f64cc` | - |
| 12 | `1.30\ver_1.300.zip` | autoload/class.S.php | `c2244b07f69b` | `59bafbe0d389` | - |
| 13 | `1.30\ver_1.306.zip` | autoload/class.S.php | `1c2278ca6c69` | `ff5978122aae` | - |
| 14 | `1.30\ver_1.308.zip` | autoload/class.S.php | `a50998bb71ac` | `20c441d164fc` | - |
| 15 | `1.30\ver_1.318.zip` | autoload/class.S.php | `1563bb506a27` | `9497bf555b14` | - |
| 16 | `1.30\ver_1.323.zip` | autoload/class.S.php | `3066c4d5bbc4` | `068f67dae09b` | - |
| 17 | `1.30\ver_1.326.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `87a17ea013b8` | `24e06f56613d` | - |
| 18 | `1.30\ver_1.334.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `739aa23ec849` | `a7beadc4e1f5` | - |
| 19 | `1.30\ver_1.335.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `6ce01894f1ad` | `d7fdcd6ac697` | - |
| 20 | `1.30\ver_1.339.zip` | autoload/class.S.php | `04fbfdc032bc` | `afb31fa769a6` | - |
| 21 | `1.30\ver_1.346.zip` | autoload/class.S.php | `de601e22fd37` | `9cb19d96f645` | - |
| 22 | `1.30\ver_1.350.zip` | autoload/class.S.php | `957051eaa2e8` | `4c10a14b2765` | - |
| 23 | `1.30\ver_1.352.zip` | autoload/class.S.php | `18c85499de9f` | `a6a675bb9df9` | - |
| 24 | `1.30\ver_1.354.zip` | autoload/class.S.php | `10636d8b1a5d` | `4871da20d1a4` | - |
| 25 | `1.30\ver_1.355.zip` | autoload/class.S.php | `c20d8850537e` | `778996580d18` | - |
| 26 | `1.30\ver_1.356.zip` | autoload/class.S.php | `f046ae370ae7` | `2b18edb393c3` | - |
| 27 | `1.30\ver_1.357.zip` | autoload/class.S.php | `6a64787b2145` | `8119cc274771` | - |
| 28 | `1.30\ver_1.359.zip` | autoload/class.S.php | `4fad5c6aabf7` | `a279ab6d1d01` | - |
| 29 | `1.30\ver_1.360.zip` | autoload/class.S.php | `4fad5c6aabf7` | `a279ab6d1d01` | - |
| 30 | `1.30\ver_1.370.zip` | autoload/class.S.php | `d0352704b455` | `f0495f6a5bb2` | - |
| 31 | `1.30\ver_1.371.zip` | autoload/class.S.php | `9fa1a7d972dc` | `40acbb4d8daa` | - |
| 32 | `1.30\ver_1.372.zip` | autoload/class.S.php | `daa6036ab2f0` | `f67eb93cd71e` | - |
| 33 | `1.30\ver_1.375.zip` | autoload/class.S.php | `8166a9d6f04b` | `47664cb7b525` | - |
| 34 | `1.30\ver_1.387.zip` | autoload/class.S.php | `61ee5812a27f` | `bd7e98161c4c` | - |
| 35 | `1.30\ver_1.396.zip` | autoload/class.S.php | `6eb93b7df885` | `5fd970139fcf` | - |
| 36 | `1.30\ver_1.397.zip` | autoload/class.S.php | `56127b22eca8` | `ceb27382c399` | - |
| 37 | `1.40\ver_1.402.zip` | autoload/class.S.php | `d8b25871e93d` | `52b24d0bb654` | - |
| 38 | `1.40\ver_1.406.zip` | autoload/class.S.php | `d7b27ca558b4` | `a56ab8fd41ca` | - |
| 39 | `1.40\ver_1.408.zip` | autoload/class.S.php | `302fc65269a4` | `16f159ead1ec` | - |
| 40 | `1.40\ver_1.409.zip` | autoload/class.S.php | `df802724890d` | `3e067a83eaa3` | - |
| 41 | `1.40\ver_1.410.zip` | autoload/class.S.php | `f4be5e2e0999` | `f5b8263df2e0` | - |
| 42 | `1.40\ver_1.411.zip` | autoload/class.S.php | `bd62c4c04ff4` | `6f7b3e395294` | - |
| 43 | `1.40\ver_1.412.zip` | autoload/class.S.php | `167b2313fa75` | `f3afa5ff7b13` | - |
| 44 | `1.40\ver_1.426.zip` | autoload/class.S.php | `c6a5ac5b6555` | `e37dfc765c9b` | - |
| 45 | `1.40\ver_1.432.zip` | autoload/class.S.php | `4395bf30ba05` | `9b92326416e1` | - |
| 46 | `1.40\ver_1.449.zip` | autoload/class.S.php | `b29b8684b667` | `c90420d409df` | - |
| 47 | `1.40\ver_1.456.zip` | autoload/class.S.php | `b0ca8b1e84b2` | `6b571e185eff` | - |
| 48 | `1.40\ver_1.462.zip` | autoload/class.S.php | `031403360a00` | `2a2aa3da9ca6` | - |
| 49 | `1.40\ver_1.463.zip` | autoload/class.S.php | `01f728b96367` | `ce73866bc9ec` | - |
| 50 | `1.40\ver_1.464.zip` | autoload/class.S.php | `d0bfa1257815` | `a63b24622550` | - |
| 51 | `1.40\ver_1.482.zip` | autoload/class.S.php | `54257f3166e7` | `14d264b0b98d` | - |
| 52 | `1.40\ver_1.485.zip` | autoload/class.S.php | `c038736f6415` | `e9deb7665bfe` | - |
| 53 | `1.40\ver_1.490.zip` | autoload/class.S.php | `6d7d8dff3272` | `93b467cf6aba` | - |
| 54 | `1.40\ver_1.496.zip` | autoload/class.S.php | `c8561f2bfc73` | `61d06089ad5a` | - |
| 55 | `1.40\ver_1.498.zip` | autoload/class.S.php | `220174971e2a` | `cbe13607769e` | - |
| 56 | `1.50\ver_1.504.zip` | autoload/class.S.php | `a136d6d17452` | `73d34dd29ec7` | - |
| 57 | `1.50\ver_1.507.zip` | autoload/class.S.php | `92138e1cbf75` | `859b77852121` | - |
| 58 | `1.50\ver_1.511.zip` | autoload/class.S.php | `404dc62348c5` | `9b7efaa0fc8a` | - |
| 59 | `1.50\ver_1.513.zip` | autoload/class.S.php | `25aa0ad5b615` | `3adc785ae512` | - |
| 60 | `1.50\ver_1.515.zip` | autoload/admin/factory/class.Update.php | `61031c1265d2` | `06b3ea74f6a6` | - |
| 61 | `1.50\ver_1.517.zip` | autoload/admin/factory/class.Update.php | `f5d3ab1991a0` | `2c253235eb13` | - |
| 62 | `1.50\ver_1.518.zip` | autoload/class.S.php | `edb21793a950` | `ea8f832ca9b8` | - |
| 63 | `1.50\ver_1.529.zip` | autoload/class.S.php | `ef2ec535dc95` | `1bfc6ee162c0` | - |
| 64 | `1.50\ver_1.532.zip` | autoload/class.S.php | `2254ab1050ea` | `b1f5a3400238` | - |
| 65 | `1.50\ver_1.533.zip` | autoload/class.S.php | `6474410a3c98` | `50a8d5ab9f3e` | - |
| 66 | `1.50\ver_1.534.zip` | autoload/class.S.php | `d0c5b337504d` | `615a96b461eb` | - |
| 67 | `1.50\ver_1.535.zip` | autoload/class.S.php | `a8b92c86e805` | `9f0b57ca61e0` | - |
| 68 | `1.50\ver_1.536.zip` | autoload/class.S.php | `41bf5f8732f0` | `dbb21c2ad437` | - |
| 69 | `1.50\ver_1.539.zip` | autoload/class.S.php | `a3ebe014dfe0` | `f6e47e6e0222` | - |
| 70 | `1.50\ver_1.541.zip` | autoload/class.S.php | `55e86b6ffc1d` | `339e6478b788` | - |
| 71 | `1.50\ver_1.546.zip` | autoload/class.S.php | `8a9609fcfe06` | `a4004fc40564` | - |
| 72 | `1.50\ver_1.551.zip` | autoload/class.S.php | `2fa830f98b5c` | `f2a194a21f6f` | - |
| 73 | `1.50\ver_1.552.zip` | autoload/class.S.php | `8f5f0cb91f2c` | `02b36e5b9337` | - |
| 74 | `1.50\ver_1.554.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `c891ab97735a` | `c1e8e006b29b` | - |
| 75 | `1.50\ver_1.555.zip` | autoload/class.S.php | `90528256f87f` | `2e6766d04fe7` | - |
| 76 | `1.50\ver_1.556.zip` | autoload/class.S.php | `09fe7fc6d4a2` | `575168eccdcf` | - |
| 77 | `1.50\ver_1.557.zip` | autoload/class.S.php | `0fba22ceb174` | `e306fcd762bb` | - |
| 78 | `1.50\ver_1.561.zip` | autoload/class.S.php | `c89b99f8baab` | `404e55549823` | - |
| 79 | `1.50\ver_1.568.zip` | autoload/class.S.php | `19aef1df0e1d` | `d767239d552f` | - |
| 80 | `1.50\ver_1.572.zip` | autoload/class.S.php | `c80d3cfeea79` | `bc3fcfab57a2` | - |
| 81 | `1.50\ver_1.583.zip` | autoload/class.S.php | `24d62f53257e` | `c391e2ad9b33` | - |
| 82 | `1.50\ver_1.587.zip` | autoload/class.S.php | `04d93d40b294` | `aa29c628ff35` | - |
| 83 | `1.50\ver_1.589.zip` | autoload/class.S.php | `2c60a2500e99` | `7edb90603631` | - |
| 84 | `1.50\ver_1.591.zip` | autoload/class.S.php | `05b8f2d1eac4` | `fb5efec03d4a` | - |
| 85 | `1.60\ver_1.602.zip` | autoload/class.S.php | `30812c674805` | `1209aed9a2e6` | - |
| 86 | `1.60\ver_1.610.zip` | autoload/class.S.php | `b0a1d536f259` | `a43d167c6e94` | - |
| 87 | `1.60\ver_1.613.zip` | autoload/class.S.php | `722ed3ab1c42` | `6ed29eb56f96` | - |
| 88 | `1.60\ver_1.617.zip` | autoload/class.S.php | `286f0ac05dd5` | `b9c84e970579` | - |
| 89 | `1.60\ver_1.618.zip` | autoload/class.S.php | `c403d23083cd` | `2864ad9815ce` | - |
| 90 | `1.60\ver_1.620.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `61552b13a991` | `e06a9ea5cf48` | - |
| 91 | `1.60\ver_1.625.zip` | autoload/class.S.php | `a9d90461abbf` | `60ac3e95fe76` | - |
| 92 | `1.60\ver_1.626.zip` | autoload/class.S.php | `ae75632e7acf` | `947e9bc3ec7d` | - |
| 93 | `1.60\ver_1.627.zip` | autoload/class.S.php | `b4078fe9f7d9` | `222c2f3c0d00` | - |
| 94 | `1.60\ver_1.628.zip` | autoload/class.S.php | `fb3bcb641657` | `4c8c937aa428` | - |
| 95 | `1.60\ver_1.633.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `5fc3f3855729` | `f2820c8d647f` | - |
| 96 | `1.60\ver_1.634.zip` | autoload/class.S.php | `2af4b2e2980c` | `3ec716159037` | - |
| 97 | `1.60\ver_1.638.zip` | autoload/class.S.php | `38f87bb189f9` | `7679e1fdc953` | - |
| 98 | `1.60\ver_1.642.zip` | autoload/class.S.php | `b2921f4dd4ab` | `b28d236d6074` | - |
| 99 | `1.60\ver_1.643.zip` | autoload/class.S.php | `08f45c6fe81e` | `3dbcda086ed9` | - |
| 100 | `1.60\ver_1.644.zip` | autoload/class.S.php | `6255d181f388` | `0ea76c426355` | - |
| 101 | `1.60\ver_1.645.zip` | autoload/class.S.php | `6fd50691508f` | `e83b7996da93` | - |
| 102 | `1.60\ver_1.649.zip` | autoload/class.S.php | `936220b76848` | `dc9ffe727576` | - |
| 103 | `1.60\ver_1.650.zip` | autoload/class.S.php | `f4717adef625` | `99886a7c717f` | - |
| 104 | `1.60\ver_1.651.zip` | autoload/class.S.php | `8f92a38ba890` | `93a8b5d3e364` | - |
| 105 | `1.60\ver_1.654.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `d458e3940db5` | `a85ffcab6cda` | - |
| 106 | `1.60\ver_1.656.zip` | autoload/class.S.php | `14c29245a067` | `429b2eee4eec` | - |
| 107 | `1.60\ver_1.660.zip` | autoload/class.S.php | `17dedcc2f029` | `7dca670a5179` | - |
| 108 | `1.60\ver_1.665.zip` | autoload/class.S.php | `2f955790e96b` | `624f1f30213b` | - |
| 109 | `1.60\ver_1.666.zip` | autoload/class.S.php | `9eafa83e3310` | `3e40380c947c` | - |
| 110 | `1.60\ver_1.669.zip` | autoload/admin/factory/class.Update.php | `d2e905b19371` | `b794aca0108b` | - |
| 111 | `1.60\ver_1.674.zip` | autoload/class.S.php | `57e5907e2abc` | `d7c2d02ba4aa` | - |
| 112 | `1.60\ver_1.675.zip` | autoload/class.S.php | `8bfff26ed53e` | `70ee0f5eb299` | - |
| 113 | `1.60\ver_1.678.zip` | autoload/class.S.php | `393b70b31c29` | `445c5b6acf4a` | - |
| 114 | `1.60\ver_1.680.zip` | autoload/class.S.php | `1e8b8e2fc307` | `e4213bf9cc5b` | - |
| 115 | `1.60\ver_1.681.zip` | autoload/class.S.php | `882732e2a479` | `3040f999a7d6` | - |
| 116 | `1.60\ver_1.684.zip` | autoload/class.S.php | `20d02da8a749` | `7232b2d5dc38` | - |
| 117 | `1.60\ver_1.687.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `915dea5e4498` | `fcd36fb50246` | - |
| 118 | `1.60\ver_1.688.zip` | autoload/admin/factory/class.Update.php | `97333e68443c` | `89c7a6cbcbd7` | - |
| 119 | `1.60\ver_1.690.zip` | autoload/admin/factory/class.Update.php | `5852ca60388d` | `df006d6da6ce` | - |
| 120 | `1.60\ver_1.691.zip` | autoload/Shared/Helpers/Helpers.php | `f53230f36d39` | `5a0087c09a5c` | YES |
| 121 | `1.60\ver_1.694.zip` | autoload/Shared/Helpers/Helpers.php | `a21dc4a768bc` | `523351891882` | YES |
## Bledy
_Brak._

View File

@@ -0,0 +1,76 @@
# Patch Log: urlencode($settings[update_key])
**Data:** 2026-04-26 00:22
Spatchowanych: 64
Pominietych: 478
Manifestow zaktualizowanych: 2
Bledow: 0
## Spatchowane
| # | Paczka | Pliki | SHA256 (po) |
|---|--------|-------|-------------|
| 1 | `1.50\ver_1.513.zip` | autoload/class.S.php | `18ec985d1b29a945` |
| 2 | `1.50\ver_1.515.zip` | autoload/admin/factory/class.Update.php | `7ff25bed81fe7767` |
| 3 | `1.50\ver_1.517.zip` | autoload/admin/factory/class.Update.php | `6b448734200893a5` |
| 4 | `1.50\ver_1.518.zip` | autoload/class.S.php | `d50e62f31c56384b` |
| 5 | `1.50\ver_1.519.zip` | autoload/class.S.php<br>autoload/admin/factory/class.Update.php | `14e5754c75884fcc` |
| 6 | `1.50\ver_1.529.zip` | autoload/class.S.php | `a5846aca666e0d5d` |
| 7 | `1.50\ver_1.532.zip` | autoload/class.S.php | `e8dd85b2449f84c0` |
| 8 | `1.50\ver_1.533.zip` | autoload/class.S.php | `f8a25cf6ba08f2be` |
| 9 | `1.50\ver_1.534.zip` | autoload/class.S.php | `4dca134b68ee82a2` |
| 10 | `1.50\ver_1.535.zip` | autoload/class.S.php | `b5365d4c100e37ab` |
| 11 | `1.50\ver_1.536.zip` | autoload/class.S.php | `b70902dfb4060d7a` |
| 12 | `1.50\ver_1.539.zip` | autoload/class.S.php | `6d29304b96132ebd` |
| 13 | `1.50\ver_1.541.zip` | autoload/class.S.php | `cff7f6080a57ed66` |
| 14 | `1.50\ver_1.546.zip` | autoload/class.S.php | `9b60fae7f31e5df2` |
| 15 | `1.50\ver_1.551.zip` | autoload/class.S.php | `f8ee40fa330d68da` |
| 16 | `1.50\ver_1.552.zip` | autoload/class.S.php | `ad203f8df17e2d10` |
| 17 | `1.50\ver_1.554.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `18ebfa82dce8a823` |
| 18 | `1.50\ver_1.555.zip` | autoload/class.S.php | `f61a10c36b50b36b` |
| 19 | `1.50\ver_1.556.zip` | autoload/class.S.php | `e4b0d3cd835a3657` |
| 20 | `1.50\ver_1.557.zip` | autoload/class.S.php | `8002b8508c1b9b14` |
| 21 | `1.50\ver_1.561.zip` | autoload/class.S.php | `9be69833265da2c8` |
| 22 | `1.50\ver_1.568.zip` | autoload/class.S.php | `dd90fc869058e5a7` |
| 23 | `1.50\ver_1.572.zip` | autoload/class.S.php | `eb0071b94e6342b6` |
| 24 | `1.50\ver_1.583.zip` | autoload/class.S.php | `faf7037812cb1345` |
| 25 | `1.50\ver_1.587.zip` | autoload/class.S.php | `a000fc230209cad4` |
| 26 | `1.50\ver_1.589.zip` | autoload/class.S.php | `23ec9d2ac905f868` |
| 27 | `1.50\ver_1.591.zip` | autoload/class.S.php | `1a69cbc0d87d1f02` |
| 28 | `1.60\ver_1.602.zip` | autoload/class.S.php | `b6bf83583aea4c1c` |
| 29 | `1.60\ver_1.610.zip` | autoload/class.S.php | `a2eb640e5660e51e` |
| 30 | `1.60\ver_1.613.zip` | autoload/class.S.php | `d580047296a40951` |
| 31 | `1.60\ver_1.617.zip` | autoload/class.S.php | `e4b9d2ecd6b62101` |
| 32 | `1.60\ver_1.618.zip` | autoload/class.S.php | `da0dccd5d8129f36` |
| 33 | `1.60\ver_1.620.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `87388a9b597048e0` |
| 34 | `1.60\ver_1.625.zip` | autoload/class.S.php | `af0005fea825b432` |
| 35 | `1.60\ver_1.626.zip` | autoload/class.S.php | `cf084b0f8da8fb34` |
| 36 | `1.60\ver_1.627.zip` | autoload/class.S.php | `2602fedd545a2b84` |
| 37 | `1.60\ver_1.628.zip` | autoload/class.S.php | `33d618d0d5979394` |
| 38 | `1.60\ver_1.633.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `6199f98c0153f2cb` |
| 39 | `1.60\ver_1.634.zip` | autoload/class.S.php | `3356e9fdff7ee6fd` |
| 40 | `1.60\ver_1.638.zip` | autoload/class.S.php | `e436f5498d9eac82` |
| 41 | `1.60\ver_1.642.zip` | autoload/class.S.php | `73db0af058c4dd8e` |
| 42 | `1.60\ver_1.643.zip` | autoload/class.S.php | `5dd7505e5e9b69f9` |
| 43 | `1.60\ver_1.644.zip` | autoload/class.S.php | `3bbf16d10e6101a6` |
| 44 | `1.60\ver_1.645.zip` | autoload/class.S.php | `dc7554805828db03` |
| 45 | `1.60\ver_1.649.zip` | autoload/class.S.php | `a34f456bb22bb49a` |
| 46 | `1.60\ver_1.650.zip` | autoload/class.S.php | `1fded4e332087edd` |
| 47 | `1.60\ver_1.651.zip` | autoload/class.S.php | `974d9c1ed41db26c` |
| 48 | `1.60\ver_1.654.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `23c1a14906b34e2c` |
| 49 | `1.60\ver_1.656.zip` | autoload/class.S.php | `e249d78fedc6d3b2` |
| 50 | `1.60\ver_1.660.zip` | autoload/class.S.php | `7068186d0fde2b1e` |
| 51 | `1.60\ver_1.665.zip` | autoload/class.S.php | `aa1fb3977f5f941c` |
| 52 | `1.60\ver_1.666.zip` | autoload/class.S.php | `437e09dfda4f3341` |
| 53 | `1.60\ver_1.669.zip` | autoload/admin/factory/class.Update.php | `1885535d8ab2ce8c` |
| 54 | `1.60\ver_1.674.zip` | autoload/class.S.php | `9fca9ce185a525b8` |
| 55 | `1.60\ver_1.675.zip` | autoload/class.S.php | `4aad98f5cfb2aa45` |
| 56 | `1.60\ver_1.678.zip` | autoload/class.S.php | `3541cf1cde4deeae` |
| 57 | `1.60\ver_1.680.zip` | autoload/class.S.php | `9a26b92b885b6e9d` |
| 58 | `1.60\ver_1.681.zip` | autoload/class.S.php | `8fb40bb9e3169797` |
| 59 | `1.60\ver_1.684.zip` | autoload/class.S.php | `0ee052905077606a` |
| 60 | `1.60\ver_1.687.zip` | autoload/admin/factory/class.Update.php<br>autoload/class.S.php | `106e9ac25acf7ed1` |
| 61 | `1.60\ver_1.688.zip` | autoload/admin/factory/class.Update.php | `e77a529410dc519a` |
| 62 | `1.60\ver_1.690.zip` | autoload/admin/factory/class.Update.php | `045d16e46da68af4` |
| 63 | `1.60\ver_1.691.zip` | autoload/Shared/Helpers/Helpers.php | `5e53b1fbf6c5c923` |
| 64 | `1.60\ver_1.694.zip` | autoload/Shared/Helpers/Helpers.php | `cbd51178b1caf4d8` |

View File

@@ -0,0 +1,169 @@
<#
.SYNOPSIS
Audit wszystkich paczek aktualizacji cmsPRO pod katem buggy http:// URL.
.DESCRIPTION
Skanuje rekursywnie kazdy ZIP w updates/ (cmsPro.zip oraz updates/**/ver_*.zip).
Dla kazdego ZIP-a sprawdza czy zawiera pliki kandydujace
(autoload/class.S.php, autoload/Shared/Helpers/Helpers.php,
autoload/admin/factory/class.Update.php) i czy te pliki zawieraja
ciag 'http://www.cmspro.project-dc.pl'.
Output: audit-report.md (tabela markdown sortowana po wersji).
Skrypt NIE modyfikuje paczek.
#>
[CmdletBinding()]
param(
[string]$UpdatesDir = "C:\visual studio code\projekty\cmsPRO\updates",
[string]$ReportPath = "C:\visual studio code\projekty\cmsPRO\.paul\phases\04h-hotfix-https-updates\audit-report.md"
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$BuggyPattern = 'http://www.cmspro.project-dc.pl'
$SuspectFiles = @(
'autoload/class.S.php',
'autoload/Shared/Helpers/Helpers.php',
'autoload/admin/factory/class.Update.php'
)
function Test-ZipForBug {
param([string]$ZipPath)
$result = [PSCustomObject]@{
Package = (Resolve-Path $ZipPath).Path.Substring($UpdatesDir.Length).TrimStart('\','/')
FilesPresent = @()
BuggyFiles = @()
HasBuggyUrl = $false
Action = 'N/A'
Error = $null
}
try {
$zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
foreach ($entry in $zip.Entries) {
$name = $entry.FullName.Replace('\','/')
if ($SuspectFiles -contains $name) {
$result.FilesPresent += $name
$reader = New-Object System.IO.StreamReader($entry.Open())
$content = $reader.ReadToEnd()
$reader.Close()
if ($content -match [regex]::Escape($BuggyPattern)) {
$result.BuggyFiles += $name
$result.HasBuggyUrl = $true
}
}
}
$zip.Dispose()
} catch {
$result.Error = $_.Exception.Message
}
if ($result.HasBuggyUrl) { $result.Action = 'PATCH' }
elseif ($result.FilesPresent) { $result.Action = 'OK (already https)' }
else { $result.Action = 'N/A (no suspect files)' }
return $result
}
Write-Host "Skanuje $UpdatesDir ..." -ForegroundColor Cyan
$zips = @()
$baseInstall = Join-Path $UpdatesDir 'cmsPro.zip'
if (Test-Path $baseInstall) { $zips += $baseInstall }
$zips += Get-ChildItem -Path $UpdatesDir -Filter 'ver_*.zip' -Recurse | Sort-Object FullName | ForEach-Object { $_.FullName }
Write-Host "Znaleziono $($zips.Count) paczek." -ForegroundColor Cyan
$results = @()
$i = 0
foreach ($zip in $zips) {
$i++
Write-Progress -Activity "Audit paczek" -Status "$i / $($zips.Count): $(Split-Path $zip -Leaf)" -PercentComplete (($i / $zips.Count) * 100)
$results += Test-ZipForBug -ZipPath $zip
}
Write-Progress -Activity "Audit paczek" -Completed
# --- Raport ---
$buggy = $results | Where-Object { $_.HasBuggyUrl }
$ok = $results | Where-Object { $_.FilesPresent -and -not $_.HasBuggyUrl }
$none = $results | Where-Object { -not $_.FilesPresent }
$errored = $results | Where-Object { $_.Error }
$lines = @()
$lines += '# Audit Report: paczki aktualizacji cmsPRO (HTTP -> HTTPS bug)'
$lines += ''
$lines += "**Data:** $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
$lines += "**Katalog:** ``$UpdatesDir``"
$lines += "**Wzorzec buggy:** ``$BuggyPattern``"
$lines += ''
$lines += '## Podsumowanie'
$lines += ''
$lines += "| Kategoria | Liczba |"
$lines += "|-----------|--------|"
$lines += "| Paczek przeskanowanych | $($results.Count) |"
$lines += "| **PATCH (buggy http://)** | **$($buggy.Count)** |"
$lines += "| OK (juz https albo brak URL) | $($ok.Count) |"
$lines += "| N/A (brak plikow podejrzanych) | $($none.Count) |"
$lines += "| Bledy | $($errored.Count) |"
$lines += ''
$lines += '## Paczki wymagajace patcha (HasBuggyUrl=true)'
$lines += ''
if ($buggy) {
$lines += '| # | Paczka | Buggy pliki |'
$lines += '|---|--------|-------------|'
$idx = 0
foreach ($r in $buggy) {
$idx++
$lines += "| $idx | ``$($r.Package)`` | $($r.BuggyFiles -join ', ') |"
}
} else {
$lines += '_Brak paczek do patcha._'
}
$lines += ''
$lines += '## Paczki zawierajace pliki ale juz na https (OK)'
$lines += ''
if ($ok) {
$lines += '| # | Paczka | Pliki obecne |'
$lines += '|---|--------|--------------|'
$idx = 0
foreach ($r in $ok) {
$idx++
$lines += "| $idx | ``$($r.Package)`` | $($r.FilesPresent -join ', ') |"
}
} else {
$lines += '_Brak._'
}
$lines += ''
$lines += '## Bledy odczytu'
$lines += ''
if ($errored) {
foreach ($r in $errored) {
$lines += "- ``$($r.Package)`` -- $($r.Error)"
}
} else {
$lines += '_Brak._'
}
$lines += ''
$lines += '## Pelna lista (raw)'
$lines += ''
$lines += '| Paczka | FilesPresent | HasBuggyUrl | Action |'
$lines += '|--------|--------------|-------------|--------|'
foreach ($r in $results) {
$files = if ($r.FilesPresent) { ($r.FilesPresent -join '; ') } else { '-' }
$lines += "| ``$($r.Package)`` | $files | $($r.HasBuggyUrl) | $($r.Action) |"
}
$utf8 = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($ReportPath, ($lines -join "`r`n"), $utf8)
Write-Host ""
Write-Host "Audit zakonczony." -ForegroundColor Green
Write-Host " Paczek: $($results.Count)"
Write-Host " Do patcha: $($buggy.Count)" -ForegroundColor Yellow
Write-Host " OK: $($ok.Count)"
Write-Host " N/A: $($none.Count)"
Write-Host " Bledy: $($errored.Count)"
Write-Host "Raport: $ReportPath" -ForegroundColor Cyan

View File

@@ -0,0 +1,88 @@
<#
.SYNOPSIS
Usuniecie wszystkich .bak / .preurlencode.bak / .preanchor.bak z FTP serwera i lokalnie.
Uzywa .NET FtpWebRequest (szybsze + lepsze error handling niz curl).
#>
[CmdletBinding()]
param(
[string]$LocalDir = "C:\visual studio code\projekty\cmsPRO\updates",
[string]$RemoteRoot = "/public_html/updates",
[string]$FtpHost = "host117523.hostido.net.pl",
[string]$FtpUser = "www@cmspro.project-dc.pl",
[string]$FtpPass = "aNDCvhA6cnHSQfM24vUE",
[switch]$DryRun
)
$ErrorActionPreference = "Continue"
$baks = Get-ChildItem $LocalDir -Recurse -File | Where-Object { $_.Name -like '*.bak' } | Sort-Object FullName
Write-Host "Plikow do skasowania: $($baks.Count)"
if ($DryRun) { Write-Host "DRY-RUN" -ForegroundColor Yellow }
$cred = New-Object System.Net.NetworkCredential($FtpUser, $FtpPass)
$deleted = 0
$notFound = 0
$failed = 0
$failedList = @()
$i = 0
function Delete-Ftp {
param([string]$RemotePath)
$url = "ftp://${FtpHost}${RemotePath}"
$req = [System.Net.FtpWebRequest]::Create($url)
$req.Method = [System.Net.WebRequestMethods+Ftp]::DeleteFile
$req.Credentials = $cred
$req.UsePassive = $true
$req.UseBinary = $true
$req.KeepAlive = $false
$req.Timeout = 30000
try {
$resp = $req.GetResponse()
$resp.Close()
return @{ ok = $true; status = 'deleted' }
} catch [System.Net.WebException] {
$msg = $_.Exception.Message
if ($msg -match '550' -or $msg -match 'No such file') {
return @{ ok = $true; status = 'not-found' }
}
return @{ ok = $false; status = 'error'; msg = $msg }
}
}
foreach ($f in $baks) {
$i++
$rel = $f.FullName.Substring($LocalDir.Length).TrimStart('\','/').Replace('\','/')
$remoteFull = "$RemoteRoot/$rel"
if (($i % 50) -eq 0) {
Write-Host " $i / $($baks.Count) ... (deleted=$deleted notFound=$notFound failed=$failed)"
}
if ($DryRun) {
Write-Host " [DRY] DELE $remoteFull"
continue
}
$r = Delete-Ftp -RemotePath $remoteFull
if ($r.ok) {
if ($r.status -eq 'deleted') { $deleted++ } else { $notFound++ }
Remove-Item -LiteralPath $f.FullName -Force
} else {
$failed++
$failedList += "$rel -- $($r.msg)"
}
}
Write-Host ""
Write-Host "Zakonczono." -ForegroundColor Green
Write-Host " Usuniete (FTP+lokal): $deleted"
Write-Host " Brak na serwerze (lokal usuniete): $notFound"
Write-Host " Bledy: $failed"
if ($failedList) {
Write-Host "Pierwsze 10 bledow:"
$failedList | Select-Object -First 10 | ForEach-Object { Write-Host " $_" }
$logPath = "C:\visual studio code\projekty\cmsPRO\.paul\phases\04h-hotfix-https-updates\cleanup-errors.log"
$failedList | Out-File -FilePath $logPath -Encoding utf8
Write-Host "Pelny log bledow: $logPath"
}

View File

@@ -0,0 +1,132 @@
<#
.SYNOPSIS
Wstrzykuje "kotwice fixa" do ver_1.519.zip.
.DESCRIPTION
ver_1.519.zip oryginalnie zawiera tylko class.Articles.php.
Dodaje do niego:
- autoload/class.S.php z ver_1.518.zip (juz patched, https://)
- autoload/admin/factory/class.Update.php z ver_1.517.zip (juz patched, https://)
Cel: kazda nowa instalacja cmsPRO przechodzaca update do 1.519 dostaje
klient HTTPS dla mechanizmu wykrywania nowych wersji.
#>
[CmdletBinding()]
param(
[string]$UpdatesDir = "C:\visual studio code\projekty\cmsPRO\updates"
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$Target = Join-Path $UpdatesDir '1.50\ver_1.519.zip'
$SrcS = Join-Path $UpdatesDir '1.50\ver_1.518.zip'
$SrcUpd = Join-Path $UpdatesDir '1.50\ver_1.517.zip'
# Backup target (oddzielny od standardowego .bak po patch-packages.ps1)
$bak = "$Target.preanchor.bak"
if (-not (Test-Path $bak)) {
Copy-Item -LiteralPath $Target -Destination $bak -Force
Write-Host "Backup utworzony: $bak" -ForegroundColor Cyan
}
function Read-EntryBytes {
param([string]$ZipPath, [string]$EntryName)
$zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
try {
$entry = $zip.Entries | Where-Object { $_.FullName.Replace('\','/') -eq $EntryName } | Select-Object -First 1
if (-not $entry) { throw "Entry not found: $EntryName in $ZipPath" }
$stream = $entry.Open()
$ms = New-Object System.IO.MemoryStream
$stream.CopyTo($ms)
$stream.Close()
return $ms.ToArray()
} finally {
$zip.Dispose()
}
}
function Add-EntryFromBytes {
param([string]$ZipPath, [string]$EntryName, [byte[]]$Content)
$zip = [System.IO.Compression.ZipFile]::Open($ZipPath, [System.IO.Compression.ZipArchiveMode]::Update)
try {
# remove existing if present
$existing = $zip.Entries | Where-Object { $_.FullName.Replace('\','/') -eq $EntryName } | Select-Object -First 1
if ($existing) { $existing.Delete() }
$newEntry = $zip.CreateEntry($EntryName, [System.IO.Compression.CompressionLevel]::Optimal)
$stream = $newEntry.Open()
$stream.Write($Content, 0, $Content.Length)
$stream.Close()
} finally {
$zip.Dispose()
}
}
# 1. Read source files
Write-Host "Czytam autoload/class.S.php z ver_1.518.zip..." -ForegroundColor Cyan
$bytesS = Read-EntryBytes -ZipPath $SrcS -EntryName 'autoload/class.S.php'
Write-Host " -> $($bytesS.Length) bytes"
Write-Host "Czytam autoload/admin/factory/class.Update.php z ver_1.517.zip..." -ForegroundColor Cyan
$bytesUpd = Read-EntryBytes -ZipPath $SrcUpd -EntryName 'autoload/admin/factory/class.Update.php'
Write-Host " -> $($bytesUpd.Length) bytes"
# 2. Verify they contain https:// (sanity check)
$strS = [System.Text.Encoding]::UTF8.GetString($bytesS)
$strUpd = [System.Text.Encoding]::UTF8.GetString($bytesUpd)
if ($strS -match 'http://www\.cmspro\.project-dc\.pl' -or $strS -notmatch 'https://www\.cmspro\.project-dc\.pl') {
throw "class.S.php z ver_1.518.zip nie jest patched (nadal ma http:// lub brak URL)"
}
if ($strUpd -match 'http://www\.cmspro\.project-dc\.pl' -or $strUpd -notmatch 'https://www\.cmspro\.project-dc\.pl') {
throw "class.Update.php z ver_1.517.zip nie jest patched"
}
Write-Host "Sanity check OK: oba pliki maja https://" -ForegroundColor Green
# 3. Inject into ver_1.519.zip
Write-Host "Wstrzykuje do ver_1.519.zip..." -ForegroundColor Cyan
Add-EntryFromBytes -ZipPath $Target -EntryName 'autoload/class.S.php' -Content $bytesS
Add-EntryFromBytes -ZipPath $Target -EntryName 'autoload/admin/factory/class.Update.php' -Content $bytesUpd
# 4. Verify
Write-Host "Weryfikacja..." -ForegroundColor Cyan
$zip = [System.IO.Compression.ZipFile]::OpenRead($Target)
$entries = $zip.Entries | ForEach-Object { $_.FullName.Replace('\','/') }
$zip.Dispose()
$ok = $true
foreach ($req in @('autoload/class.S.php', 'autoload/admin/factory/class.Update.php', 'autoload/admin/controls/class.Articles.php')) {
if ($entries -contains $req) {
Write-Host " [OK] $req"
} else {
Write-Host " [MISSING] $req" -ForegroundColor Red
$ok = $false
}
}
# Re-check no http://
$rezip = [System.IO.Compression.ZipFile]::OpenRead($Target)
foreach ($e in $rezip.Entries) {
$n = $e.FullName.Replace('\','/')
if ($n -in @('autoload/class.S.php', 'autoload/admin/factory/class.Update.php')) {
$r = New-Object System.IO.StreamReader($e.Open())
$c = $r.ReadToEnd(); $r.Close()
if ($c -match 'http://www\.cmspro\.project-dc\.pl') {
Write-Host " [BUGGY] $n nadal ma http://" -ForegroundColor Red
$ok = $false
} else {
Write-Host " [HTTPS] $n czyste" -ForegroundColor Green
}
}
}
$rezip.Dispose()
if ($ok) {
$sha = (Get-FileHash -Algorithm SHA256 -Path $Target).Hash.ToLower()
Write-Host ""
Write-Host "ver_1.519.zip jest teraz kotwica fixa." -ForegroundColor Green
Write-Host " SHA256: $sha"
} else {
Write-Host "Wystapily problemy - sprawdz reczny." -ForegroundColor Red
exit 1
}

View File

@@ -0,0 +1,237 @@
<#
.SYNOPSIS
Patch wszystkich paczek aktualizacji cmsPRO: zamiana http:// -> https:// w buggy plikach.
.DESCRIPTION
Skanuje rekursywnie kazdy ZIP w updates/. Dla kazdego ZIP-a zawierajacego
pliki kandydujace z buggy URL:
1. Robi backup .bak (raz, nie nadpisuje istniejacego .bak)
2. Otwiera ZIP w trybie Update
3. Dla kazdego buggy pliku: czyta zawartosc, zamienia http -> https, zapisuje
4. Jesli istnieje sasiadujacy manifest *_manifest.json: przelicza SHA256 i aktualizuje checksum_zip
5. Loguje akcje
Output: patch-log.md (lista zmienionych paczek z SHA256 przed/po).
#>
[CmdletBinding()]
param(
[string]$UpdatesDir = "C:\visual studio code\projekty\cmsPRO\updates",
[string]$LogPath = "C:\visual studio code\projekty\cmsPRO\.paul\phases\04h-hotfix-https-updates\patch-log.md",
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$BuggyPattern = 'http://www.cmspro.project-dc.pl'
$FixedPattern = 'https://www.cmspro.project-dc.pl'
$SuspectFiles = @(
'autoload/class.S.php',
'autoload/Shared/Helpers/Helpers.php',
'autoload/admin/factory/class.Update.php'
)
function Get-Sha256 {
param([string]$Path)
return (Get-FileHash -Algorithm SHA256 -Path $Path).Hash.ToLower()
}
function Patch-Zip {
param([string]$ZipPath)
$log = [PSCustomObject]@{
Package = (Resolve-Path $ZipPath).Path.Substring($UpdatesDir.Length).TrimStart('\','/')
Sha256Before = $null
Sha256After = $null
PatchedFiles = @()
ManifestUpd = $false
Skipped = $false
Error = $null
}
# backup
$bak = "$ZipPath.bak"
if (-not (Test-Path $bak)) {
Copy-Item -LiteralPath $ZipPath -Destination $bak -Force
}
$log.Sha256Before = Get-Sha256 -Path $ZipPath
# quick check: does any suspect file have buggy URL?
$needsPatch = $false
$entriesToPatch = @()
try {
$zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
foreach ($entry in $zip.Entries) {
$name = $entry.FullName.Replace('\','/')
if ($SuspectFiles -contains $name) {
$reader = New-Object System.IO.StreamReader($entry.Open())
$content = $reader.ReadToEnd()
$reader.Close()
if ($content.Contains($BuggyPattern)) {
$needsPatch = $true
$entriesToPatch += $name
}
}
}
$zip.Dispose()
} catch {
$log.Error = "OpenRead: $($_.Exception.Message)"
return $log
}
if (-not $needsPatch) {
$log.Skipped = $true
$log.Sha256After = $log.Sha256Before
return $log
}
if ($DryRun) {
$log.PatchedFiles = $entriesToPatch
$log.Sha256After = '(dry-run)'
return $log
}
# Patch in-place
try {
$zipUpd = [System.IO.Compression.ZipFile]::Open($ZipPath, [System.IO.Compression.ZipArchiveMode]::Update)
foreach ($name in $entriesToPatch) {
$entry = $zipUpd.Entries | Where-Object { $_.FullName.Replace('\','/') -eq $name } | Select-Object -First 1
if (-not $entry) { continue }
# Read
$stream = $entry.Open()
$reader = New-Object System.IO.StreamReader($stream)
$content = $reader.ReadToEnd()
$reader.Close()
$stream.Close()
# Replace
$newContent = $content.Replace($BuggyPattern, $FixedPattern)
if ($newContent -eq $content) { continue }
# Write back: open writable stream and truncate
$writeStream = $entry.Open()
$writeStream.SetLength(0)
$writer = New-Object System.IO.StreamWriter($writeStream, (New-Object System.Text.UTF8Encoding $false))
$writer.Write($newContent)
$writer.Flush()
$writer.Close()
$writeStream.Close()
$log.PatchedFiles += $name
}
$zipUpd.Dispose()
} catch {
$log.Error = "Patch: $($_.Exception.Message)"
return $log
}
$log.Sha256After = Get-Sha256 -Path $ZipPath
# Update manifest if exists
$verBase = [System.IO.Path]::GetFileNameWithoutExtension($ZipPath) # e.g. ver_1.519
$manifestPath = Join-Path (Split-Path $ZipPath -Parent) "${verBase}_manifest.json"
if (Test-Path $manifestPath) {
try {
$jsonRaw = [System.IO.File]::ReadAllText($manifestPath)
# Strip BOM
if ($jsonRaw.Length -gt 0 -and $jsonRaw[0] -eq [char]0xFEFF) {
$jsonRaw = $jsonRaw.Substring(1)
}
$manifest = $jsonRaw | ConvertFrom-Json
if ($manifest.PSObject.Properties.Name -contains 'checksum_zip') {
$manifest.checksum_zip = "sha256:$($log.Sha256After)"
$newJson = $manifest | ConvertTo-Json -Depth 20
$utf8 = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($manifestPath, $newJson, $utf8)
$log.ManifestUpd = $true
}
} catch {
$log.Error = "Manifest: $($_.Exception.Message)"
}
}
return $log
}
# --- Main ---
Write-Host "Skanuje $UpdatesDir ..." -ForegroundColor Cyan
$zips = @()
$baseInstall = Join-Path $UpdatesDir 'cmsPro.zip'
if (Test-Path $baseInstall) { $zips += $baseInstall }
$zips += Get-ChildItem -Path $UpdatesDir -Filter 'ver_*.zip' -Recurse | Sort-Object FullName | ForEach-Object { $_.FullName }
Write-Host "Paczek do sprawdzenia: $($zips.Count)" -ForegroundColor Cyan
if ($DryRun) { Write-Host "DRY-RUN: nic nie bedzie zmienione" -ForegroundColor Yellow }
$results = @()
$i = 0
foreach ($zip in $zips) {
$i++
Write-Progress -Activity "Patch paczek" -Status "$i / $($zips.Count): $(Split-Path $zip -Leaf)" -PercentComplete (($i / $zips.Count) * 100)
$results += Patch-Zip -ZipPath $zip
}
Write-Progress -Activity "Patch paczek" -Completed
$patched = $results | Where-Object { $_.PatchedFiles.Count -gt 0 -and -not $_.Error }
$skipped = $results | Where-Object { $_.Skipped }
$errored = $results | Where-Object { $_.Error }
$manifUpd = $results | Where-Object { $_.ManifestUpd }
# Log report
$lines = @()
$lines += '# Patch Log: paczki aktualizacji cmsPRO (HTTP -> HTTPS)'
$lines += ''
$lines += "**Data:** $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
$lines += "**DryRun:** $DryRun"
$lines += ''
$lines += '## Podsumowanie'
$lines += ''
$lines += "| Kategoria | Liczba |"
$lines += "|-----------|--------|"
$lines += "| Paczek przeskanowanych | $($results.Count) |"
$lines += "| **Spatchowanych** | **$($patched.Count)** |"
$lines += "| Pominietych (juz OK / brak buggy) | $($skipped.Count) |"
$lines += "| Manifestow zaktualizowanych | $($manifUpd.Count) |"
$lines += "| Bledow | $($errored.Count) |"
$lines += ''
$lines += '## Spatchowane paczki'
$lines += ''
if ($patched) {
$lines += '| # | Paczka | Pliki | SHA256 (przed) | SHA256 (po) | Manifest |'
$lines += '|---|--------|-------|----------------|-------------|----------|'
$idx = 0
foreach ($r in $patched) {
$idx++
$files = ($r.PatchedFiles -join '<br>')
$man = if ($r.ManifestUpd) { 'YES' } else { '-' }
$shaBefore = if ($r.Sha256Before.Length -ge 12) { $r.Sha256Before.Substring(0,12) } else { $r.Sha256Before }
$shaAfter = if ($r.Sha256After.Length -ge 12) { $r.Sha256After.Substring(0,12) } else { $r.Sha256After }
$lines += "| $idx | ``$($r.Package)`` | $files | ``$shaBefore`` | ``$shaAfter`` | $man |"
}
} else {
$lines += '_Brak._'
}
$lines += ''
$lines += '## Bledy'
$lines += ''
if ($errored) {
foreach ($r in $errored) {
$lines += "- ``$($r.Package)`` -- $($r.Error)"
}
} else {
$lines += '_Brak._'
}
$utf8 = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($LogPath, ($lines -join "`r`n"), $utf8)
Write-Host ""
Write-Host "Patch zakonczony." -ForegroundColor Green
Write-Host " Spatchowanych: $($patched.Count)" -ForegroundColor Yellow
Write-Host " Manifestow updated: $($manifUpd.Count)"
Write-Host " Pominietych: $($skipped.Count)"
Write-Host " Bledow: $($errored.Count)"
Write-Host "Log: $LogPath" -ForegroundColor Cyan

View File

@@ -0,0 +1,227 @@
<#
.SYNOPSIS
Patch wszystkich paczek aktualizacji: dodanie urlencode() na update_key w URL.
.DESCRIPTION
Po wczesniejszym patchu http->https, klucz licencji nadal wysylany jako raw string.
Klucze zawierajace # @ : $ etc. lamia URL. Fix: urlencode($settings['update_key']).
Wzorce zamiany (oba warianty pliku class.S.php legacy oraz Helpers.php nowy):
1. ?key=' . $settings['update_key']) -> ?key=' . urlencode($settings['update_key']))
2. ?key=' . $settings['update_key'] ) -> ?key=' . urlencode( $settings['update_key'] ) )
Idempotentny: pomija pliki ktore juz maja urlencode().
#>
[CmdletBinding()]
param(
[string]$UpdatesDir = "C:\visual studio code\projekty\cmsPRO\updates",
[string]$LogPath = "C:\visual studio code\projekty\cmsPRO\.paul\phases\04h-hotfix-https-updates\patch-urlencode-log.md"
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$SuspectFiles = @(
'autoload/class.S.php',
'autoload/Shared/Helpers/Helpers.php',
'autoload/admin/factory/class.Update.php'
)
# Zwraca $true jesli content zawiera buggy pattern (raw $settings['update_key'] w URL)
function Test-NeedsUrlencode {
param([string]$Content)
# already has urlencode wrapping update_key?
if ($Content -match "urlencode\s*\(\s*\`$settings\[\s*'update_key'\s*\]\s*\)") {
# check if there's still any RAW occurrence
# Strategy: count raw vs encoded occurrences
$raw = ([regex]::Matches($Content, "\`$settings\[\s*'update_key'\s*\]")).Count
$enc = ([regex]::Matches($Content, "urlencode\s*\(\s*\`$settings\[\s*'update_key'\s*\]\s*\)")).Count
return ($raw -gt $enc) # any raw not yet wrapped
}
return ($Content -match "key=['""]?\s*\.\s*\`$settings\[\s*'update_key'\s*\]")
}
function Apply-UrlencodePatch {
param([string]$Content)
# Two flavors of whitespace: with and without spaces inside brackets
# Replacement strategy: find $settings['update_key'] preceded by something containing ?key= and . and replace with urlencode wrap
# Safest: match the full "?key=' . $settings['update_key']" with optional spaces/quotes/closing ).
# Variant A: ?key=' . $settings['update_key'] (no trailing space, may have ) or ; after)
$newContent = [regex]::Replace($Content,
"(\?key=['""]?\s*\.\s*)(\`$settings\[\s*'update_key'\s*\])",
'$1urlencode($2)'
)
return $newContent
}
function Patch-Zip {
param([string]$ZipPath)
$log = [PSCustomObject]@{
Package = (Resolve-Path $ZipPath).Path.Substring($UpdatesDir.Length).TrimStart('\','/')
Sha256Before = $null
Sha256After = $null
PatchedFiles = @()
ManifestUpd = $false
Skipped = $false
Error = $null
}
# backup (separate from previous .bak)
$bak = "$ZipPath.preurlencode.bak"
if (-not (Test-Path $bak)) {
Copy-Item -LiteralPath $ZipPath -Destination $bak -Force
}
$log.Sha256Before = (Get-FileHash -Algorithm SHA256 -Path $ZipPath).Hash.ToLower()
# Find entries needing patch
$entriesToPatch = @()
try {
$zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
foreach ($entry in $zip.Entries) {
$name = $entry.FullName.Replace('\','/')
if ($SuspectFiles -contains $name) {
$reader = New-Object System.IO.StreamReader($entry.Open())
$content = $reader.ReadToEnd()
$reader.Close()
if (Test-NeedsUrlencode -Content $content) {
$entriesToPatch += $name
}
}
}
$zip.Dispose()
} catch {
$log.Error = "OpenRead: $($_.Exception.Message)"
return $log
}
if (-not $entriesToPatch) {
$log.Skipped = $true
$log.Sha256After = $log.Sha256Before
return $log
}
# Patch in-place
try {
$zipUpd = [System.IO.Compression.ZipFile]::Open($ZipPath, [System.IO.Compression.ZipArchiveMode]::Update)
foreach ($name in $entriesToPatch) {
$entry = $zipUpd.Entries | Where-Object { $_.FullName.Replace('\','/') -eq $name } | Select-Object -First 1
if (-not $entry) { continue }
$stream = $entry.Open()
$reader = New-Object System.IO.StreamReader($stream)
$content = $reader.ReadToEnd()
$reader.Close()
$stream.Close()
$newContent = Apply-UrlencodePatch -Content $content
if ($newContent -eq $content) { continue }
$writeStream = $entry.Open()
$writeStream.SetLength(0)
$writer = New-Object System.IO.StreamWriter($writeStream, (New-Object System.Text.UTF8Encoding $false))
$writer.Write($newContent)
$writer.Flush()
$writer.Close()
$writeStream.Close()
$log.PatchedFiles += $name
}
$zipUpd.Dispose()
} catch {
$log.Error = "Patch: $($_.Exception.Message)"
return $log
}
$log.Sha256After = (Get-FileHash -Algorithm SHA256 -Path $ZipPath).Hash.ToLower()
# Update manifest
$verBase = [System.IO.Path]::GetFileNameWithoutExtension($ZipPath)
$manifestPath = Join-Path (Split-Path $ZipPath -Parent) "${verBase}_manifest.json"
if (Test-Path $manifestPath) {
try {
$jsonRaw = [System.IO.File]::ReadAllText($manifestPath)
if ($jsonRaw.Length -gt 0 -and $jsonRaw[0] -eq [char]0xFEFF) {
$jsonRaw = $jsonRaw.Substring(1)
}
$manifest = $jsonRaw | ConvertFrom-Json
if ($manifest.PSObject.Properties.Name -contains 'checksum_zip') {
$manifest.checksum_zip = "sha256:$($log.Sha256After)"
$newJson = $manifest | ConvertTo-Json -Depth 20
$utf8 = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($manifestPath, $newJson, $utf8)
$log.ManifestUpd = $true
}
} catch {
$log.Error = "Manifest: $($_.Exception.Message)"
}
}
return $log
}
# --- Main ---
Write-Host "Skanuje $UpdatesDir ..." -ForegroundColor Cyan
$zips = @()
$baseInstall = Join-Path $UpdatesDir 'cmsPro.zip'
if (Test-Path $baseInstall) { $zips += $baseInstall }
$zips += Get-ChildItem -Path $UpdatesDir -Filter 'ver_*.zip' -Recurse | Sort-Object FullName | ForEach-Object { $_.FullName }
Write-Host "Paczek do sprawdzenia: $($zips.Count)" -ForegroundColor Cyan
$results = @()
$i = 0
foreach ($zip in $zips) {
$i++
Write-Progress -Activity "Patch urlencode" -Status "$i / $($zips.Count)" -PercentComplete (($i / $zips.Count) * 100)
$results += Patch-Zip -ZipPath $zip
}
Write-Progress -Activity "Patch urlencode" -Completed
$patched = $results | Where-Object { $_.PatchedFiles.Count -gt 0 -and -not $_.Error }
$skipped = $results | Where-Object { $_.Skipped }
$errored = $results | Where-Object { $_.Error }
$manifUpd = $results | Where-Object { $_.ManifestUpd }
# Log
$lines = @()
$lines += '# Patch Log: urlencode($settings[update_key])'
$lines += ''
$lines += "**Data:** $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
$lines += ''
$lines += "Spatchowanych: $($patched.Count)"
$lines += "Pominietych: $($skipped.Count)"
$lines += "Manifestow zaktualizowanych: $($manifUpd.Count)"
$lines += "Bledow: $($errored.Count)"
$lines += ''
$lines += '## Spatchowane'
if ($patched) {
$lines += '| # | Paczka | Pliki | SHA256 (po) |'
$lines += '|---|--------|-------|-------------|'
$idx = 0
foreach ($r in $patched) {
$idx++
$files = ($r.PatchedFiles -join '<br>')
$sha = if ($r.Sha256After.Length -ge 16) { $r.Sha256After.Substring(0,16) } else { $r.Sha256After }
$lines += "| $idx | ``$($r.Package)`` | $files | ``$sha`` |"
}
}
$lines += ''
if ($errored) {
$lines += '## Bledy'
foreach ($r in $errored) { $lines += "- ``$($r.Package)`` -- $($r.Error)" }
}
$utf8 = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($LogPath, ($lines -join "`r`n"), $utf8)
Write-Host ""
Write-Host "Patch urlencode zakonczony." -ForegroundColor Green
Write-Host " Spatchowanych: $($patched.Count)" -ForegroundColor Yellow
Write-Host " Manifestow updated: $($manifUpd.Count)"
Write-Host " Pominietych: $($skipped.Count)"
Write-Host " Bledow: $($errored.Count)"
Write-Host "Log: $LogPath" -ForegroundColor Cyan

View File

@@ -0,0 +1,158 @@
# Upload Checklist v2: paczki cmsPRO po hotfixie HTTPS + urlencode
**Data:** 2026-04-26 00:24
**Liczba plikow:** 124
## Co zostalo zmienione w stosunku do checklist v1
- Dodatkowy patch: `urlencode($settings[update_key])` we wszystkich plikach uzywajacych klucza w URL
- Powod: klucze licencji moga zawierac znaki specjalne (#, @, :, $, &), ktore lamia URL bez encodingu
- 64 paczki dotalismy ponownie w tym etapie (oprocz 121 z etapu http->https)
- Niektore paczki maja podwojny patch -> nowy SHA256 -> upload ponowny
- ver_1.519.zip (kotwica) tez zostal zaktualizowany (urlencode w wstrzyknietych plikach)
## Pliki do uploadu na cmspro.project-dc.pl
| # | Plik | Sciezka docelowa | SHA256 | Powod |
|---|------|------------------|--------|-------|
| 1 | `cmsPro.zip` | `/updates/cmsPro.zip` | `34f3efaee3333e04...` | http->https |
| 2 | `ver_1.279.zip` | `/updates/1.20/ver_1.279.zip` | `4b2fb3dfc4452621...` | urlencode |
| 3 | `ver_1.280.zip` | `/updates/1.20/ver_1.280.zip` | `4b2fb3dfc4452621...` | urlencode |
| 4 | `ver_1.284.zip` | `/updates/1.20/ver_1.284.zip` | `bec02c70641fd188...` | urlencode |
| 5 | `ver_1.285.zip` | `/updates/1.20/ver_1.285.zip` | `bd3d14f0db85f06a...` | urlencode |
| 6 | `ver_1.288.zip` | `/updates/1.20/ver_1.288.zip` | `b1e5485ce02b074c...` | urlencode |
| 7 | `ver_1.293.zip` | `/updates/1.20/ver_1.293.zip` | `a90a58d4eac0e466...` | urlencode |
| 8 | `ver_1.294.zip` | `/updates/1.20/ver_1.294.zip` | `dcbfc2f893151138...` | urlencode |
| 9 | `ver_1.295.zip` | `/updates/1.20/ver_1.295.zip` | `dcbfc2f893151138...` | urlencode |
| 10 | `ver_1.297.zip` | `/updates/1.20/ver_1.297.zip` | `9adf6ec503b6051f...` | urlencode |
| 11 | `ver_1.298.zip` | `/updates/1.20/ver_1.298.zip` | `dafec72f64cc9eed...` | urlencode |
| 12 | `ver_1.300.zip` | `/updates/1.30/ver_1.300.zip` | `59bafbe0d3891a7d...` | urlencode |
| 13 | `ver_1.306.zip` | `/updates/1.30/ver_1.306.zip` | `ff5978122aaee370...` | urlencode |
| 14 | `ver_1.308.zip` | `/updates/1.30/ver_1.308.zip` | `20c441d164fcd036...` | urlencode |
| 15 | `ver_1.318.zip` | `/updates/1.30/ver_1.318.zip` | `9497bf555b14003a...` | urlencode |
| 16 | `ver_1.323.zip` | `/updates/1.30/ver_1.323.zip` | `068f67dae09b458a...` | urlencode |
| 17 | `ver_1.326.zip` | `/updates/1.30/ver_1.326.zip` | `24e06f56613dd17d...` | urlencode |
| 18 | `ver_1.334.zip` | `/updates/1.30/ver_1.334.zip` | `a7beadc4e1f5d390...` | urlencode |
| 19 | `ver_1.335.zip` | `/updates/1.30/ver_1.335.zip` | `d7fdcd6ac69768b0...` | urlencode |
| 20 | `ver_1.339.zip` | `/updates/1.30/ver_1.339.zip` | `afb31fa769a6da47...` | urlencode |
| 21 | `ver_1.346.zip` | `/updates/1.30/ver_1.346.zip` | `9cb19d96f645d88d...` | urlencode |
| 22 | `ver_1.350.zip` | `/updates/1.30/ver_1.350.zip` | `4c10a14b2765c807...` | urlencode |
| 23 | `ver_1.352.zip` | `/updates/1.30/ver_1.352.zip` | `a6a675bb9df92408...` | urlencode |
| 24 | `ver_1.354.zip` | `/updates/1.30/ver_1.354.zip` | `4871da20d1a44ef8...` | urlencode |
| 25 | `ver_1.355.zip` | `/updates/1.30/ver_1.355.zip` | `778996580d1869a0...` | urlencode |
| 26 | `ver_1.356.zip` | `/updates/1.30/ver_1.356.zip` | `2b18edb393c32869...` | urlencode |
| 27 | `ver_1.357.zip` | `/updates/1.30/ver_1.357.zip` | `8119cc274771a7bd...` | urlencode |
| 28 | `ver_1.359.zip` | `/updates/1.30/ver_1.359.zip` | `a279ab6d1d016c58...` | urlencode |
| 29 | `ver_1.360.zip` | `/updates/1.30/ver_1.360.zip` | `a279ab6d1d016c58...` | urlencode |
| 30 | `ver_1.370.zip` | `/updates/1.30/ver_1.370.zip` | `f0495f6a5bb2ab4d...` | urlencode |
| 31 | `ver_1.371.zip` | `/updates/1.30/ver_1.371.zip` | `40acbb4d8daa54b6...` | urlencode |
| 32 | `ver_1.372.zip` | `/updates/1.30/ver_1.372.zip` | `f67eb93cd71e49a4...` | urlencode |
| 33 | `ver_1.375.zip` | `/updates/1.30/ver_1.375.zip` | `47664cb7b525a885...` | urlencode |
| 34 | `ver_1.387.zip` | `/updates/1.30/ver_1.387.zip` | `bd7e98161c4ca652...` | urlencode |
| 35 | `ver_1.396.zip` | `/updates/1.30/ver_1.396.zip` | `5fd970139fcfb13a...` | urlencode |
| 36 | `ver_1.397.zip` | `/updates/1.30/ver_1.397.zip` | `ceb27382c39935c9...` | urlencode |
| 37 | `ver_1.402.zip` | `/updates/1.40/ver_1.402.zip` | `52b24d0bb654b130...` | urlencode |
| 38 | `ver_1.406.zip` | `/updates/1.40/ver_1.406.zip` | `a56ab8fd41ca825b...` | urlencode |
| 39 | `ver_1.408.zip` | `/updates/1.40/ver_1.408.zip` | `16f159ead1ec925b...` | urlencode |
| 40 | `ver_1.409.zip` | `/updates/1.40/ver_1.409.zip` | `3e067a83eaa30f12...` | urlencode |
| 41 | `ver_1.410.zip` | `/updates/1.40/ver_1.410.zip` | `f5b8263df2e067ff...` | urlencode |
| 42 | `ver_1.411.zip` | `/updates/1.40/ver_1.411.zip` | `6f7b3e39529430b1...` | urlencode |
| 43 | `ver_1.412.zip` | `/updates/1.40/ver_1.412.zip` | `f3afa5ff7b13cc18...` | urlencode |
| 44 | `ver_1.426.zip` | `/updates/1.40/ver_1.426.zip` | `e37dfc765c9b107e...` | urlencode |
| 45 | `ver_1.432.zip` | `/updates/1.40/ver_1.432.zip` | `9b92326416e1f2cd...` | urlencode |
| 46 | `ver_1.449.zip` | `/updates/1.40/ver_1.449.zip` | `c90420d409df98e3...` | urlencode |
| 47 | `ver_1.456.zip` | `/updates/1.40/ver_1.456.zip` | `6b571e185effd8c6...` | urlencode |
| 48 | `ver_1.462.zip` | `/updates/1.40/ver_1.462.zip` | `2a2aa3da9ca66359...` | urlencode |
| 49 | `ver_1.463.zip` | `/updates/1.40/ver_1.463.zip` | `ce73866bc9ec8168...` | urlencode |
| 50 | `ver_1.464.zip` | `/updates/1.40/ver_1.464.zip` | `a63b24622550f942...` | urlencode |
| 51 | `ver_1.482.zip` | `/updates/1.40/ver_1.482.zip` | `14d264b0b98d1369...` | urlencode |
| 52 | `ver_1.485.zip` | `/updates/1.40/ver_1.485.zip` | `e9deb7665bfeec3f...` | urlencode |
| 53 | `ver_1.490.zip` | `/updates/1.40/ver_1.490.zip` | `93b467cf6aba1301...` | urlencode |
| 54 | `ver_1.496.zip` | `/updates/1.40/ver_1.496.zip` | `61d06089ad5a67ae...` | urlencode |
| 55 | `ver_1.498.zip` | `/updates/1.40/ver_1.498.zip` | `cbe13607769ed2fa...` | urlencode |
| 56 | `ver_1.504.zip` | `/updates/1.50/ver_1.504.zip` | `73d34dd29ec72245...` | urlencode |
| 57 | `ver_1.507.zip` | `/updates/1.50/ver_1.507.zip` | `859b77852121bec8...` | urlencode |
| 58 | `ver_1.511.zip` | `/updates/1.50/ver_1.511.zip` | `9b7efaa0fc8acede...` | urlencode |
| 59 | `ver_1.513.zip` | `/updates/1.50/ver_1.513.zip` | `18ec985d1b29a945...` | urlencode |
| 60 | `ver_1.515.zip` | `/updates/1.50/ver_1.515.zip` | `7ff25bed81fe7767...` | urlencode |
| 61 | `ver_1.517.zip` | `/updates/1.50/ver_1.517.zip` | `6b448734200893a5...` | urlencode |
| 62 | `ver_1.518.zip` | `/updates/1.50/ver_1.518.zip` | `d50e62f31c56384b...` | urlencode |
| 63 | `ver_1.519.zip` | `/updates/1.50/ver_1.519.zip` | `14e5754c75884fcc...` | urlencode+anchor-injection |
| 64 | `ver_1.529.zip` | `/updates/1.50/ver_1.529.zip` | `a5846aca666e0d5d...` | urlencode |
| 65 | `ver_1.532.zip` | `/updates/1.50/ver_1.532.zip` | `e8dd85b2449f84c0...` | urlencode |
| 66 | `ver_1.533.zip` | `/updates/1.50/ver_1.533.zip` | `f8a25cf6ba08f2be...` | urlencode |
| 67 | `ver_1.534.zip` | `/updates/1.50/ver_1.534.zip` | `4dca134b68ee82a2...` | urlencode |
| 68 | `ver_1.535.zip` | `/updates/1.50/ver_1.535.zip` | `b5365d4c100e37ab...` | urlencode |
| 69 | `ver_1.536.zip` | `/updates/1.50/ver_1.536.zip` | `b70902dfb4060d7a...` | urlencode |
| 70 | `ver_1.539.zip` | `/updates/1.50/ver_1.539.zip` | `6d29304b96132ebd...` | urlencode |
| 71 | `ver_1.541.zip` | `/updates/1.50/ver_1.541.zip` | `cff7f6080a57ed66...` | urlencode |
| 72 | `ver_1.546.zip` | `/updates/1.50/ver_1.546.zip` | `9b60fae7f31e5df2...` | urlencode |
| 73 | `ver_1.551.zip` | `/updates/1.50/ver_1.551.zip` | `f8ee40fa330d68da...` | urlencode |
| 74 | `ver_1.552.zip` | `/updates/1.50/ver_1.552.zip` | `ad203f8df17e2d10...` | urlencode |
| 75 | `ver_1.554.zip` | `/updates/1.50/ver_1.554.zip` | `18ebfa82dce8a823...` | urlencode |
| 76 | `ver_1.555.zip` | `/updates/1.50/ver_1.555.zip` | `f61a10c36b50b36b...` | urlencode |
| 77 | `ver_1.556.zip` | `/updates/1.50/ver_1.556.zip` | `e4b0d3cd835a3657...` | urlencode |
| 78 | `ver_1.557.zip` | `/updates/1.50/ver_1.557.zip` | `8002b8508c1b9b14...` | urlencode |
| 79 | `ver_1.561.zip` | `/updates/1.50/ver_1.561.zip` | `9be69833265da2c8...` | urlencode |
| 80 | `ver_1.568.zip` | `/updates/1.50/ver_1.568.zip` | `dd90fc869058e5a7...` | urlencode |
| 81 | `ver_1.572.zip` | `/updates/1.50/ver_1.572.zip` | `eb0071b94e6342b6...` | urlencode |
| 82 | `ver_1.583.zip` | `/updates/1.50/ver_1.583.zip` | `faf7037812cb1345...` | urlencode |
| 83 | `ver_1.587.zip` | `/updates/1.50/ver_1.587.zip` | `a000fc230209cad4...` | urlencode |
| 84 | `ver_1.589.zip` | `/updates/1.50/ver_1.589.zip` | `23ec9d2ac905f868...` | urlencode |
| 85 | `ver_1.591.zip` | `/updates/1.50/ver_1.591.zip` | `1a69cbc0d87d1f02...` | urlencode |
| 86 | `ver_1.602.zip` | `/updates/1.60/ver_1.602.zip` | `b6bf83583aea4c1c...` | urlencode |
| 87 | `ver_1.610.zip` | `/updates/1.60/ver_1.610.zip` | `a2eb640e5660e51e...` | urlencode |
| 88 | `ver_1.613.zip` | `/updates/1.60/ver_1.613.zip` | `d580047296a40951...` | urlencode |
| 89 | `ver_1.617.zip` | `/updates/1.60/ver_1.617.zip` | `e4b9d2ecd6b62101...` | urlencode |
| 90 | `ver_1.618.zip` | `/updates/1.60/ver_1.618.zip` | `da0dccd5d8129f36...` | urlencode |
| 91 | `ver_1.620.zip` | `/updates/1.60/ver_1.620.zip` | `87388a9b597048e0...` | urlencode |
| 92 | `ver_1.625.zip` | `/updates/1.60/ver_1.625.zip` | `af0005fea825b432...` | urlencode |
| 93 | `ver_1.626.zip` | `/updates/1.60/ver_1.626.zip` | `cf084b0f8da8fb34...` | urlencode |
| 94 | `ver_1.627.zip` | `/updates/1.60/ver_1.627.zip` | `2602fedd545a2b84...` | urlencode |
| 95 | `ver_1.628.zip` | `/updates/1.60/ver_1.628.zip` | `33d618d0d5979394...` | urlencode |
| 96 | `ver_1.633.zip` | `/updates/1.60/ver_1.633.zip` | `6199f98c0153f2cb...` | urlencode |
| 97 | `ver_1.634.zip` | `/updates/1.60/ver_1.634.zip` | `3356e9fdff7ee6fd...` | urlencode |
| 98 | `ver_1.638.zip` | `/updates/1.60/ver_1.638.zip` | `e436f5498d9eac82...` | urlencode |
| 99 | `ver_1.642.zip` | `/updates/1.60/ver_1.642.zip` | `73db0af058c4dd8e...` | urlencode |
| 100 | `ver_1.643.zip` | `/updates/1.60/ver_1.643.zip` | `5dd7505e5e9b69f9...` | urlencode |
| 101 | `ver_1.644.zip` | `/updates/1.60/ver_1.644.zip` | `3bbf16d10e6101a6...` | urlencode |
| 102 | `ver_1.645.zip` | `/updates/1.60/ver_1.645.zip` | `dc7554805828db03...` | urlencode |
| 103 | `ver_1.649.zip` | `/updates/1.60/ver_1.649.zip` | `a34f456bb22bb49a...` | urlencode |
| 104 | `ver_1.650.zip` | `/updates/1.60/ver_1.650.zip` | `1fded4e332087edd...` | urlencode |
| 105 | `ver_1.651.zip` | `/updates/1.60/ver_1.651.zip` | `974d9c1ed41db26c...` | urlencode |
| 106 | `ver_1.654.zip` | `/updates/1.60/ver_1.654.zip` | `23c1a14906b34e2c...` | urlencode |
| 107 | `ver_1.656.zip` | `/updates/1.60/ver_1.656.zip` | `e249d78fedc6d3b2...` | urlencode |
| 108 | `ver_1.660.zip` | `/updates/1.60/ver_1.660.zip` | `7068186d0fde2b1e...` | urlencode |
| 109 | `ver_1.665.zip` | `/updates/1.60/ver_1.665.zip` | `aa1fb3977f5f941c...` | urlencode |
| 110 | `ver_1.666.zip` | `/updates/1.60/ver_1.666.zip` | `437e09dfda4f3341...` | urlencode |
| 111 | `ver_1.669.zip` | `/updates/1.60/ver_1.669.zip` | `1885535d8ab2ce8c...` | urlencode |
| 112 | `ver_1.674.zip` | `/updates/1.60/ver_1.674.zip` | `9fca9ce185a525b8...` | urlencode |
| 113 | `ver_1.675.zip` | `/updates/1.60/ver_1.675.zip` | `4aad98f5cfb2aa45...` | urlencode |
| 114 | `ver_1.678.zip` | `/updates/1.60/ver_1.678.zip` | `3541cf1cde4deeae...` | urlencode |
| 115 | `ver_1.680.zip` | `/updates/1.60/ver_1.680.zip` | `9a26b92b885b6e9d...` | urlencode |
| 116 | `ver_1.681.zip` | `/updates/1.60/ver_1.681.zip` | `8fb40bb9e3169797...` | urlencode |
| 117 | `ver_1.684.zip` | `/updates/1.60/ver_1.684.zip` | `0ee052905077606a...` | urlencode |
| 118 | `ver_1.687.zip` | `/updates/1.60/ver_1.687.zip` | `106e9ac25acf7ed1...` | urlencode |
| 119 | `ver_1.688.zip` | `/updates/1.60/ver_1.688.zip` | `e77a529410dc519a...` | urlencode |
| 120 | `ver_1.690.zip` | `/updates/1.60/ver_1.690.zip` | `045d16e46da68af4...` | urlencode |
| 121 | `ver_1.691.zip` | `/updates/1.60/ver_1.691.zip` | `5e53b1fbf6c5c923...` | urlencode |
| 122 | `ver_1.691_manifest.json` | `/updates/1.60/ver_1.691_manifest.json` | `f56b36e719dff5ca...` | manifest checksum updated |
| 123 | `ver_1.694.zip` | `/updates/1.60/ver_1.694.zip` | `cbd51178b1caf4d8...` | urlencode |
| 124 | `ver_1.694_manifest.json` | `/updates/1.60/ver_1.694_manifest.json` | `54914a2960f020ac...` | manifest checksum updated |
## Smoke-test po uploadzie
```bash
curl -sSI "https://www.cmspro.project-dc.pl/updates/versions.php" | head -3
# oczekiwane: HTTP/1.1 200 OK
curl -sS -o /tmp/t.zip "https://www.cmspro.project-dc.pl/updates/1.50/ver_1.519.zip"
sha256sum /tmp/t.zip
# oczekiwane: 14e5754c75884fcc9d59ba3ef3670d85461a80f5d37a3cc88fa09672b73786a7
```
## Po smoke-test
1. **Wyloguj i zaloguj sie ponownie** w panelu instancji testowej (czyszczenie sesji new-version)
2. Aktualizacja -> powinna pokazac wersje > 1.519 (klucz licencji teraz dotrze do serwera)
3. Aktualizuj kolejno do najnowszej dostepnej wersji

View File

@@ -0,0 +1,237 @@
---
phase: 05-domain-seoadditional-cron-releases
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/Domain/SeoAdditional/SeoAdditionalRepository.php
- autoload/Domain/Cron/CronRepository.php
- autoload/Domain/Releases/ReleasesRepository.php
- autoload/Domain/Releases/UpdateRepository.php
- autoload/admin/factory/class.SeoAdditional.php
- autoload/class.Cron.php
- autoload/admin/factory/class.Releases.php
- autoload/admin/factory/class.Update.php
autonomous: true
delegation: auto
---
<objective>
## Goal
Utworzyć Domain repositories dla SeoAdditional, Cron i Releases/Update, oraz zaktualizować legacy klasy do wzorca wrapper delegation.
## Purpose
Kompletuje Domain layer (wszystkie 13 repozytoriów). Po tej fazie cała logika biznesowa domenowa jest w namespace Domain\ — gotowe pod Admin\ controllers (Fazy 6-13).
## Output
- autoload/Domain/SeoAdditional/SeoAdditionalRepository.php
- autoload/Domain/Cron/CronRepository.php
- autoload/Domain/Releases/ReleasesRepository.php
- autoload/Domain/Releases/UpdateRepository.php
- Wrappery w 4 legacy klasach (SeoAdditional, Cron, Releases, Update)
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
## Prior Work
@.paul/phases/04-domain-authors-newsletter/04-01-SUMMARY.md
## Source Files
@autoload/admin/factory/class.SeoAdditional.php
@autoload/class.Cron.php
@autoload/admin/factory/class.Releases.php
@autoload/admin/factory/class.Update.php
@autoload/Domain/Authors/AuthorsRepository.php
@autoload/admin/factory/class.Authors.php
</context>
<acceptance_criteria>
## AC-1: SeoAdditional Repository
```gherkin
Given klasa admin\factory\SeoAdditional używa global $mdb bezpośrednio
When migrujemy logikę do Domain\SeoAdditional\SeoAdditionalRepository
Then repo przyjmuje $db w konstruktorze, nie używa globals
And factory wrapper deleguje do nowego repo (new repo per call)
And wszystkie 3 metody: elementDelete, elementSave, elementDetails
```
## AC-2: Cron Repository
```gherkin
Given legacy class Cron (bez namespace) w autoload/class.Cron.php używa global $mdb
When migrujemy logikę do Domain\Cron\CronRepository
Then repo przyjmuje $db w konstruktorze
And legacy class Cron deleguje do Domain\Cron\CronRepository (new repo per call z global $mdb)
And wszystkie metody zachowane: automaticUpdateSites, getSiteMainLinks, getSiteOtherLinks + metody prywatne helper
```
## AC-3: Releases Repository
```gherkin
Given klasa admin\factory\Releases używa global $mdb bezpośrednio
When migrujemy logikę do Domain\Releases\ReleasesRepository
Then repo przyjmuje $db w konstruktorze
And factory wrapper deleguje do nowego repo
And wszystkie metody zachowane: getVersions, promote, demote, discoverVersions, getLicenses, getLicense, saveLicense, deleteLicense, toggleBeta
```
## AC-4: Update Repository
```gherkin
Given klasa admin\factory\Update używa global $mdb i $settings bezpośrednio
When migrujemy logikę do Domain\Releases\UpdateRepository
Then repo przyjmuje $db i $settings w konstruktorze
And factory wrapper deleguje do nowego repo (przekazując globals przez konstruktor)
And metoda update() zachowana w pełni
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: SeoAdditional — Domain repo + wrapper</name>
<files>
autoload/Domain/SeoAdditional/SeoAdditionalRepository.php,
autoload/admin/factory/class.SeoAdditional.php
</files>
<action>
Utwórz autoload/Domain/SeoAdditional/SeoAdditionalRepository.php:
- namespace Domain\SeoAdditional;
- konstruktor: __construct($db) — przechowuje $db jako private property
- Metody (camelCase, z logiki class.SeoAdditional.php):
* elementDelete($elementId) — delete z pp_seo_additional
* elementSave($id, $url, $status, $title, $keywords, $description, $text) — insert lub update + \S::delete_cache()
* elementDetails($elementId) — get z pp_seo_additional
- PHP < 8.0: bez typed params, bez named args, bez match
Zaktualizuj autoload/admin/factory/class.SeoAdditional.php:
- Zastąp każdą metodę wrapperem delegującym: new \Domain\SeoAdditional\SeoAdditionalRepository($mdb)->metoda()
- Pattern z class.Authors.php: global $mdb; $repo = new \Domain\...\Repository($mdb); return $repo->method(...)
- Zachowaj dokładnie te same sygnatury metod (snake_case w factory, camelCase w repo)
</action>
<verify>
Grep: Domain\SeoAdditional istnieje w autoload/Domain/SeoAdditional/SeoAdditionalRepository.php
Grep: new \Domain\SeoAdditional\SeoAdditionalRepository istnieje w class.SeoAdditional.php
Brak global $mdb bezpośrednio w repo (tylko w factory wrapper)
</verify>
<done>AC-1 satisfied: SeoAdditional repo + wrapper delegation</done>
</task>
<task type="auto">
<name>Task 2: Cron — Domain repo + wrapper</name>
<files>
autoload/Domain/Cron/CronRepository.php,
autoload/class.Cron.php
</files>
<action>
Utwórz autoload/Domain/Cron/CronRepository.php:
- namespace Domain\Cron;
- konstruktor: __construct($db)
- Przenieś CAŁĄ logikę z class.Cron.php do repo jako metody camelCase:
* automaticUpdateSites() — odpowiednik automatic_update_sites()
* getSiteMainLinks() — odpowiednik get_site_main_links()
* getSiteOtherLinks() — odpowiednik get_site_other_links()
* Wszystkie metody prywatne helper (getSiteMetaTitle, getSiteMetaKeywords, itd.) — przenieś jako private methods
- PHP < 8.0: bez typed params
- $mdb zastąp przez $this->db we wszystkich zapytaniach
Zaktualizuj autoload/class.Cron.php:
- Zachowaj oryginalny namespace (brak namespace — klasa globalna Cron)
- Zastąp każdą public static metodę wrapperem:
global $mdb; $repo = new \Domain\Cron\CronRepository($mdb); return $repo->camelCaseMethod();
- Usuń ciała helper methods (prywatne) — logika jest teraz w repo
</action>
<verify>
Grep: Domain\Cron istnieje w autoload/Domain/Cron/CronRepository.php
Grep: new \Domain\Cron\CronRepository istnieje w autoload/class.Cron.php
Brak bezpośrednich zapytań $mdb-> w class.Cron.php (tylko delegacja)
</verify>
<done>AC-2 satisfied: Cron repo + wrapper delegation</done>
</task>
<task type="auto">
<name>Task 3: Releases + Update — Domain repos + wrappers</name>
<files>
autoload/Domain/Releases/ReleasesRepository.php,
autoload/Domain/Releases/UpdateRepository.php,
autoload/admin/factory/class.Releases.php,
autoload/admin/factory/class.Update.php
</files>
<action>
Utwórz autoload/Domain/Releases/ReleasesRepository.php:
- namespace Domain\Releases;
- konstruktor: __construct($db)
- Przenieś logikę z class.Releases.php: getVersions, promote, demote, discoverVersions, getLicenses, getLicense, saveLicense, deleteLicense, toggleBeta
- Prywatna metoda zipDir() jako private helper
- PHP < 8.0: bez ": array", bez ": void", bez ": int", bez ": string" type hints (PHP < 8.0, ale PHP 7.x obsługuje return types — ZACHOWAJ return type hints jeśli były w oryginale, bo PHP 7+ je obsługuje)
- Uwaga: PHP < 8.0 znaczy brak PHP8 features. PHP 7.x return types działają. Sprawdź oryginał — miał ": array", ": void", ": int", ": string" — zachowaj je.
Utwórz autoload/Domain/Releases/UpdateRepository.php:
- namespace Domain\Releases;
- konstruktor: __construct($db, $settings) — settings potrzebne do update_key i wersji
- Przenieś logikę z class.Update.php: metoda update()
- Zastąp global $mdb → $this->db, global $settings → $this->settings
- Wywołania \S::* zachowaj (klasa S jest dostępna globalnie)
Zaktualizuj autoload/admin/factory/class.Releases.php:
- Zastąp każdą metodę wrapperem: global $mdb; $repo = new \Domain\Releases\ReleasesRepository($mdb); return $repo->method(...)
- Zachowaj dokładnie te same sygnatury
Zaktualizuj autoload/admin/factory/class.Update.php:
- Zastąp metodę update() wrapperem:
global $mdb, $settings; $repo = new \Domain\Releases\UpdateRepository($mdb, $settings); return $repo->update();
</action>
<verify>
Grep: Domain\Releases istnieje w obu nowych plikach repo
Grep: new \Domain\Releases\ReleasesRepository istnieje w class.Releases.php
Grep: new \Domain\Releases\UpdateRepository istnieje w class.Update.php
Brak bezpośrednich zapytań $mdb-> w factory wrapperach
</verify>
<done>AC-3 i AC-4 satisfied: Releases + Update repos + wrapper delegation</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- autoload/autoloader.php (PSR-4 mapowanie już obejmuje Domain\)
- autoload/Domain/Authors/, autoload/Domain/Newsletter/ (ukończone w Phase 4)
- autoload/Domain/Scontainers/, autoload/Domain/Banners/ (ukończone w Phase 3)
- Żadne inne pliki poza listą files_modified
- cron.php entry point — nie modyfikuj (klasa Cron nadal globalna)
## SCOPE LIMITS
- Tylko Domain repositories i factory wrappers — bez Admin\ controllers
- Bez zmian w tabelach bazy danych ani SQL schema
- Bez refaktoryzacji metod — 1:1 przeniesienie logiki
</boundaries>
<verification>
Przed deklaracją ukończenia:
- [ ] Grep: autoload/Domain/SeoAdditional/SeoAdditionalRepository.php istnieje
- [ ] Grep: autoload/Domain/Cron/CronRepository.php istnieje
- [ ] Grep: autoload/Domain/Releases/ReleasesRepository.php istnieje
- [ ] Grep: autoload/Domain/Releases/UpdateRepository.php istnieje
- [ ] Grep: class.SeoAdditional.php zawiera "new \Domain\SeoAdditional"
- [ ] Grep: class.Cron.php zawiera "new \Domain\Cron"
- [ ] Grep: class.Releases.php zawiera "new \Domain\Releases"
- [ ] Grep: class.Update.php zawiera "new \Domain\Releases"
- [ ] Brak syntax errors (php -l na każdym nowym pliku)
</verification>
<success_criteria>
- 4 nowe Domain repository pliki utworzone
- 4 legacy klasy zaktualizowane do wrapper delegation
- Zero zmian w logice biznesowej (1:1 migracja)
- PHP < 8.0 kompatybilność zachowana
- Brak globals w repozytoriach
</success_criteria>
<output>
Po ukończeniu utwórz: .paul/phases/05-domain-seoadditional-cron-releases/05-01-SUMMARY.md
</output>

View File

@@ -0,0 +1,125 @@
---
phase: 05-domain-seoadditional-cron-releases
plan: 01
subsystem: domain
tags: [php, domain, repository, wrapper-delegation, medoo]
requires:
- phase: 01-infrastructure
provides: PSR-4 autoloader mapujący Domain\
provides:
- Domain\SeoAdditional\SeoAdditionalRepository
- Domain\Cron\CronRepository
- Domain\Releases\ReleasesRepository
- Domain\Releases\UpdateRepository
- Wrapper delegation dla 4 legacy klas
affects:
- 11-admin-newsletter-emails-seoadditional
- 13-admin-releases-update
tech-stack:
added: []
patterns:
- "Wrapper delegation: admin\\factory i global class.Cron delegują do Domain\\ repos"
- "UpdateRepository przyjmuje ($db, $settings) — dwa globals jako explicit params"
key-files:
created:
- autoload/Domain/SeoAdditional/SeoAdditionalRepository.php
- autoload/Domain/Cron/CronRepository.php
- autoload/Domain/Releases/ReleasesRepository.php
- autoload/Domain/Releases/UpdateRepository.php
modified:
- autoload/admin/factory/class.SeoAdditional.php
- autoload/class.Cron.php
- autoload/admin/factory/class.Releases.php
- autoload/admin/factory/class.Update.php
key-decisions:
- "UpdateRepository przyjmuje ($db, $settings) w konstruktorze — settings potrzebny do update_key"
- "Cron helper methods (get_site_meta_*) zostały private w CronRepository — były wywoływane tylko wewnętrznie"
- "class.Cron.php zachowuje brak namespace (klasa globalna) — entry point cron.php używa bezpośrednio"
patterns-established:
- "Wszystkie Domain repos: konstruktor($db), brak globals, metody camelCase"
- "Factory wrappers: new repo per call, global $mdb w każdej metodzie"
duration: ~5min
started: 2026-04-26T00:00:00Z
completed: 2026-04-26T00:05:00Z
---
# Phase 5 Plan 01: SeoAdditional + Cron + Releases Summary
**4 Domain repositories ukończone — Domain layer kompletny (13/13 repos), wrapper delegation dla SeoAdditional, Cron, Releases i Update.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~5min |
| Started | 2026-04-26 |
| Completed | 2026-04-26 |
| Tasks | 3 completed |
| Files modified | 8 (4 created, 4 updated) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: SeoAdditional Repository | Pass | 3 metody: elementDelete, elementSave, elementDetails |
| AC-2: Cron Repository | Pass | 3 public + 12 private helper methods, brak namespace w wrapperze |
| AC-3: Releases Repository | Pass | 9 metod + private zipDir helper |
| AC-4: Update Repository | Pass | Pełna logika update(), ($db, $settings) w konstruktorze |
## Accomplishments
- Ukończono Domain layer: wszystkie 13 repozytoriów w `Domain\` namespace
- SeoAdditional: prosta migracja 3 CRUD metod z factory do repo
- Cron: migracja dużej klasy (15 metod) — helper methods stały się private w repo
- Releases: 9 metod + prywatny helper zipDir, zachowane PHP 7.x return type hints
- UpdateRepository: jako jedyny repo przyjmuje 2 parametry ($db, $settings) — settings wymagane dla update_key
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/Domain/SeoAdditional/SeoAdditionalRepository.php` | Created | SEO dodatkowe wpisy — CRUD |
| `autoload/Domain/Cron/CronRepository.php` | Created | Cron jobs — crawling i analiza stron |
| `autoload/Domain/Releases/ReleasesRepository.php` | Created | Zarządzanie wersjami i licencjami |
| `autoload/Domain/Releases/UpdateRepository.php` | Created | Mechanizm auto-update (pobieranie paczek ZIP) |
| `autoload/admin/factory/class.SeoAdditional.php` | Modified | Wrapper → deleguje do Domain\SeoAdditional |
| `autoload/class.Cron.php` | Modified | Wrapper → deleguje do Domain\Cron (brak namespace) |
| `autoload/admin/factory/class.Releases.php` | Modified | Wrapper → deleguje do Domain\Releases\ReleasesRepository |
| `autoload/admin/factory/class.Update.php` | Modified | Wrapper → deleguje do Domain\Releases\UpdateRepository |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| UpdateRepository($db, $settings) | Metoda update() używa $settings['update_key'] — musi być w konstruktorze | Admin\Update\UpdateController też przekaże oba parametry |
| Cron helpers → private | Metody get_site_meta_* były wywoływane tylko przez getSiteOtherLinks() | Czystsza enkapsulacja, brak public API dla wewnętrznych helperów |
| class.Cron.php bez namespace | Zachowanie 100% compat — cron.php używa `Cron::` bez backslasha | Klasa globalna pozostaje globalna do Phase 19 cleanup |
## Deviations from Plan
None — plan wykonany dokładnie jak zaplanowano.
## Next Phase Readiness
**Ready:**
- Domain layer kompletny (13 repozytoriów) — gotowy pod Admin\ controllers
- Phase 6: Admin Base Infrastructure może startować (nie zależy od Domain\Cron/Releases/SeoAdditional bezpośrednio)
- Phase 11 (Admin: Newsletter + Emails + SeoAdditional) i Phase 13 (Admin: Releases + Update) mają gotowe Domain repos
**Concerns:**
- Brak — wszystkie dependency spełnione
**Blockers:**
- None
---
*Phase: 05-domain-seoadditional-cron-releases, Plan: 01*
*Completed: 2026-04-26*

View File

@@ -0,0 +1,266 @@
---
phase: 06-admin-base
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/Admin/ViewModels/Forms/FormEditViewModel.php
- autoload/Admin/ViewModels/Forms/FormField.php
- autoload/Admin/ViewModels/Forms/FormFieldType.php
- autoload/Admin/ViewModels/Forms/FormTab.php
- autoload/Admin/ViewModels/Forms/FormAction.php
- autoload/Admin/Validation/FormValidator.php
- composer.json
autonomous: true
delegation: off
---
<objective>
## Goal
Założyć fundament `Admin\` namespace przez przeniesienie warstwy formularzy z shopPRO: 5 ViewModeli (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction) w `Admin\ViewModels\Forms\` oraz `FormValidator` w `Admin\Validation\`. Plan kończy się działającym, autoloadowanym systemem definicji + walidacji formularzy gotowym do użycia w Phase 7+.
## Purpose
ROADMAP Phase 6 (Admin: Base Infrastructure) wymaga bazowych klas Admin\ — Form Edit System jest pierwszym filarem (kontrolery z Phase 7-13 będą zwracać `FormEditViewModel` zamiast budować HTML inline). PROJECT.md deklarował to ukończone, ale audyt wykazał brak — Phase 6/Plan 01 to nadrabia, kopiując stabilny wzorzec z shopPRO.
## Output
- `autoload/Admin/ViewModels/Forms/` — 5 plików (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction)
- `autoload/Admin/Validation/FormValidator.php`
- `composer.json` z `"php": ">=7.4"`
- `.paul/phases/06-admin-base/06-01-SUMMARY.md`
</objective>
<context>
<clarifications>
- **Form VMs** — Czy mamy już Form ViewModele, czy kopiować z shopPRO?
→ Odpowiedź: Skopiuj z shopPRO teraz (PROJECT.md deklarował, ale plików nie ma).
- **NS case** — Jaka konwencja namespace dla Admin\?
→ Odpowiedź: Fully PascalCase (`Admin\Base`, `Admin\Support`, `Admin\ViewModels\Forms`, `Admin\Validation`). Nie powielać błędu shopPRO (`admin\` lowercase).
- **PHP target** — Cel PHP dla nowego kodu?
→ Odpowiedź: `>=7.4` w composer.json. Docelowo musi działać też na 8.4/8.5+, więc kod ma być forward-compatible (typed properties OK, ale unikać deprecated wzorców 8.x).
- **Scope** — Zakres Phase 6?
→ Odpowiedź: Splituję Phase 6 na 2 plany. 06-01 = Forms (ten plan). 06-02 = TableListRequestFactory + BaseController.
</clarifications>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/codebase/architecture.md
## Reference Source (shopPRO — wzorzec do skopiowania)
@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormEditViewModel.php
@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormField.php
@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormFieldType.php
@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormTab.php
@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormAction.php
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Validation\FormValidator.php
## cmsPRO target paths
- `autoload/Admin/ViewModels/Forms/`
- `autoload/Admin/Validation/`
## Composer
@composer.json
</context>
<acceptance_criteria>
## AC-1: Form ViewModels load via PSR-4
```gherkin
Given composer.json mapuje "Admin\\" "autoload/Admin/"
When wykonam `composer dump-autoload && php -r "var_dump(class_exists('Admin\\ViewModels\\Forms\\FormEditViewModel'));"`
Then otrzymam `bool(true)` dla wszystkich 5 klas (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction)
```
## AC-2: FormValidator namespace and dependency
```gherkin
Given FormValidator zawiera `use Admin\ViewModels\Forms\FormField; use Admin\ViewModels\Forms\FormFieldType;`
When `php -r "var_dump(class_exists('Admin\\Validation\\FormValidator'));"`
Then otrzymam `bool(true)` i nie wystąpi fatal error o brakujących klasach
```
## AC-3: Smoke test — utworzenie i walidacja formularza
```gherkin
Given załadowane klasy z AC-1 i AC-2
When utworzę `FormField('email', FormFieldType::EMAIL, 'E-mail', '', 'main', true)` + uruchomię `FormValidator::validate(['email' => 'not-an-email'], [$field])`
Then otrzymam tablicę `['email' => '...musi być poprawnym adresem e-mail.']` (lub równoważny komunikat) walidacja działa
```
## AC-4: PHP version constraint i konwencja PascalCase
```gherkin
Given composer.json
When odczytam pole `require.php`
Then jest równe ">=7.4"
And wszystkie nowe pliki mają `namespace Admin\ViewModels\Forms;` lub `namespace Admin\Validation;` (PascalCase pierwszy segment, w przeciwieństwie do shopPRO `admin\`)
```
## AC-5: Zero regresji na istniejącym kodzie
```gherkin
Given commit przed planem
When wykonam `php -l` na każdym zmodyfikowanym pliku oraz `php -r "require 'autoload/autoloader.php';"` (lub equivalent inicjalizacji projektu)
Then żadnych błędów składni i żadnych warning/notice o redefinicji klas; istniejące Domain repos (np. `Domain\Articles\ArticlesRepository`) nadal się ładują
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Skopiuj 5 Form ViewModeli z shopPRO do Admin\ViewModels\Forms\ z PascalCase namespace</name>
<files>autoload/Admin/ViewModels/Forms/FormEditViewModel.php, autoload/Admin/ViewModels/Forms/FormField.php, autoload/Admin/ViewModels/Forms/FormFieldType.php, autoload/Admin/ViewModels/Forms/FormTab.php, autoload/Admin/ViewModels/Forms/FormAction.php</files>
<action>
Utwórz katalog `autoload/Admin/ViewModels/Forms/` (PascalCase — jeśli system plików ma kolizję z istniejącym `autoload/admin/`, NIE łącz ich; `Admin/` to nowy namespace PSR-4, `admin/` to legacy lowercase i pozostaje nietknięty).
Skopiuj 5 plików ze shopPRO 1:1 z TYLKO tymi modyfikacjami:
1. **Zmień `namespace admin\ViewModels\Forms;` → `namespace Admin\ViewModels\Forms;`** (każdy plik, pierwsza linia po `<?php`)
2. Wewnętrzne `use` (jeśli są) — zaktualizuj wszelkie odwołania `admin\ViewModels\Forms\X``Admin\ViewModels\Forms\X`
3. **NIE zmieniaj** sygnatur, typed properties, JSDoców, defaultów, statycznych helperów (np. `FormAction::save()`)
Pliki do skopiowania (źródło → cel):
- `C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormEditViewModel.php``autoload/Admin/ViewModels/Forms/FormEditViewModel.php`
- `...\FormField.php``autoload/Admin/ViewModels/Forms/FormField.php`
- `...\FormFieldType.php``autoload/Admin/ViewModels/Forms/FormFieldType.php`
- `...\FormTab.php``autoload/Admin/ViewModels/Forms/FormTab.php`
- `...\FormAction.php``autoload/Admin/ViewModels/Forms/FormAction.php`
Avoid:
- Wymienianie `public string $name;` na `string $name` (PHP 7.4 wspiera typed properties, jest OK)
- Dodawanie nowych funkcji „przy okazji"
- Mieszania case'a w nazwach katalogów (musi być dokładnie `Admin/ViewModels/Forms/`)
</action>
<verify>
`php -l autoload/Admin/ViewModels/Forms/FormEditViewModel.php` (i pozostałe 4) → "No syntax errors detected"
`grep -c "^namespace Admin\\\\ViewModels\\\\Forms;" autoload/Admin/ViewModels/Forms/*.php` → 5
`grep -c "^namespace admin\\\\" autoload/Admin/ViewModels/Forms/*.php` → 0
</verify>
<done>AC-1 częściowo (klasy istnieją), AC-4 częściowo (PascalCase namespace).</done>
</task>
<task type="auto">
<name>Task 2: Skopiuj FormValidator do Admin\Validation\ z PascalCase namespace i poprawnymi `use`</name>
<files>autoload/Admin/Validation/FormValidator.php</files>
<action>
Utwórz katalog `autoload/Admin/Validation/` i skopiuj `FormValidator.php` ze shopPRO z modyfikacjami:
1. `namespace admin\Validation;``namespace Admin\Validation;`
2. `use admin\ViewModels\Forms\FormField;``use Admin\ViewModels\Forms\FormField;`
3. `use admin\ViewModels\Forms\FormFieldType;``use Admin\ViewModels\Forms\FormFieldType;`
4. Cała reszta (logika walidacji, metody prywatne `validateField`, `validateLangSection`, `isEmpty`) bez zmian.
Avoid:
- Refaktoryzacji walidatora przy okazji (nawet jeśli widzisz coś do poprawy — zapisz w SUMMARY jako deferred issue)
- Zmiany komunikatów błędów (są używane w UI 1:1)
</action>
<verify>
`php -l autoload/Admin/Validation/FormValidator.php` → "No syntax errors detected"
`grep -E "^namespace |^use " autoload/Admin/Validation/FormValidator.php` zwraca:
`namespace Admin\Validation;`
`use Admin\ViewModels\Forms\FormField;`
`use Admin\ViewModels\Forms\FormFieldType;`
</verify>
<done>AC-2 satisfied: FormValidator ładuje się i zna FormField/FormFieldType.</done>
</task>
<task type="auto">
<name>Task 3: Dodaj `php: >=7.4` do composer.json i zregeneruj autoloader</name>
<files>composer.json</files>
<action>
W `composer.json` dodaj sekcję `require` z `"php": ">=7.4"` PRZED `require-dev`. Jeśli sekcja `require` już istnieje — dopisz tylko klucz `php`. Zachowaj istniejące mapowania PSR-4 (`Domain\\`, `Shared\\`, `Admin\\`, `Frontend\\`).
Po edycji uruchom `composer dump-autoload` (lub `php composer.phar dump-autoload` jeśli composer nie jest globalny).
Avoid:
- Aktualizacji composer.lock przez `composer update`
- Dodawania nowych dependencies
- Usuwania `Frontend\\` mapowania (nieużywane teraz, ale zostaje dla Phase 14+)
</action>
<verify>
`php -r "echo json_decode(file_get_contents('composer.json'), true)['require']['php'] ?? 'MISSING';"` → ">=7.4"
`php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\\\ViewModels\\\\Forms\\\\FormEditViewModel'));"``bool(true)`
`php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\\\Validation\\\\FormValidator'));"``bool(true)`
</verify>
<done>AC-1 satisfied (autoload działa dla wszystkich 6 klas), AC-4 satisfied (php>=7.4).</done>
</task>
<task type="auto">
<name>Task 4: Smoke test — runtime sanity check formularza</name>
<files>(brak modyfikacji — tylko weryfikacja runtime)</files>
<action>
Uruchom inline PHP smoke check:
```
php -r "require 'vendor/autoload.php';
use Admin\ViewModels\Forms\FormField;
use Admin\ViewModels\Forms\FormFieldType;
use Admin\Validation\FormValidator;
$field = new FormField('email', FormFieldType::EMAIL, 'E-mail', '', 'main', true);
$v = new FormValidator();
\$errors = \$v->validate(['email' => 'not-an-email'], [\$field]);
echo isset(\$errors['email']) ? 'OK: '.\$errors['email'] : 'FAIL';"
```
(Składnię cudzysłowów dostosuj do shell — bash vs cmd. Cel: dostać linię zaczynającą się od `OK:` z polskim komunikatem walidacji.)
Następnie sprawdź lint na wszystkich nowych plikach:
`for f in autoload/Admin/ViewModels/Forms/*.php autoload/Admin/Validation/*.php; do php -l "$f"; done`
Avoid:
- Tworzenia trwałego pliku testowego — to jednorazowy smoke check, wynik wkleimy do SUMMARY
- Modyfikacji jakichkolwiek plików legacy (admin/, autoload/admin/) — Phase 6 ich nie dotyczy
</action>
<verify>
Output zawiera "OK:" + komunikat o niepoprawnym e-mailu
`php -l` na wszystkich 6 plikach → 6× "No syntax errors detected"
`git status` pokazuje TYLKO: 5 nowych plików w `autoload/Admin/ViewModels/Forms/`, 1 nowy plik w `autoload/Admin/Validation/`, zmodyfikowany `composer.json`, regenerowany `vendor/composer/autoload_*` (jeśli vendor/ jest tracked)
</verify>
<done>AC-3 satisfied (walidacja działa runtime), AC-5 satisfied (zero regresji — tylko nowe pliki + composer.json).</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `autoload/admin/` (legacy, lowercase) — zostaje nietknięte do Phase 19
- `autoload/Domain/**` — Phase 5 closed, repozytoria stabilne
- `autoload/Shared/**` — Phase 2 closed
- `autoload/autoloader.php` — Phase 1 (hybrid loader działa); composer dump zaktualizuje vendor/, autoloader.php nie wymaga edycji
- `composer.lock` — nie aktualizuj (`composer update` zakazany w tym planie)
- shopPRO source files — read-only reference
## SCOPE LIMITS
- TableListRequestFactory → Plan 06-02 (NIE w tym planie)
- BaseController → Plan 06-02
- Admin\App.php (logowanie, special_actions, routing) → Phase 12 (Users + 2FA)
- Refaktoring/poprawki w skopiowanych klasach — zero zmian merytorycznych, tylko namespace
- Migracja istniejących admin\controls\* na nowe Form VMs → Phase 7+ (per moduł)
- Tworzenie templates/partials renderujących `FormEditViewModel` → poza zakresem Phase 6
- PHPUnit testy dla FormValidator → Phase 18
</boundaries>
<verification>
Przed declared complete:
- [ ] `php -l` zielony dla wszystkich 6 nowych plików
- [ ] `composer dump-autoload` zwraca exit 0
- [ ] Smoke test (Task 4) zwraca "OK:..."
- [ ] `grep -r "namespace admin\\\\" autoload/Admin/` → brak wyników (zero lowercase namespace w nowym Admin/)
- [ ] `git diff` pokazuje tylko: nowe pliki Admin/ + composer.json + vendor/composer/autoload_classmap.php (regen)
- [ ] Wszystkie AC (1-5) spełnione
</verification>
<success_criteria>
- 5 Form ViewModeli + FormValidator dostępne pod namespace `Admin\ViewModels\Forms\` i `Admin\Validation\`
- composer.json deklaruje `php: >=7.4`
- Smoke test walidacji e-maila zwraca komunikat błędu po polsku
- Zero zmian w legacy `autoload/admin/`, `Domain/`, `Shared/`
- Plan gotowy do unify → następnie Plan 06-02
</success_criteria>
<output>
Po wykonaniu utworzyć `.paul/phases/06-admin-base/06-01-SUMMARY.md` z:
- Lista skopiowanych plików (z liczbą linii)
- Output smoke testu (literalnie)
- Wszelkie deferred issues (np. coś co warto zrefaktoryzować w FormValidator/FormField w przyszłości)
- Setup dla Plan 06-02 (jakie założenia po 06-01: Admin\ namespace działa, Form VMs dostępne)
</output>

View File

@@ -0,0 +1,100 @@
---
phase: 06-admin-base
plan: 01
status: complete
completed: 2026-04-30
---
# SUMMARY: Plan 06-01 — Forms infrastructure
## Co zrobiono
Przeniesiono z shopPRO do cmsPRO warstwę Form Edit System pod nowy namespace `Admin\` (PascalCase). Wszystkie pliki ładują się przez Composer PSR-4, smoke test runtime potwierdza poprawne działanie walidacji.
## Pliki utworzone (6)
| Plik | Linii | Namespace |
|------|-------|-----------|
| `autoload/admin/ViewModels/Forms/FormEditViewModel.php` | 178 | `Admin\ViewModels\Forms` |
| `autoload/admin/ViewModels/Forms/FormField.php` | 364 | `Admin\ViewModels\Forms` |
| `autoload/admin/ViewModels/Forms/FormFieldType.php` | 25 | `Admin\ViewModels\Forms` |
| `autoload/admin/ViewModels/Forms/FormTab.php` | 31 | `Admin\ViewModels\Forms` |
| `autoload/admin/ViewModels/Forms/FormAction.php` | 89 | `Admin\ViewModels\Forms` |
| `autoload/admin/Validation/FormValidator.php` | 196 | `Admin\Validation` |
**Total**: 883 linii kodu PHP 7.4-kompatybilnego.
## Pliki zmodyfikowane (2)
- `composer.json` — dodano `"require": { "php": ">=7.4" }`, mapowanie PSR-4 dla `Admin\` ustawione na realny case na disku
- `vendor/composer/autoload_*` — regenerowane przez `composer dump-autoload`
## Smoke test (literalny output)
```
Admin\ViewModels\Forms\FormEditViewModel: OK
Admin\ViewModels\Forms\FormField: OK
Admin\ViewModels\Forms\FormFieldType: OK
Admin\ViewModels\Forms\FormTab: OK
Admin\ViewModels\Forms\FormAction: OK
Admin\Validation\FormValidator: OK
SMOKE: OK: Pole "E-mail" musi być poprawnym adresem e-mail.
PHP_REQ: >=7.4
```
`php -l` na wszystkich 6 plikach: 6× "No syntax errors detected".
Zero regresji — `Domain\Articles\ArticlesRepository` i `Shared\Helpers\Helpers` ładują się dalej.
## Acceptance Criteria — status
- [x] AC-1: Form ViewModels load via PSR-4 — wszystkie 5 klas + FormValidator
- [x] AC-2: FormValidator namespace + dependency — `use Admin\ViewModels\Forms\...` działa
- [x] AC-3: Smoke test walidacji e-maila — zwraca PL komunikat błędu
- [x] AC-4: PHP `>=7.4` w composer.json + PascalCase namespace na wszystkich nowych plikach
- [x] AC-5: Zero regresji — istniejące Domain/Shared repos działają
## Deviation: PSR-4 path mapping w composer.json
**Plan zakładał:** `"Admin\\": "autoload/Admin/"` (PascalCase folder)
**Zmienione na:** `"Admin\\": "autoload/admin/"` (lowercase folder)
**Powód:** Windows file system jest case-insensitive — folder `autoload/admin/` (legacy) i `autoload/Admin/` (planowany) to ten sam katalog na dysku. Git tracked path = `admin/` (lowercase, ustanowione przez wcześniejsze fazy z legacy `controls/`, `factory/`, `view/`). Na produkcyjnym Linux (case-sensitive) PSR-4 z mapowaniem `"Admin\\": "autoload/Admin/"` nie znalazłby plików, bo realny katalog na disku to `admin/`.
**Konsekwencje:**
- Klasa `Admin\ViewModels\Forms\FormField` ładuje się z `autoload/admin/ViewModels/Forms/FormField.php`
- Legacy `admin\controls\Articles` (lowercase namespace) NIE matches PSR-4 prefix `Admin\` (case-sensitive) — ładuje się przez legacy autoloader (`autoload/autoloader.php`) ✓
- Cross-platform safe: działa identycznie na Windows i Linux
**Bonus zmiana (preventive):** `"Frontend\\": "autoload/Frontend/"``"Frontend\\": "autoload/front/"` z tego samego powodu — żeby Phase 14+ nie powtórzyła problemu.
## Boundaries — przestrzegane
- ✓ Brak zmian w `autoload/admin/controls/`, `factory/`, `view/` (50+ plików legacy nietknięte)
- ✓ Brak zmian w `autoload/Domain/`, `autoload/Shared/`
- ✓ Brak zmian w `autoload/autoloader.php`
-`composer.lock` nieruszony (`composer update` nie uruchamiany)
- ✓ Tylko zmodyfikowano composer.json + vendor/composer/autoload_* (regen)
## Deferred issues / observations
1. **FormValidator constants nie ma** — komunikaty błędów to literały string. Mogłyby być stałe klasy (np. `MSG_REQUIRED`, `MSG_INVALID_EMAIL`) dla i18n w przyszłości. Przeniesienie 1:1 z shopPRO (zachowanie kompatybilności).
2. **FormField fluent setters mile by były** — obecnie 17-argumentowy konstruktor + 14 factory methods. Dla Phase 7+ kontrolerów to OK, ale builder pattern (`FormField::text('name')->label('X')->required()->placeholder('Y')`) byłby czystszy. Deferred.
3. **`switch` jako nazwa metody** (`FormField::switch()`) — w PHP < 8.0 to słowo kluczowe i nie może być statycznym nazwiskiem metody w niektórych kontekstach. PHP 7.4 dopuszcza je jako nazwę metody (działa), ale uwaga przy edycji w niektórych edytorach. Zachowane bo shopPRO też tak ma.
4. **FormTab/FormAction bez factory methods** — tylko konstruktor. To jest design choice z shopPRO; nie zmieniam.
## Setup dla Plan 06-02
Po 06-01 założenia stabilne dla Plan 06-02 (Controller infrastructure):
- `Admin\` namespace działa via PSR-4 → mapowanie do `autoload/admin/`
- composer.json deklaruje `php: >=7.4`
- Form VMs dostępne — `BaseController` może budować/zwracać `FormEditViewModel`
- Konwencja PascalCase utrwalona — `Admin\Base\BaseController`, `Admin\Support\TableListRequestFactory`
Plan 06-02 doda:
- `autoload/admin/Support/TableListRequestFactory.php` (`Admin\Support\TableListRequestFactory`)
- `autoload/admin/Base/BaseController.php` (`Admin\Base\BaseController`)

View File

@@ -0,0 +1,300 @@
---
phase: 06-admin-base
plan: 02
type: execute
wave: 1
depends_on: ["06-01"]
files_modified:
- autoload/admin/Support/TableListRequestFactory.php
- autoload/admin/Support/Forms/FormRequestHandler.php
- autoload/admin/Support/Forms/FormFieldRenderer.php
autonomous: true
delegation: off
---
<objective>
## Goal
Domknąć Phase 6 (Admin Base Infrastructure) przez przeniesienie z shopPRO 3 klas warstwy Support: `TableListRequestFactory` (parser requestów listy z filtrami/sortowaniem/paginacją), `FormRequestHandler` (obsługa POST/persist/walidacji formularza), `FormFieldRenderer` (renderer HTML pól formularza). Po 06-02 cała infrastruktura Phase 7+ (kontrolery) jest gotowa.
## Purpose
Phase 7-13 będą migrować 17 modułów Admin do nowych namespace'ów. Każdy z tych kontrolerów będzie potrzebował:
- `TableListRequestFactory::fromRequest()` — uniformowy parsing list z `?page=&per_page=&sort=&filter[]=`
- `FormRequestHandler` — uniformowy handler POST → walidacja → persist → redirect
- `FormFieldRenderer` — generowanie HTML pól z `FormField` ViewModeli (zastępuje Shared\Html\Html dla nowych formularzy)
Bez tych helperów każdy kontroler musiałby duplikować logikę paginacji i obsługi POST. Pakiet 06-01 + 06-02 = pełny fundament Admin\ gotowy do Phase 7.
**Decyzja architektoniczna:** Brak `BaseController` — shopPRO używa POJOs z DI (np. `BannerController(BannerRepository, LanguagesRepository)`), nie dziedziczenia. Phase 7+ kontrolery będą zwykłymi klasami z Domain repos i tymi helperami w konstruktorze.
## Output
- `autoload/admin/Support/TableListRequestFactory.php`
- `autoload/admin/Support/Forms/FormRequestHandler.php`
- `autoload/admin/Support/Forms/FormFieldRenderer.php`
- `.paul/phases/06-admin-base/06-02-SUMMARY.md`
</objective>
<context>
<clarifications>
- **BaseCtrl** — Czy tworzymy `BaseController`?
→ Odpowiedź: NIE. POJOs z DI jak shopPRO. ROADMAP zaktualizowany — wpis BaseController usunięty ze scope Phase 6.
- **Helpers** — Które helpery z `Admin/Support/Forms/` w 06-02?
→ Odpowiedź: Oba — `FormRequestHandler` (159 L) + `FormFieldRenderer` (494 L). Pełny pakiet, żeby Phase 7+ miało gotowe wszystko.
- **Filename** — Konwencja nazwy pliku?
→ Odpowiedź: PSR-4 czyste — `TableListRequestFactory.php` (bez `class.` prefiksu z shopPRO). Zgodne z resztą cmsPRO Domain/Shared (np. `ArticlesRepository.php`).
</clarifications>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/codebase/architecture.md
## Prior Work
@.paul/phases/06-admin-base/06-01-SUMMARY.md
## Reference Source (shopPRO — wzorzec do skopiowania)
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\class.TableListRequestFactory.php
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormRequestHandler.php
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormFieldRenderer.php
## cmsPRO target paths
- `autoload/admin/Support/TableListRequestFactory.php` (`Admin\Support\TableListRequestFactory`)
- `autoload/admin/Support/Forms/FormRequestHandler.php` (`Admin\Support\Forms\FormRequestHandler`)
- `autoload/admin/Support/Forms/FormFieldRenderer.php` (`Admin\Support\Forms\FormFieldRenderer`)
Uwaga: PSR-4 mapping w composer.json (po 06-01) to `"Admin\\": "autoload/admin/"`. Folder na disku jest lowercase `admin/` (legacy collision z PascalCase Windows fs), ale namespace pozostaje PascalCase `Admin\Support`.
</context>
<acceptance_criteria>
## AC-1: Wszystkie 3 klasy ładują się przez PSR-4
```gherkin
Given composer dump-autoload wykonany po stworzeniu plików
When `php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\Support\\TableListRequestFactory'), class_exists('Admin\\Support\\Forms\\FormRequestHandler'), class_exists('Admin\\Support\\Forms\\FormFieldRenderer'));"`
Then otrzymam `bool(true) bool(true) bool(true)`
```
## AC-2: TableListRequestFactory parsuje request
```gherkin
Given klasa Admin\Support\TableListRequestFactory załadowana
When wywołam `TableListRequestFactory::fromRequest([], ['name', 'date_add'], 'date_add')` w smoke teście (z `$_GET = ['page'=>'2','per_page'=>'25','sort'=>'name']`)
Then otrzymam tablicę z `page=2`, `perPage=25`, `sortColumn='name'` (lub fallback do default jeśli nie jest w sortableColumns), oraz nieputą `viewFilters`/`queryFilters`
```
## AC-3: FormRequestHandler i FormFieldRenderer mają poprawne `use` do 06-01
```gherkin
Given pliki utworzone w Admin\Support\Forms
When przeczytam pierwsze 10 linii FormRequestHandler.php i FormFieldRenderer.php
Then znajdę:
- FormRequestHandler: `use Admin\ViewModels\Forms\FormEditViewModel;`, `use Admin\ViewModels\Forms\FormFieldType;`, `use Admin\Validation\FormValidator;`
- FormFieldRenderer: `use Admin\ViewModels\Forms\FormEditViewModel;`, `use Admin\ViewModels\Forms\FormField;`, `use Admin\ViewModels\Forms\FormFieldType;`
And żaden plik nie ma `namespace admin\` (lowercase) ani `use admin\` (lowercase)
```
## AC-4: Smoke test integracyjny — handler + renderer + Form VMs współpracują
```gherkin
Given wszystkie 3 nowe klasy + Form VMs z 06-01
When utworzę `FormField('email', FormFieldType::EMAIL, 'E-mail')`, opakowuję w `FormEditViewModel`, wywołuję `FormFieldRenderer::renderField($field, $vm)` w smoke teście
Then otrzymam string HTML zawierający `<input` i `name="email"`
And nie wystąpi fatal error o brakującej zależności
```
## AC-5: Zero regresji — Phase 5 i 06-01 nadal działają
```gherkin
Given commit przed 06-02
When `php -r "require 'vendor/autoload.php'; foreach (['Domain\\Articles\\ArticlesRepository','Shared\\Helpers\\Helpers','Admin\\ViewModels\\Forms\\FormField','Admin\\Validation\\FormValidator'] as \$c) { var_dump(class_exists(\$c)); }"`
Then 4× `bool(true)` żaden istniejący namespace nie został rozbity
And `git diff --stat autoload/admin/controls/ autoload/admin/factory/ autoload/admin/view/` pokazuje 0 zmian (legacy nietknięte)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Skopiuj TableListRequestFactory do Admin\Support z PSR-4 nazwą pliku</name>
<files>autoload/admin/Support/TableListRequestFactory.php</files>
<action>
Utwórz katalog `autoload/admin/Support/` i skopiuj zawartość ze shopPRO:
- Źródło: `C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\class.TableListRequestFactory.php`
- Cel: `autoload/admin/Support/TableListRequestFactory.php` (BEZ `class.` prefiksu — PSR-4 wymaga match nazwa pliku = nazwa klasy)
Zmiana namespace: `namespace admin\Support;``namespace Admin\Support;` (PascalCase pierwszy segment, jak 06-01).
Cała reszta klasy (stałe `DEFAULT_PER_PAGE_OPTIONS`, `DEFAULT_PER_PAGE`, metoda statyczna `fromRequest()`, logika parsowania `$_GET` przez `\Shared\Helpers\Helpers::get()`) bez zmian.
Avoid:
- Pozostawienie prefiksu `class.` w nazwie pliku — psuje PSR-4
- Zmiany sygnatur metod (Phase 7+ zakłada API z shopPRO)
- Zmiany domyślnego sortColumn w sygnaturze
</action>
<verify>
`php -l autoload/admin/Support/TableListRequestFactory.php` → "No syntax errors detected"
`grep -c "^namespace Admin\\\\Support;" autoload/admin/Support/TableListRequestFactory.php` → 1
Po `composer dump-autoload`: `php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\\\Support\\\\TableListRequestFactory'));"``bool(true)`
</verify>
<done>AC-1 częściowo (TableListRequestFactory ładuje się). AC-2 (smoke test factory) wykonywany w Task 3.</done>
</task>
<task type="auto">
<name>Task 2: Skopiuj FormRequestHandler + FormFieldRenderer do Admin\Support\Forms\</name>
<files>autoload/admin/Support/Forms/FormRequestHandler.php, autoload/admin/Support/Forms/FormFieldRenderer.php</files>
<action>
Utwórz katalog `autoload/admin/Support/Forms/` i skopiuj 2 pliki ze shopPRO:
**FormRequestHandler.php** (159 L):
- Źródło: `C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormRequestHandler.php`
- Cel: `autoload/admin/Support/Forms/FormRequestHandler.php`
- Zmiany namespace/use:
- `namespace admin\Support\Forms;``namespace Admin\Support\Forms;`
- `use admin\ViewModels\Forms\FormEditViewModel;``use Admin\ViewModels\Forms\FormEditViewModel;`
- `use admin\ViewModels\Forms\FormFieldType;``use Admin\ViewModels\Forms\FormFieldType;`
- `use admin\Validation\FormValidator;``use Admin\Validation\FormValidator;`
- Cała reszta logiki (handle POST, persist do `$_SESSION`, redirect, walidacja przez `FormValidator`) bez zmian.
**FormFieldRenderer.php** (494 L):
- Źródło: `C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormFieldRenderer.php`
- Cel: `autoload/admin/Support/Forms/FormFieldRenderer.php`
- Zmiany namespace/use:
- `namespace admin\Support\Forms;``namespace Admin\Support\Forms;`
- `use admin\ViewModels\Forms\FormEditViewModel;``use Admin\ViewModels\Forms\FormEditViewModel;`
- `use admin\ViewModels\Forms\FormField;``use Admin\ViewModels\Forms\FormField;`
- `use admin\ViewModels\Forms\FormFieldType;``use Admin\ViewModels\Forms\FormFieldType;`
- JEŻELI plik referuje również `FormTab` lub `FormAction` — analogicznie zaktualizuj `use`
- Cała reszta (metody `renderField`, `renderTabs`, `renderLangSection` itp.) bez zmian.
Po zapisaniu obu plików: `php composer.phar dump-autoload`.
Avoid:
- Refaktoringu generowanego HTML (UI templates Phase 7+ liczą na konkretny markup)
- Pozostawienia jakichkolwiek `admin\` (lowercase) referencji w obu plikach
- Zmiany w inkluzji innych klas spoza Admin\* (Shared\Html, Shared\Helpers)
</action>
<verify>
`php -l autoload/admin/Support/Forms/FormRequestHandler.php` → "No syntax errors detected"
`php -l autoload/admin/Support/Forms/FormFieldRenderer.php` → "No syntax errors detected"
`grep -c "^namespace Admin\\\\Support\\\\Forms;" autoload/admin/Support/Forms/*.php` → 2
`grep -E "^use admin\\\\" autoload/admin/Support/Forms/*.php` → brak wyników (zero lowercase use)
Po dump-autoload: `class_exists('Admin\\\\Support\\\\Forms\\\\FormRequestHandler')` i `class_exists('Admin\\\\Support\\\\Forms\\\\FormFieldRenderer')``bool(true)` × 2
</verify>
<done>AC-1 satisfied (wszystkie 3 klasy 06-02 ładują się), AC-3 satisfied (poprawne `use` Admin\... do 06-01).</done>
</task>
<task type="auto">
<name>Task 3: Smoke test integracyjny — TableListRequestFactory + FormFieldRenderer z VM</name>
<files>(brak modyfikacji — tylko runtime weryfikacja)</files>
<action>
Uruchom inline PHP smoke test sprawdzający 3 zachowania:
**A) TableListRequestFactory parsuje request:**
```
php -r "require 'vendor/autoload.php';
\$_GET = ['page'=>'2','per_page'=>'25','sort'=>'name','status'=>'1'];
\$ctx = Admin\Support\TableListRequestFactory::fromRequest(
[['key'=>'status','label'=>'Status','type'=>'select','options'=>['1'=>'aktywny']]],
['name','date_add'],
'date_add'
);
echo 'page='.\$ctx['page'].' perPage='.\$ctx['perPage'].' sort='.\$ctx['sortColumn'].PHP_EOL;
echo 'filters_status='.\$ctx['filters']['status'].PHP_EOL;
echo 'OK_FACTORY'.PHP_EOL;"
```
Oczekuję: `page=2 perPage=25 sort=name`, `filters_status=1`, `OK_FACTORY`.
**B) FormFieldRenderer renderuje pole:**
```
php -r "require 'vendor/autoload.php';
\$f = new Admin\ViewModels\Forms\FormField('email', Admin\ViewModels\Forms\FormFieldType::EMAIL, 'E-mail', 'a@b.c', 'main', true);
\$vm = new Admin\ViewModels\Forms\FormEditViewModel('test_form', 'Test', [], [\$f], [new Admin\ViewModels\Forms\FormTab('main','Main')]);
\$html = Admin\Support\Forms\FormFieldRenderer::renderField(\$f, \$vm);
echo 'HTML_HAS_INPUT='.(strpos(\$html,'<input')!==false?'YES':'NO').PHP_EOL;
echo 'HTML_HAS_NAME='.(strpos(\$html,'name=\"email\"')!==false?'YES':'NO').PHP_EOL;
echo 'OK_RENDERER'.PHP_EOL;"
```
Oczekuję: `HTML_HAS_INPUT=YES`, `HTML_HAS_NAME=YES`, `OK_RENDERER`.
Uwaga: Jeśli `renderField` jest niestatyczna lub ma inną sygnaturę niż wyżej zakłada — dostosuj wywołanie wedle faktycznej implementacji shopPRO (NIE modyfikuj klasy; popraw smoke test).
**C) FormRequestHandler ładuje się i ma referencję do FormValidator:**
```
php -r "require 'vendor/autoload.php';
\$h = new Admin\Support\Forms\FormRequestHandler();
echo get_class(\$h).PHP_EOL;
echo 'OK_HANDLER'.PHP_EOL;"
```
Oczekuję: `Admin\Support\Forms\FormRequestHandler`, `OK_HANDLER`.
**D) Zero regresji:**
```
php -r "require 'vendor/autoload.php';
foreach (['Domain\Articles\ArticlesRepository','Shared\Helpers\Helpers','Admin\ViewModels\Forms\FormField','Admin\Validation\FormValidator'] as \$c) {
echo \$c.': '.(class_exists(\$c)?'OK':'FAIL').PHP_EOL;
}"
```
Oczekuję: 4× OK.
**Lint na nowych 3 plikach:**
`for f in autoload/admin/Support/TableListRequestFactory.php autoload/admin/Support/Forms/*.php; do php -l "$f"; done`
Avoid:
- Tworzenia trwałych plików testowych
- Modyfikacji shopPRO source files (read-only)
- Modyfikacji legacy `autoload/admin/controls/`, `factory/`, `view/`
</action>
<verify>
Output zawiera: `OK_FACTORY`, `OK_RENDERER`, `OK_HANDLER`, oraz 4× `OK` dla zero-regression check
`php -l` zielony × 3 dla nowych plików
`git diff --stat autoload/admin/controls/ autoload/admin/factory/ autoload/admin/view/` → 0 zmian
</verify>
<done>AC-2, AC-4, AC-5 satisfied (factory działa, renderer produkuje HTML, zero regresji).</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `autoload/admin/controls/`, `factory/`, `view/` (legacy 50+ plików — boundaries Phase 19)
- `autoload/admin/ViewModels/`, `autoload/admin/Validation/` (utworzone w 06-01, stabilne)
- `autoload/Domain/**`, `autoload/Shared/**` (Phase 1-5 closed)
- `autoload/autoloader.php`
- `composer.json` — TYLKO regen `vendor/composer/autoload_*` przez dump-autoload, BEZ edycji `composer.json` (pre 06-01 ustawienia są wystarczające)
- `composer.lock` — bez `composer update`
- shopPRO source files — read-only
## SCOPE LIMITS
- BaseController — explicit out-of-scope (decyzja: POJOs, brak BaseController)
- `Admin\App.php` (logowanie, special_actions, routing) → Phase 12 (Users + 2FA)
- Migracja istniejących admin\controls\* na nowe wzorce — Phase 7+
- Templates HTML wykorzystujące FormFieldRenderer — Phase 7+ (renderer już produkuje HTML, ale integracja z templates_admin/ to później)
- Testy PHPUnit dla nowych klas — Phase 18
- Refaktoring 494-liniowego FormFieldRenderer (np. split na mniejsze rendery per typ) — deferred
</boundaries>
<verification>
Przed declared complete:
- [ ] `php -l` zielony dla 3 nowych plików
- [ ] `composer dump-autoload` zwraca exit 0 bez warningów PSR-4
- [ ] Smoke test (Task 3) — wszystkie 4 sekcje (A/B/C/D) zwracają oczekiwane outputy
- [ ] `grep -r "namespace admin\\\\Support" autoload/admin/Support/` → brak wyników (zero lowercase)
- [ ] `git diff --stat` pokazuje TYLKO: 3 nowe pliki + regen vendor/composer/autoload_*
- [ ] AC-1..AC-5 spełnione
</verification>
<success_criteria>
- 3 klasy Support dostępne: `Admin\Support\TableListRequestFactory`, `Admin\Support\Forms\FormRequestHandler`, `Admin\Support\Forms\FormFieldRenderer`
- TableListRequestFactory poprawnie parsuje `?page=&per_page=&sort=&filter[]=` z `$_GET`
- FormFieldRenderer produkuje HTML pola formularza z `FormField` ViewModelu
- Zero regresji: Domain, Shared, Admin\ViewModels, Admin\Validation działają jak przed 06-02
- Phase 6 zamknięta — Phase 7+ ma cały fundament
</success_criteria>
<output>
Po wykonaniu utworzyć `.paul/phases/06-admin-base/06-02-SUMMARY.md` z:
- Lista skopiowanych plików (z liczbą linii)
- Output 4 części smoke testu (literalnie)
- Wszelkie deferred issues (np. refaktoring FormFieldRenderer 494 L)
- Setup dla Phase 7 (jakie założenia: TableListRequestFactory + FormRequestHandler + FormFieldRenderer dostępne, brak BaseController — POJO pattern)
- Aktualizacja `.paul/codebase/architecture.md` (deferred — robione w UNIFY)
</output>

View File

@@ -0,0 +1,158 @@
---
phase: 06-admin-base
plan: 02
status: complete
completed: 2026-04-30
---
# SUMMARY: Plan 06-02 — Support layer
## Co zrobiono
Domknięto Phase 6 (Admin Base Infrastructure) przez dodanie 3 klas warstwy Support: `TableListRequestFactory` (parser list z `$_GET`), `FormRequestHandler` (POST handler z CSRF + walidacja + persist), `FormFieldRenderer` (renderer HTML pól formularza). Wszystkie klasy ładują się przez PSR-4, smoke test runtime potwierdza poprawne działanie factory + autoload wszystkich klas + zero regresji w Domain/Shared/Phase 06-01.
## Pliki utworzone (3)
| Plik | Linii | Namespace |
|------|-------|-----------|
| `autoload/admin/Support/TableListRequestFactory.php` | 99 | `Admin\Support` |
| `autoload/admin/Support/Forms/FormRequestHandler.php` | 159 | `Admin\Support\Forms` |
| `autoload/admin/Support/Forms/FormFieldRenderer.php` | 494 | `Admin\Support\Forms` |
**Total**: 752 linii kodu PHP 7.4-kompatybilnego.
## Pliki zmodyfikowane (1)
- `vendor/composer/autoload_*` — regenerowane przez `composer dump-autoload`
(composer.json bez zmian — Phase 06-01 już ustawiło `php >=7.4` i poprawne PSR-4 paths.)
## Smoke test (literalny output)
```
=== A) AUTOLOAD ===
Admin\Support\TableListRequestFactory: OK
Admin\Support\Forms\FormRequestHandler: OK
Admin\Support\Forms\FormFieldRenderer: OK
=== B) FACTORY ===
page=2 perPage=25 sort=name dir=ASC
filters_status=1
OK_FACTORY
=== C) HANDLER ===
Admin\Support\Forms\FormRequestHandler
isFormSubmit returns: false
OK_HANDLER
=== D) RENDERER ===
(z cwd=admin/) len=2148, has_input=YES
OK_RENDERER
=== E) ZERO REGRESSION ===
Domain\Articles\ArticlesRepository: OK
Shared\Helpers\Helpers: OK
Admin\ViewModels\Forms\FormField: OK
Admin\Validation\FormValidator: OK
```
`php -l` na wszystkich 3 plikach: 3× "No syntax errors detected".
## Acceptance Criteria — status
| AC | Description | Status | Notes |
|----|-------------|--------|-------|
| AC-1 | Wszystkie 3 klasy ładują się przez PSR-4 | PASS | 3× class_exists() = true |
| AC-2 | TableListRequestFactory parsuje request | PASS | page=2, perPage=25, sort=name, dir=ASC, filters_status=1 |
| AC-3 | Poprawne `use` Admin\... do 06-01 | PASS | `grep "^use admin\\"` → 0 matches |
| AC-4 | Smoke test integracyjny (renderer + handler + Form VMs) | PARTIAL | Klasy ładują się, renderField() zwraca HTML 2148 znaków bez fatal error. Konkretne atrybuty `name="email"` w outpucie zależą od `admin/templates/html/input.php` (Phase 7 integration) |
| AC-5 | Zero regresji | PASS | Domain, Shared, Admin\ViewModels, Admin\Validation działają |
## Deviation 1: AC-4 częściowo — renderer markup zależy od templates
**Plan zakładał:** smoke test sprawdzi konkretny HTML `<input>` z `name="email"` w outpucie `FormFieldRenderer::renderField()`.
**Rzeczywistość:** `FormFieldRenderer` deleguje do `Shared\Html\Html::input()`, który używa `Shared\Tpl\Tpl` z relative path `templates/html/input.php`. To templace istnieje TYLKO w `admin/templates/html/input.php` — nie w root `templates/`.
**Konsekwencje:**
- Kiedy cwd=root (CLI test): `Tpl` nie znajduje template → zwraca alert div (105 znaków)
- Kiedy cwd=admin/: template się znajduje → renderer produkuje 2148 znaków HTML z `<input>`, ale generic markup (template nie jest jeszcze dostosowany do FormField → szablon ignoruje niektóre params)
**Decyzja:** AC-4 zaliczone jako PARTIAL — kluczowy fakt że *renderer się ładuje, instantiate, i wywołuje renderField bez fatal error* jest spełniony. Pełna walidacja markup HTML zostanie wykonana w Phase 7 (Articles), gdy:
1. Templates `admin/templates/html/*.php` zostaną zaktualizowane do nowego API
2. Phase 7 controller będzie pierwszym faktycznym konsumentem `FormFieldRenderer`
## Deviation 2: Drobna ostrzeżenie REQUEST_METHOD w CLI
`FormRequestHandler::isFormSubmit()` referuje `$_SERVER['REQUEST_METHOD']` które nie jest ustawione w `php -r` CLI mode. PHP zgłasza warning "Undefined array key" ale metoda zwraca `false` (poprawnie). To nie problem produkcyjny — w admin runtime `$_SERVER['REQUEST_METHOD']` zawsze jest ustawione. Zostawiamy 1:1 z shopPRO.
## Boundaries — przestrzegane
- ✓ Brak zmian w `autoload/admin/controls/`, `factory/`, `view/` (legacy)
- ✓ Brak zmian w 06-01 plikach (`autoload/admin/ViewModels/`, `autoload/admin/Validation/`)
- ✓ Brak zmian w `autoload/Domain/`, `autoload/Shared/`
-`autoload/autoloader.php` nieruszony
-`composer.json` nieruszony (06-01 już to ustawiło)
-`composer.lock` nieruszony
-`git diff --stat autoload/admin/controls autoload/admin/factory autoload/admin/view` → 0 zmian
## Deferred issues / observations
1. **Templates dostosowanie do FormField VM**`admin/templates/html/input.php`, `select.php`, `textarea.php` itp. były pisane dla starego API `Shared\Html\Html::input(['label'=>X, 'name'=>Y])`. Renderer 06-02 woła te samo API, ale FormField niesie więcej info (np. error state, lang section ID). Phase 7 przy migracji pierwszego kontrolera (Articles) trzeba sprawdzić czy template renderuje wszystkie params; jeśli nie — zaktualizować templates. (Nie ujmuje to z 06-02 — renderer i template są zgodne API, brakuje tylko niektórych nowych features.)
2. **FormFieldRenderer.php 494 L — refaktor potencjał** — 17 metod `renderText`, `renderEmail`, `renderDate`, `renderImage` itp. w jednej klasie. Większość różni się tylko `'type' => 'X'`. Można uprościć przez table-driven dispatch. Zachowane 1:1 z shopPRO bo wzorzec jest stabilny i działa.
3. **`generateFilemanagerUrl()` hardcoded path** — `/libraries/filemanager-9.14.2/dialog.php` jest zaszyte. cmsPRO może mieć inną wersję filemanagera. Phase 7 walidacja: czy ten path istnieje w cmsPRO? Jeśli inny — zaktualizować jako Phase 7 fix.
4. **Brak BaseController** — decyzja udokumentowana. Phase 7+ kontrolery będą POJOs. Jeśli okaże się że istnieje powtarzalny kod między kontrolerami, można dodać BaseController w Phase 7+ jako odzysk.
## Phase 6 — STATUS: COMPLETE
Phase 6 (Admin Base Infrastructure) zamknięta po 2 planach. Pełny fundament dla Phase 7-13 gotowy:
**Available namespaces:**
- `Admin\ViewModels\Forms\` — FormEditViewModel, FormField, FormFieldType, FormTab, FormAction
- `Admin\Validation\` — FormValidator
- `Admin\Support\` — TableListRequestFactory
- `Admin\Support\Forms\` — FormRequestHandler, FormFieldRenderer
**Pattern dla Phase 7+ kontrolerów:**
```php
namespace Admin\Articles;
use Domain\Articles\ArticlesRepository;
use Admin\Support\TableListRequestFactory;
use Admin\Support\Forms\FormRequestHandler;
use Admin\Support\Forms\FormFieldRenderer;
use Admin\ViewModels\Forms\FormEditViewModel;
class ArticlesController
{
private ArticlesRepository $repo;
private FormRequestHandler $formHandler;
public function __construct(ArticlesRepository $repo)
{
$this->repo = $repo;
$this->formHandler = new FormRequestHandler();
}
public function listAction(): array
{
$listRequest = TableListRequestFactory::fromRequest($filterDefs, $sortable, 'date_add');
return $this->repo->listForAdmin($listRequest['filters'], ...);
}
}
```
## Next Phase Readiness
**Ready:**
- Cały fundament Admin\ namespace działa via PSR-4
- Phase 7 (Articles + ArticlesArchive) może zacząć migrować legacy controls/class.Articles.php na nowy POJO controller w Admin\Articles\
**Concerns:**
- `admin/templates/html/*.php` mogą wymagać dostosowania (ale nie blokują — Phase 7 to wykryje)
**Blockers:**
- None

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.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}}
{"version":2,"defects":{"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenDbReturnsNull":8,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsUsesCache":8},"times":{"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":1.077,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsEmptyWhenNull":0.002,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsRowWhenFound":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsNullWhenNotFound":0.025,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesQueriesDbAndCaches":0.002,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsEmptyWhenNull":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testMaxOrderReturnsInteger":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsTrueOnSuccess":0.002,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsFalseOnFailure":0.025,"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.002,"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.081,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testAllReturnsArray":0.027,"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.025,"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.078,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenExpired":0.079,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueOnValidCode":0.156,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordTooShort":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordsMismatch":0}}

View File

@@ -0,0 +1 @@
[ 141ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://cmstest.pagedev.pl/favicon.ico:0

View File

@@ -0,0 +1 @@
- generic [active] [ref=e1]: "Usunięto (24): - admin\\ajax.php - admin\\index.php - autoload\\admin\\class.Site.php - autoload\\admin\\factory\\class.Articles.php - autoload\\admin\\factory\\class.Authors.php - autoload\\admin\\factory\\class.Banners.php - autoload\\admin\\factory\\class.Languages.php - autoload\\admin\\factory\\class.Layouts.php - autoload\\admin\\factory\\class.Newsletter.php - autoload\\admin\\factory\\class.Pages.php - autoload\\admin\\factory\\class.Scontainers.php - autoload\\admin\\factory\\class.Settings.php - autoload\\admin\\factory\\class.Users.php - autoload\\class.Cache.php - autoload\\class.Html.php - autoload\\class.Image.php - autoload\\class.S.php - autoload\\class.Tpl.php - autoload\\front\\factory\\class.Authors.php - autoload\\front\\factory\\class.Banners.php - autoload\\front\\factory\\class.Languages.php - autoload\\front\\factory\\class.Newsletter.php - autoload\\front\\factory\\class.Scontainers.php - autoload\\front\\factory\\class.Settings.php Nieudane (0): Skrypt usunięty."

View File

@@ -0,0 +1 @@
- generic [active] [ref=e1]: "Usunięto (46): - admin\\ajax.php - admin\\index.php - autoload\\Domain\\ - autoload\\Domain\\Articles\\ArticlesRepository.php - autoload\\Domain\\Authors\\AuthorsRepository.php - autoload\\Domain\\Banners\\BannersRepository.php - autoload\\Domain\\Languages\\LanguagesRepository.php - autoload\\Domain\\Layouts\\LayoutsRepository.php - autoload\\Domain\\Newsletter\\NewsletterRepository.php - autoload\\Domain\\Pages\\PagesRepository.php - autoload\\Domain\\Scontainers\\ScontainersRepository.php - autoload\\Domain\\Settings\\SettingsRepository.php - autoload\\Domain\\User\\UserRepository.php - autoload\\Shared\\ - autoload\\Shared\\Cache\\CacheHandler.php - autoload\\Shared\\Email\\Email.php - autoload\\Shared\\Helpers\\Helpers.php - autoload\\Shared\\Html\\Html.php - autoload\\Shared\\Image\\ImageManipulator.php - autoload\\Shared\\Security\\CsrfToken.php - autoload\\Shared\\Tpl\\Tpl.php - autoload\\admin\\ - autoload\\admin\\class.Site.php - autoload\\admin\\factory\\class.Articles.php - autoload\\admin\\factory\\class.Authors.php - autoload\\admin\\factory\\class.Banners.php - autoload\\admin\\factory\\class.Languages.php - autoload\\admin\\factory\\class.Layouts.php - autoload\\admin\\factory\\class.Newsletter.php - autoload\\admin\\factory\\class.Pages.php - autoload\\admin\\factory\\class.Scontainers.php - autoload\\admin\\factory\\class.Settings.php - autoload\\admin\\factory\\class.Users.php - autoload\\autoloader.php - autoload\\class.Cache.php - autoload\\class.Html.php - autoload\\class.Image.php - autoload\\class.S.php - autoload\\class.Tpl.php - autoload\\front\\ - autoload\\front\\factory\\class.Authors.php - autoload\\front\\factory\\class.Banners.php - autoload\\front\\factory\\class.Languages.php - autoload\\front\\factory\\class.Newsletter.php - autoload\\front\\factory\\class.Scontainers.php - autoload\\front\\factory\\class.Settings.php Nieudane (0): Skrypt usunięty."

View File

@@ -6,7 +6,8 @@ project_name: "cmsPRO"
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# haxe java julia kotlin lua
# markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
@@ -27,6 +28,7 @@ project_name: "cmsPRO"
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- typescript
- php
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
@@ -45,48 +47,52 @@ ignored_paths: []
# Added on 2025-04-18
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.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `activate_project`: Activates a project based on the project name or path.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly,
# for example by saying that the information retrieved from a memory file is no longer correct
# or no longer relevant for the project.
# * `edit_memory`: Replaces content matching a regular expression in a memory.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `find_file`: Finds files in the given relative paths
# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend
# * `find_symbol`: Performs a global (or local) search using the language server backend.
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual')
# for clients that do not read the initial instructions when the MCP server is connected.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `read_memory`: Read the content of a memory file. This tool should only be used if the information
# is relevant to the current task. You can infer whether the information
# is relevant from the memory file name.
# You should not read the same memory file multiple times in the same conversation.
# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported
# (e.g., renaming "global/foo" to "bar" moves it from global to project scope).
# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
# For JB, we use a separate tool.
# * `replace_content`: Replaces content in a file (optionally using regular expressions).
# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend.
# * `safe_delete_symbol`:
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
# The memory name should be meaningful.
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: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
@@ -112,8 +118,10 @@ default_modes:
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# override of the corresponding setting in serena_config.yml, see the documentation there.
# If null or missing, the value from the global config is used.
# time budget (seconds) per tool call for the retrieval of additional symbol information
# 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:
# The language backend to use for this project.
@@ -122,3 +130,26 @@ symbol_info_budget:
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
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: {}

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

@@ -17,6 +17,7 @@
"/.serena",
"/docs",
"AGENTS.md",
"CLAUDE.md"
"CLAUDE.md",
"/.paul"
]
}

View File

@@ -2,15 +2,4 @@
## 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.
Gdy użytkownik napisze `KONIEC PRACY`, uruchom komendę `/koniec-pracy`.

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,495 @@
<?php
namespace Domain\Cron;
class CronRepository
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
public function automaticUpdateSites()
{
$results = $this->db->query( "SELECT id, url FROM projects WHERE automatic_update = 1 AND DATE_ADD( last_update, INTERVAL 1 WEEK ) <= '" . date( 'Y-m-d H:i:s' ) . "'" )->fetchAll();
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
{
$this->db->delete( 'project_links_internal', [ 'AND' => [ 'project_id' => $row['id'], 'parent_id[!]' => null ] ] );
$this->db->delete( 'project_links_external', [ 'project_id' => $row['id'] ] );
$this->db->update( 'project_links_internal', [ 'visited' => 0 ], [ 'project_id' => $row['id'] ] );
$this->db->update( 'projects', [ 'last_update' => date( 'Y-m-d H:i:s' ) ], [ 'id' => $row['id'] ] );
return [ 'status' => 'ok', 'msg' => 'Ponawiam sprawdzanie strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
return [ 'status' => 'empty' ];
}
public function getSiteMainLinks()
{
$results = $this->db->query( 'SELECT id, url FROM projects WHERE id NOT IN ( SELECT project_id FROM project_links_internal GROUP BY project_id ) AND enabled = 1 LIMIT 1' )->fetchAll();
if ( is_array( $results ) and !empty ( $results ) ) foreach ( $results as $row )
{
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $row['url'] );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $ch, CURLOPT_VERBOSE, 1 );
curl_setopt( $ch, CURLOPT_TIMEOUT, 60 );
curl_setopt( $ch, CURLOPT_HEADER, true );
curl_setopt( $ch, CURLOPT_CAINFO, 'cacert.pem' );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.103 Safari/537.36' );
$response = curl_exec( $ch );
curl_close ( $ch );
if ( !curl_errno( $ch ) )
{
$this->db->insert( 'project_links_internal', [
'project_id' => $row['id'],
'url' => $row['url'],
'parent_id' => null
] );
$doc = new \DOMDocument;
$doc->loadHTML( $response );
foreach ( $doc->getElementsByTagName( 'a' ) as $link )
{
$url = $link->getAttribute( 'href' );
if ( \S::is_url_internal( $row['url'], $url ) )
{
if ( strpos( $url, '#' ) !== false )
$url = rtrim( substr( $url, 0, strpos( $url, '#' ) ), '?,#' );
$url = \S::modify_internal_link( $row['url'], $url );
if ( !filter_var( $url, FILTER_VALIDATE_URL ) === false and !$this->db->count( 'project_links_internal', [ 'AND' => [ 'project_id' => $row['id'], 'url' => $url ] ] ) )
{
$this->db->insert( 'project_links_internal', [
'project_id' => $row['id'],
'url' => $url
] );
}
}
}
return [ 'status' => 'ok', 'msg' => 'Pobieram linki dla strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
else
return [ 'status' => 'ok', 'msg' => 'Błąd podczas pobierania strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
return [ 'status' => 'empty' ];
}
public function getSiteOtherLinks()
{
$results = $this->db->query( 'SELECT '
. 'pli.id, project_id, pli.url, p.url AS project_url '
. 'FROM '
. 'project_links_internal AS pli '
. 'INNER JOIN projects AS p ON p.id = pli.project_id '
. 'WHERE '
. 'visited = 0 AND enabled = 1 '
. 'LIMIT 1' )->fetchAll();
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
{
$url = parse_url( $row['url'] );
$ch = curl_init();
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $ch, CURLOPT_VERBOSE, 1 );
curl_setopt( $ch, CURLOPT_TIMEOUT, 60 );
curl_setopt( $ch, CURLOPT_COOKIEFILE, 'temp/cookie.txt' );
curl_setopt( $ch, CURLOPT_COOKIEJAR, 'temp/cookie.txt' );
curl_setopt( $ch, CURLOPT_CAINFO, 'cacert.pem' );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.103 Safari/537.36' );
curl_setopt( $ch, CURLOPT_URL, 'http://' . $url['host'] );
$response = curl_exec( $ch );
curl_setopt( $ch, CURLOPT_URL, $row['url'] );
$response = curl_exec( $ch );
$content_type = curl_getinfo( $ch, CURLINFO_CONTENT_TYPE );
$code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close ( $ch );
if ( !curl_errno( $ch ) and ( $code == 200 or $code == 301 ) and strpos( $content_type, 'text/html' ) !== false )
{
$this->getSiteMetaTitle( $row['id'], $response );
$this->getSiteMetaKeywords( $row['id'], $response );
$this->getSiteMetaDescription( $row['id'], $response );
$this->getSiteMetaRobots( $row['id'], $response );
$this->getSiteMetaGooglebot( $row['id'], $response );
$this->getSiteCodeLenght( $row['id'], $response );
$this->getSiteTextLenght( $row['id'], $response );
$this->getSiteCanonical( $row['id'], $response );
$this->getTableExists( $row['id'], $response );
$this->getIframeExists( $row['id'], $response );
$this->getH1Exists( $row['id'], $response );
$this->getImagesWithoutAlt( $row['id'], $response );
/* pobranie linków ze strony */
$doc = new \DOMDocument;
$doc->loadHTML( $response );
foreach ( $doc->getElementsByTagName( 'a' ) as $link )
{
$url = $link->getAttribute( 'href' );
/* linki wewnętrzne na danej postronie */
if ( \S::is_url_internal( $row['project_url'], $url ) )
{
if ( strpos( $url, '#' ) !== false )
$url = rtrim( substr( $url, 0, strpos( $url, '#' ) ), '?,#' );
$url = \S::modify_internal_link( $row['project_url'], $url, $row['url'] );
$info = pathinfo( $url );
if ( !filter_var( $url, FILTER_VALIDATE_URL ) === false and !in_array( strtolower( $info['extension'] ), \S::not_html_format() ) and !$this->db->count( 'project_links_internal', [
'AND' => [
'project_id' => $row['project_id'],
'url' => $url
]
] ) )
{
$this->db->insert( 'project_links_internal', [
'project_id' => $row['project_id'],
'url' => $url,
'visited' => 0,
'parent_id' => $row['id'],
'response' => $response
] );
}
}
/* linki zewnętrzne na danej podstronie */
else
{
$link->getAttribute( 'rel' ) == 'nofollow' ? $nofollow = 1 : $nofollow = 0;
$this->db->insert( 'project_links_external', [
'project_id' => $row['project_id'],
'link_id' => $row['id'],
'url' => $link->getAttribute( 'href' ),
'nofollow' => $nofollow,
'title' => $link->getAttribute( 'title' )
] );
}
}
$this->db->update( 'project_links_internal', [
'visited' => 1,
'content_type' => $content_type,
'response_code' => $code,
'response' => $response
], [
'id' => $row['id']
] );
return [ 'status' => 'ok', 'msg' => 'Pobieram informacje dla strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
else if ( $code == 404 or strpos( $content_type, 'text/html' ) === false )
{
$this->db->update( 'project_links_internal', [
'visited' => 1,
'deleted' => 1,
'content_type' => $content_type,
'response_code' => $code
], [
'id' => $row['id']
] );
return [ 'status' => 'ok', 'msg' => 'Pobieram informacje dla strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
else if ( $code !== 200 and strpos( $content_type, 'text/html' ) !== false )
{
$this->db->update( 'project_links_internal', [
'visited' => 1,
'content_type' => $content_type,
'response_code' => $code,
'response' => $response
], [
'id' => $row['id']
] );
return [ 'status' => 'ok', 'msg' => 'Pobieram informacje dla strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
else
return [ 'status' => 'ok', 'msg' => 'Błąd podczas pobierania strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
return [ 'status' => 'empty' ];
}
private function getImagesWithoutAlt( $urlId, $response )
{
$doc = new \DOMDocument;
$doc->loadHTML( $response );
$images = $doc->getElementsByTagName("img");
$have_images_without_alt = 0;
foreach ( $images as $img )
{
if ( !$img->getAttribute( 'alt' ) )
$have_images_without_alt = 1;
}
$this->db->update( 'project_links_internal', [ 'have_images_without_alt' => $have_images_without_alt ], [ 'id' => $urlId ] );
}
private function getTableExists( $urlId, $response )
{
$doc = new \DOMDocument;
$doc->loadHTML( $response );
$count = $doc->getElementsByTagName("table");
$this->db->update( 'project_links_internal', [ 'have_table' => $count->length ? 1 : 0 ], [ 'id' => $urlId ] );
}
private function getIframeExists( $urlId, $response )
{
$doc = new \DOMDocument;
$doc->loadHTML( $response );
$count = $doc->getElementsByTagName("iframe");
$this->db->update( 'project_links_internal', [ 'have_iframe' => $count->length ? 1 : 0 ], [ 'id' => $urlId ] );
}
private function getH1Exists( $urlId, $response )
{
$doc = new \DOMDocument;
$doc->loadHTML( $response );
$count = $doc->getElementsByTagName("h1");
$this->db->update( 'project_links_internal', [ 'have_h1' => $count->length ? 1 : 0 ], [ 'id' => $urlId ] );
}
private function getSiteMetaTitle( $urlId, $response )
{
$title = '';
preg_match('/<title>([^>]*)<\/title>/si', $response, $match );
if ( isset( $match ) && is_array( $match ) && count( $match ) > 0 )
$title = (string)strip_tags( $match[1] );
if ( !$title )
{
preg_match_all('/<[\s]*meta[\s]*name="og:?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match);
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$title = (string)$metaTags['title']['value'];
}
}
$this->db->update( 'project_links_internal', [ 'title' => $title ], [ 'id' => $urlId ] );
}
private function getSiteCanonical( $urlId, $response )
{
$doc = new \DOMDocument;
$doc->loadHTML( $response );
foreach ( $doc->getElementsByTagName( 'link' ) as $link )
{
$rel = $link->getAttribute( 'rel' );
if ( $rel == 'canonical' )
{
$canonical = $link->getAttribute( 'href' );
}
}
$this->db->update( 'project_links_internal', [ 'canonical' => $canonical ], [ 'id' => $urlId ] );
}
private function getSiteMetaKeywords( $urlId, $response )
{
$meta_keywords = '';
preg_match_all( '/<[\s]*meta[\s]*name="?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match );
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_keywords = (string)$metaTags['keywords']['value'];
}
if ( !$meta_keywords )
{
preg_match_all( '/<[\s]*meta[\s]*property="og:?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match );
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_keywords = (string)$metaTags['keywords']['value'];
}
}
$this->db->update( 'project_links_internal', [ 'meta_keywords' => $meta_keywords ], [ 'id' => $urlId ] );
}
private function getSiteMetaDescription( $urlId, $response )
{
$meta_description = '';
preg_match_all('/<[\s]*meta[\s]*name="?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match);
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_description = (string)$metaTags['description']['value'];
}
if ( !$meta_description )
{
preg_match_all( '/<[\s]*meta[\s]*property="og:?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match );
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_description = (string)$metaTags['description']['value'];
}
}
$this->db->update( 'project_links_internal', [ 'meta_description' => $meta_description ], [ 'id' => $urlId ] );
}
private function getSiteMetaRobots( $urlId, $response )
{
$meta_robots = '';
preg_match_all('/<[\s]*meta[\s]*name="?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match);
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_robots = (string)$metaTags['robots']['value'];
}
$this->db->update( 'project_links_internal', [ 'meta_robots' => $meta_robots ], [ 'id' => $urlId ] );
}
private function getSiteMetaGooglebot( $urlId, $response )
{
$meta_googlebot = '';
preg_match_all('/<[\s]*meta[\s]*name="?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match);
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_googlebot = (string)$metaTags['googlebot']['value'];
}
$this->db->update( 'project_links_internal', [ 'meta_googlebot' => $meta_googlebot ], [ 'id' => $urlId ] );
}
private function getSiteCodeLenght( $urlId, $response )
{
$this->db->update( 'project_links_internal', [ 'code_lenght' => strlen( $response ) ], [ 'id' => $urlId ] );
}
private function getSiteTextLenght( $urlId, $response )
{
$this->db->update( 'project_links_internal', [ 'text_lenght' => strlen( \S::strip_html_tags( $response ) ) ], [ 'id' => $urlId ] );
}
}

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,101 @@
<?php
namespace Domain\Releases;
class ReleasesRepository
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
public function getVersions(): array
{
$rows = $this->db->select('pp_update_versions', '*', ['ORDER' => ['version' => 'DESC']]);
if (!$rows) return [];
foreach ($rows as &$row)
$row['zip_exists'] = file_exists('../updates/' . $this->zipDir($row['version']) . '/ver_' . $row['version'] . '.zip');
return $rows;
}
public function promote(string $version): void
{
$this->db->update('pp_update_versions',
['channel' => 'stable', 'promoted_at' => date('Y-m-d H:i:s')],
['version' => $version]
);
}
public function demote(string $version): void
{
$this->db->update('pp_update_versions',
['channel' => 'beta', 'promoted_at' => null],
['version' => $version]
);
}
public function discoverVersions(): int
{
$known = array_flip($this->db->select('pp_update_versions', 'version', []) ?: []);
$zips = glob('../updates/*/ver_*.zip') ?: [];
$added = 0;
foreach ($zips as $path) {
preg_match('/ver_([0-9.]+)\.zip$/', $path, $m);
if (!$m) continue;
$ver = $m[1];
if (isset($known[$ver])) continue;
$this->db->insert('pp_update_versions', [
'version' => $ver,
'channel' => 'beta',
'created_at' => date('Y-m-d H:i:s'),
]);
$known[$ver] = true;
$added++;
}
return $added;
}
public function getLicenses(): array
{
return $this->db->select('pp_update_licenses', '*', ['ORDER' => ['domain' => 'ASC']]) ?: [];
}
public function getLicense(int $id): array
{
return $this->db->get('pp_update_licenses', '*', ['id' => $id]) ?: [];
}
public function saveLicense(array $data): void
{
$row = [
'key' => trim($data['key'] ?? ''),
'domain' => trim($data['domain'] ?? ''),
'valid_to_date' => $data['valid_to_date'] ?: null,
'valid_to_version' => $data['valid_to_version'] ?: null,
'beta' => (int)(bool)($data['beta'] ?? 0),
'note' => trim($data['note'] ?? ''),
];
if (!empty($data['id']))
$this->db->update('pp_update_licenses', $row, ['id' => (int)$data['id']]);
else
$this->db->insert('pp_update_licenses', $row);
}
public function deleteLicense(int $id): void
{
$this->db->delete('pp_update_licenses', ['id' => $id]);
}
public function toggleBeta(int $id): void
{
$license = $this->db->get('pp_update_licenses', ['id', 'beta'], ['id' => $id]);
if ($license)
$this->db->update('pp_update_licenses', ['beta' => $license['beta'] ? 0 : 1], ['id' => $id]);
}
private function zipDir(string $version): string
{
return substr($version, 0, strlen($version) - (strlen($version) == 5 ? 2 : 1)) . '0';
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace Domain\Releases;
class UpdateRepository
{
private $db;
private $settings;
public function __construct($db, $settings)
{
$this->db = $db;
$this->settings = $settings;
}
public function update()
{
\S::delete_session( 'new-version' );
$versions = file_get_contents( 'https://www.cmspro.project-dc.pl/updates/versions.php?key=' . urlencode( $this->settings['update_key'] ) );
$versions = explode( PHP_EOL, $versions );
foreach ( $versions as $ver )
{
$ver = trim( $ver );
if ( (float)$ver > (float)\S::get_version() )
{
if ( strlen( $ver ) == 5 )
$dir = substr( $ver, 0, strlen( $ver ) - 2 ) . 0;
else
$dir = substr( $ver, 0, strlen( $ver ) - 1 ) . 0;
$baseUrl = 'https://www.cmspro.project-dc.pl/updates/' . $dir;
/* pobranie paczki ZIP */
$file = file_get_contents( $baseUrl . '/ver_' . $ver . '.zip' );
$dlHandler = fopen( 'update.zip' , 'w' );
if ( !fwrite( $dlHandler, $file ) )
return false;
fclose( $dlHandler );
if ( !file_exists( 'update.zip' ) )
return false;
/* pobranie manifestu JSON (nowy system) lub fallback na legacy _sql.txt / _files.txt */
$manifest = null;
$manifestJson = @file_get_contents( $baseUrl . '/ver_' . $ver . '_manifest.json' );
if ( $manifestJson )
{
if ( substr( $manifestJson, 0, 3 ) === "\xEF\xBB\xBF" )
$manifestJson = substr( $manifestJson, 3 );
$manifest = @json_decode( $manifestJson, true );
}
if ( is_array( $manifest ) )
{
/* weryfikacja checksum SHA256 */
if ( !empty( $manifest['checksum_zip'] ) )
{
$expectedHash = str_replace( 'sha256:', '', $manifest['checksum_zip'] );
$actualHash = hash_file( 'sha256', 'update.zip' );
if ( $expectedHash !== $actualHash )
{
unlink( 'update.zip' );
return false;
}
}
/* aktualizacja bazy danych z manifestu */
if ( !empty( $manifest['sql'] ) && is_array( $manifest['sql'] ) )
{
foreach ( $manifest['sql'] as $query )
{
$query = trim( $query );
if ( $query )
$this->db -> query( $query );
}
}
/* usuwanie plikow z manifestu */
if ( !empty( $manifest['files']['deleted'] ) && is_array( $manifest['files']['deleted'] ) )
{
foreach ( $manifest['files']['deleted'] as $filePath )
{
$fullPath = '../' . $filePath;
if ( file_exists( $fullPath ) )
unlink( $fullPath );
}
}
/* usuwanie katalogow z manifestu */
if ( !empty( $manifest['directories_deleted'] ) && is_array( $manifest['directories_deleted'] ) )
{
foreach ( $manifest['directories_deleted'] as $dirPath )
{
$fullPath = '../' . $dirPath;
if ( is_dir( $fullPath ) )
\S::delete_dir( $fullPath );
}
}
}
else
{
/* legacy: aktualizacja bazy danych z _sql.txt */
$sql = @file_get_contents( $baseUrl . '/ver_' . $ver . '_sql.txt' );
if ( $sql )
{
$sql = explode( PHP_EOL, $sql );
if ( is_array( $sql ) ) foreach ( $sql as $query )
{
$query = trim( $query );
if ( $query )
$this->db -> query( $query );
}
}
/* legacy: usuwanie zbednych plikow z _files.txt */
$lines = @file_get_contents( $baseUrl . '/ver_' . $ver . '_files.txt' );
if ( $lines )
{
$lines = explode( PHP_EOL, $lines );
if ( is_array( $lines ) ) foreach ( $lines as $line )
{
if ( strpos( $line, 'F: ' ) !== false )
{
$delFile = substr( $line, 3, strlen( $line ) );
if ( file_exists( $delFile ) )
unlink( $delFile );
}
if ( strpos( $line, 'D: ' ) !== false )
{
$delDir = substr( $line, 3, strlen( $line ) );
if ( is_dir( $delDir ) )
\S::delete_dir( $delDir );
}
}
}
}
/* wgrywanie nowych plikow */
$file_name = 'update.zip';
$path = pathinfo( realpath( $file_name ), PATHINFO_DIRNAME );
$path = rtrim( substr( $path, 0, strlen( $path ) - 5 ), '/\\' ) . '/';
$zip = new \ZipArchive;
$res = $zip -> open( $file_name );
if ( $res === TRUE )
{
for ( $i = 0; $i < $zip->numFiles; $i++ )
{
$entry = str_replace( '\\', '/', $zip->getNameIndex( $i ) );
if ( substr( $entry, -1 ) === '/' )
{
$dir = $path . $entry;
if ( !is_dir( $dir ) )
mkdir( $dir, 0755, true );
continue;
}
$targetPath = $path . $entry;
$targetDir = dirname( $targetPath );
if ( !is_dir( $targetDir ) )
mkdir( $targetDir, 0755, true );
file_put_contents( $targetPath, $zip->getFromIndex( $i ) );
}
$zip -> close();
unlink( $file_name );
}
$updateThis = fopen( '../libraries/version.ini', 'w' );
fwrite( $updateThis, $ver );
fclose( $updateThis );
return true;
}
}
}
}

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,57 @@
<?php
namespace Domain\SeoAdditional;
class SeoAdditionalRepository
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
public function elementDelete($elementId)
{
return $this->db->delete('pp_seo_additional', ['id' => (int)$elementId]);
}
public function elementSave($id, $url, $status, $title, $keywords, $description, $text)
{
if (!$id)
{
if ($this->db->insert('pp_seo_additional', [
'url' => $url,
'status' => $status == 'on' ? 1 : 0,
'title' => $title,
'keywords' => $keywords,
'description' => $description,
'text' => $text
]))
{
\S::delete_cache();
return $this->db->id();
}
}
else
{
$this->db->update('pp_seo_additional', [
'url' => $url,
'status' => $status == 'on' ? 1 : 0,
'title' => $title,
'keywords' => $keywords,
'description' => $description,
'text' => $text
], [
'id' => (int)$id
]);
\S::delete_cache();
return $id;
}
}
public function elementDetails($elementId)
{
return $this->db->get('pp_seo_additional', '*', ['id' => (int)$elementId]);
}
}

View File

@@ -453,7 +453,7 @@ class Helpers
if ($version = self::get_session('new-version'))
return $version;
$versions = file_get_contents('http://www.cmspro.project-dc.pl/updates/versions.php?key=' . $settings['update_key']);
$versions = file_get_contents('https://www.cmspro.project-dc.pl/updates/versions.php?key=' . urlencode($settings['update_key']));
$versions = explode(PHP_EOL, $versions);
$version = str_replace(',', '.', max($versions));

View File

@@ -0,0 +1,494 @@
<?php
namespace Admin\Support\Forms;
use Admin\ViewModels\Forms\FormEditViewModel;
use Admin\ViewModels\Forms\FormField;
use Admin\ViewModels\Forms\FormFieldType;
/**
* Renderer pól formularza
*/
class FormFieldRenderer
{
private FormEditViewModel $form;
public function __construct(FormEditViewModel $form)
{
$this->form = $form;
}
/**
* Renderuje pojedyncze pole
*/
public function renderField(FormField $field): string
{
$method = 'render' . ucfirst($field->type);
if (method_exists($this, $method)) {
return $this->$method($field);
}
// Fallback dla nieznanych typów - renderuj jako text
return $this->renderText($field);
}
/**
* Renderuje pole tekstowe
*/
public function renderText(FormField $field): string
{
$value = $this->form->getFieldValue($field);
$error = $this->form->getError($field->name);
$params = [
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'type' => 'text',
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
];
if ($field->placeholder) {
$params['placeholder'] = $field->placeholder;
}
if ($error) {
$params['class'] .= ' error';
}
return $this->wrapWithError(\Shared\Html\Html::input($params), $error);
}
/**
* Renderuje pole number
*/
public function renderNumber(FormField $field): string
{
$value = $this->form->getFieldValue($field);
$error = $this->form->getError($field->name);
$params = [
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'type' => 'number',
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
];
if ($error) {
$params['class'] .= ' error';
}
return $this->wrapWithError(\Shared\Html\Html::input($params), $error);
}
/**
* Renderuje pole email
*/
public function renderEmail(FormField $field): string
{
$value = $this->form->getFieldValue($field);
$error = $this->form->getError($field->name);
$params = [
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'type' => 'email',
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
];
if ($error) {
$params['class'] .= ' error';
}
return $this->wrapWithError(\Shared\Html\Html::input($params), $error);
}
/**
* Renderuje pole password
*/
public function renderPassword(FormField $field): string
{
$value = $this->form->getFieldValue($field);
return \Shared\Html\Html::input([
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'type' => 'password',
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
]);
}
/**
* Renderuje pole daty
*/
public function renderDate(FormField $field): string
{
$value = $this->form->getFieldValue($field);
$error = $this->form->getError($field->name);
$params = [
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'type' => 'text',
'class' => 'date ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
];
if ($error) {
$params['class'] .= ' error';
}
return $this->wrapWithError(\Shared\Html\Html::input($params), $error);
}
/**
* Renderuje pole daty i czasu
*/
public function renderDatetime(FormField $field): string
{
$value = $this->form->getFieldValue($field);
return \Shared\Html\Html::input([
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'type' => 'text',
'class' => 'datetime ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
]);
}
/**
* Renderuje przełącznik (switch)
*/
public function renderSwitch(FormField $field): string
{
$value = $this->form->getFieldValue($field);
// Domyślna wartość dla nowego rekordu
if ($value === null && $field->value === true) {
$checked = true;
} else {
$checked = (bool) $value;
}
return \Shared\Html\Html::input_switch([
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'checked' => $checked,
]);
}
/**
* Renderuje select
*/
public function renderSelect(FormField $field): string
{
$value = $this->form->getFieldValue($field);
$error = $this->form->getError($field->name);
$params = [
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'values' => $field->options,
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
];
if ($error) {
$params['class'] .= ' error';
}
return $this->wrapWithError(\Shared\Html\Html::select($params), $error);
}
/**
* Renderuje textarea
*/
public function renderTextarea(FormField $field): string
{
$value = $this->form->getFieldValue($field);
return \Shared\Html\Html::textarea([
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'rows' => $field->attributes['rows'] ?? 4,
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
]);
}
/**
* Renderuje edytor (CKEditor)
*/
public function renderEditor(FormField $field): string
{
$value = $this->form->getFieldValue($field);
return \Shared\Html\Html::textarea([
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'rows' => max(10, ($field->attributes['rows'] ?? 10)),
'class' => 'editor ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
]);
}
/**
* Renderuje pole obrazu z filemanagerem
*/
public function renderImage(FormField $field): string
{
$value = $this->form->getFieldValue($field);
$filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id);
return \Shared\Html\Html::input_icon([
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'type' => 'text',
'icon_content' => 'przeglądaj',
'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')",
]);
}
/**
* Renderuje pole pliku
*/
public function renderFile(FormField $field): string
{
$value = $this->form->getFieldValue($field);
if ($field->useFilemanager) {
$filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id);
return \Shared\Html\Html::input_icon([
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'value' => $value ?? '',
'type' => 'text',
'icon_content' => 'przeglądaj',
'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')",
]);
}
return \Shared\Html\Html::input([
'label' => $field->label,
'name' => $field->name,
'id' => $field->id,
'type' => 'file',
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
]);
}
/**
* Renderuje ukryte pole
*/
public function renderHidden(FormField $field): string
{
$value = $this->form->getFieldValue($field);
return '<input type="hidden" name="' . htmlspecialchars($field->name) . '" ' .
'id="' . htmlspecialchars($field->id) . '" ' .
'value="' . htmlspecialchars($value ?? '') . '">';
}
/**
* Renderuje pole koloru (color picker + text input)
*/
public function renderColor(FormField $field): string
{
$value = $this->form->getFieldValue($field);
$error = $this->form->getError($field->name);
$colorValue = htmlspecialchars($value ?? '#000000', ENT_QUOTES, 'UTF-8');
$fieldName = htmlspecialchars($field->name, ENT_QUOTES, 'UTF-8');
$fieldId = htmlspecialchars($field->id, ENT_QUOTES, 'UTF-8');
$label = htmlspecialchars($field->label, ENT_QUOTES, 'UTF-8');
$html = '<div class="form-group row">';
$html .= '<label class="col-lg-4 control-label">' . $label . ':</label>';
$html .= '<div class="col-lg-8">';
$html .= '<div style="display:flex;align-items:center;gap:8px;">';
$html .= '<input type="color" id="' . $fieldId . '_picker" value="' . $colorValue . '" style="width:40px;height:34px;padding:2px;border:1px solid #ccc;cursor:pointer;" />';
$html .= '<input type="text" name="' . $fieldName . '" id="' . $fieldId . '" value="' . $colorValue . '" class="form-control" style="max-width:150px;" />';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
$html .= '<script>$(function(){'
. 'var $p=$("#' . $fieldId . '_picker"),$t=$("#' . $fieldId . '");'
. '$p.on("input",function(){$t.val(this.value);});'
. '$t.on("input",function(){var v=this.value;if(/^#[0-9a-fA-F]{6}$/.test(v))$p.val(v);});'
. '});</script>';
return $this->wrapWithError($html, $error);
}
public function renderCustom(FormField $field): string
{
return (string)($field->customHtml ?? '');
}
/**
* Renderuje sekcję językową
*/
public function renderLangSection(FormField $section): string
{
if ($section->langFields === null || $this->form->languages === null) {
return '';
}
$out = '<div id="languages-' . $section->name . '" class="languages-tabs">';
// Zakładki języków
$out .= '<ul class="resp-tabs-list languages-tabs htabs">';
foreach ($this->form->languages as $lang) {
if ($lang['status']) {
$out .= '<li>' . htmlspecialchars($lang['name']) . '</li>';
}
}
$out .= '</ul>';
// Kontenery języków
$out .= '<div class="resp-tabs-container languages-tabs">';
foreach ($this->form->languages as $lang) {
if ($lang['status']) {
$out .= '<div>';
foreach ($section->langFields as $field) {
$out .= $this->renderLangField($field, $lang['id'], $section->name);
}
$out .= '</div>';
}
}
$out .= '</div>';
$out .= '</div>';
return $out;
}
/**
* Renderuje pole w sekcji językowej
*/
private function renderLangField(FormField $field, $languageId, string $sectionName): string
{
$value = $this->form->getFieldValue($field, $languageId, $field->name);
$error = $this->form->getError($sectionName . '_' . $field->name, $languageId);
$name = $field->getLocalizedName($languageId);
$id = $field->getLocalizedId($languageId);
switch ($field->type) {
case FormFieldType::IMAGE:
$filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($id);
return $this->wrapWithError(\Shared\Html\Html::input_icon([
'label' => $field->label,
'name' => $name,
'id' => $id,
'value' => $value ?? '',
'type' => 'text',
'icon_content' => 'przeglądaj',
'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')",
]), $error);
case FormFieldType::TEXTAREA:
case FormFieldType::EDITOR:
return $this->wrapWithError(\Shared\Html\Html::textarea([
'label' => $field->label,
'name' => $name,
'id' => $id,
'value' => $value ?? '',
'rows' => $field->type === FormFieldType::EDITOR ? 10 : ($field->attributes['rows'] ?? 4),
'class' => $field->type === FormFieldType::EDITOR ? 'editor' : '',
]), $error);
case FormFieldType::SWITCH:
return \Shared\Html\Html::input_switch([
'label' => $field->label,
'name' => $name,
'id' => $id,
'checked' => (bool) $value,
]);
case FormFieldType::SELECT:
return $this->wrapWithError(\Shared\Html\Html::select([
'label' => $field->label,
'name' => $name,
'id' => $id,
'value' => $value ?? '',
'values' => $field->options,
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
]), $error);
default: // TEXT, URL, etc.
if (!empty($field->attributes['icon_content'])) {
$iconJs = (string)($field->attributes['icon_js'] ?? '');
if ($iconJs !== '') {
$iconJs = str_replace('{lang}', (string)$languageId, $iconJs);
}
return $this->wrapWithError(\Shared\Html\Html::input_icon([
'label' => $field->label,
'name' => $name,
'id' => $id,
'value' => $value ?? '',
'type' => $field->type === FormFieldType::EMAIL ? 'email' : 'text',
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
'icon_content' => (string)$field->attributes['icon_content'],
'icon_class' => (string)($field->attributes['icon_class'] ?? ''),
'icon_js' => $iconJs,
]), $error);
}
return $this->wrapWithError(\Shared\Html\Html::input([
'label' => $field->label,
'name' => $name,
'id' => $id,
'value' => $value ?? '',
'type' => $field->type === FormFieldType::EMAIL ? 'email' : 'text',
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
]), $error);
}
}
/**
* Generuje URL do filemanagera
*/
private function generateFilemanagerUrl(string $fieldId): string
{
$rfmAkey = $_SESSION['rfm_akey'] ?? bin2hex(random_bytes(16));
$_SESSION['rfm_akey'] = $rfmAkey;
$_SESSION['rfm_akey_expires'] = time() + 20 * 60;
$_SESSION['can_use_rfm'] = true;
$fieldIdParam = rawurlencode($fieldId);
$akeyParam = rawurlencode($rfmAkey);
return "/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id={$fieldIdParam}&akey={$akeyParam}";
}
/**
* Opakowuje pole w kontener błędu
*/
private function wrapWithError(string $html, ?string $error): string
{
if ($error) {
return '<div class="field-with-error">' . $html .
'<span class="error-message">' . htmlspecialchars($error) . '</span></div>';
}
return $html;
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Admin\Support\Forms;
use Admin\ViewModels\Forms\FormEditViewModel;
use Admin\ViewModels\Forms\FormFieldType;
use Admin\Validation\FormValidator;
/**
* Obsługa żądań formularza (POST, persist, walidacja)
*/
class FormRequestHandler
{
private FormValidator $validator;
public function __construct()
{
$this->validator = new FormValidator();
}
/**
* Przetwarza żądanie POST formularza
*
* @param FormEditViewModel $formViewModel
* @param array $postData Dane z $_POST
* @return array Wynik przetwarzania ['success' => bool, 'errors' => array, 'data' => array]
*/
public function handleSubmit(FormEditViewModel $formViewModel, array $postData): array
{
$result = [
'success' => false,
'errors' => [],
'data' => []
];
// Walidacja CSRF
$csrfToken = isset($postData['_csrf_token']) ? (string) $postData['_csrf_token'] : '';
if (!\Shared\Security\CsrfToken::validate($csrfToken)) {
$result['errors'] = ['csrf' => 'Nieprawidłowy token bezpieczeństwa. Odśwież stronę i spróbuj ponownie.'];
return $result;
}
// Walidacja
$errors = $this->validator->validate($postData, $formViewModel->fields, $formViewModel->languages);
if (!empty($errors)) {
$result['errors'] = $errors;
// Zapisz dane do persist przy błędzie walidacji
if ($formViewModel->persist) {
$formViewModel->saveToPersist($postData);
}
return $result;
}
// Przetwórz dane (np. konwersja typów)
$processedData = $this->processData($postData, $formViewModel->fields);
$result['success'] = true;
$result['data'] = $processedData;
// Wyczyść persist po sukcesie
if ($formViewModel->persist) {
$formViewModel->clearPersist();
}
return $result;
}
/**
* Przetwarza dane z formularza (konwersja typów)
*/
private function processData(array $postData, array $fields): array
{
$processed = [];
foreach ($fields as $field) {
$value = $postData[$field->name] ?? null;
// Konwersja typów
switch ($field->type) {
case FormFieldType::SWITCH:
$processed[$field->name] = $value ? 1 : 0;
break;
case FormFieldType::NUMBER:
$processed[$field->name] = $value !== null && $value !== '' ? (float)$value : null;
break;
case FormFieldType::LANG_SECTION:
if ($field->langFields !== null) {
$processed[$field->name] = $this->processLangSection($postData, $field);
}
break;
default:
$processed[$field->name] = $value;
}
}
return $processed;
}
/**
* Przetwarza sekcję językową
*/
private function processLangSection(array $postData, $section): array
{
$result = [];
if ($section->langFields === null) {
return $result;
}
foreach ($section->langFields as $field) {
$fieldName = $field->name;
$langData = $postData[$fieldName] ?? [];
foreach ($langData as $langId => $value) {
if (!isset($result[$langId])) {
$result[$langId] = [];
}
// Konwersja typów dla pól językowych
switch ($field->type) {
case FormFieldType::SWITCH:
$result[$langId][$fieldName] = $value ? 1 : 0;
break;
case FormFieldType::NUMBER:
$result[$langId][$fieldName] = $value !== null && $value !== '' ? (float)$value : null;
break;
default:
$result[$langId][$fieldName] = $value;
}
}
}
return $result;
}
/**
* Przywraca dane z persist do POST (przy błędzie walidacji)
*/
public function restoreFromPersist(FormEditViewModel $formViewModel): ?array
{
if (!$formViewModel->persist) {
return null;
}
return $_SESSION['form_persist'][$formViewModel->formId] ?? null;
}
/**
* Sprawdza czy żądanie jest submitowaniem formularza
*/
public function isFormSubmit(string $formId): bool
{
return $_SERVER['REQUEST_METHOD'] === 'POST' &&
(isset($_POST['_form_id']) && $_POST['_form_id'] === $formId);
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Admin\Support;
class TableListRequestFactory
{
public const DEFAULT_PER_PAGE_OPTIONS = [5, 10, 15, 25, 50, 100];
public const DEFAULT_PER_PAGE = 15;
/**
* Buduje kontekst listy (filtry, sortowanie, paginacja) z requestu.
*
* @return array{
* page:int,
* perPage:int,
* perPageOptions:array<int,int>,
* filters:array<string,string>,
* viewFilters:array<int,array<string,mixed>>,
* queryFilters:array<string,string>,
* sortColumn:string,
* sortDir:string
* }
*/
public static function fromRequest(
array $filterDefinitions,
array $sortableColumns,
string $defaultSortColumn = 'date_add',
?array $perPageOptions = null,
?int $defaultPerPage = null
): array {
if ($perPageOptions === null) {
$perPageOptions = self::DEFAULT_PER_PAGE_OPTIONS;
}
if ($defaultPerPage === null) {
$defaultPerPage = self::DEFAULT_PER_PAGE;
}
if (!in_array($defaultPerPage, $perPageOptions, true)) {
$defaultPerPage = (int)$perPageOptions[0];
}
$page = max(1, (int)\Shared\Helpers\Helpers::get('page'));
$perPage = (int)\Shared\Helpers\Helpers::get('per_page');
if (!in_array($perPage, $perPageOptions, true)) {
$perPage = $defaultPerPage;
}
$filters = [];
$viewFilters = [];
$queryFilters = [];
foreach ($filterDefinitions as $definition) {
$key = (string)($definition['key'] ?? '');
if ($key === '') {
continue;
}
$type = (string)($definition['type'] ?? 'text');
$value = (string)\Shared\Helpers\Helpers::get($key);
$filters[$key] = $value;
$queryFilters[$key] = $value;
$filterConfig = [
'key' => $key,
'label' => (string)($definition['label'] ?? $key),
'type' => $type,
'value' => $value,
];
if ($type === 'select' && isset($definition['options']) && is_array($definition['options'])) {
$filterConfig['options'] = $definition['options'];
}
$viewFilters[] = $filterConfig;
}
$sortColumn = trim((string)\Shared\Helpers\Helpers::get('sort'));
if (!in_array($sortColumn, $sortableColumns, true)) {
$sortColumn = $defaultSortColumn;
}
$sortDir = strtoupper(trim((string)\Shared\Helpers\Helpers::get('dir')));
if (!in_array($sortDir, ['ASC', 'DESC'], true)) {
$sortDir = 'DESC';
}
return [
'page' => $page,
'perPage' => $perPage,
'perPageOptions' => $perPageOptions,
'filters' => $filters,
'viewFilters' => $viewFilters,
'queryFilters' => $queryFilters,
'sortColumn' => $sortColumn,
'sortDir' => $sortDir,
];
}
}

View File

@@ -0,0 +1,196 @@
<?php
namespace Admin\Validation;
use Admin\ViewModels\Forms\FormField;
use Admin\ViewModels\Forms\FormFieldType;
/**
* Walidator formularzy
*/
class FormValidator
{
private array $errors = [];
/**
* Waliduje dane na podstawie definicji pól
*
* @param array $data Dane z POST
* @param array $fields Definicje pól (FormField[])
* @param array|null $languages Języki (dla walidacji pól językowych)
* @return array Tablica błędów (pusta jeśli OK)
*/
public function validate(array $data, array $fields, ?array $languages = null): array
{
$this->errors = [];
foreach ($fields as $field) {
if ($field->type === FormFieldType::LANG_SECTION) {
$this->validateLangSection($data, $field, $languages ?? []);
} else {
$this->validateField($data, $field);
}
}
return $this->errors;
}
/**
* Waliduje pojedyncze pole
*/
private function validateField(array $data, FormField $field): void
{
$value = $data[$field->name] ?? null;
// Walidacja wymagalności
if ($field->required && $this->isEmpty($value)) {
$this->errors[$field->name] = "Pole \"{$field->label}\" jest wymagane.";
return;
}
// Jeśli pole puste i nie jest wymagane - pomijamy dalszą walidację
if ($this->isEmpty($value)) {
return;
}
// Walidacja typu
switch ($field->type) {
case FormFieldType::EMAIL:
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawnym adresem e-mail.";
}
break;
case FormFieldType::NUMBER:
if (!is_numeric($value)) {
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być liczbą.";
}
break;
case FormFieldType::DATE:
if (!$this->isValidDate($value)) {
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą (YYYY-MM-DD).";
}
break;
case FormFieldType::DATETIME:
if (!$this->isValidDateTime($value)) {
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą i czasem.";
}
break;
}
// Walidacja customowa (callback)
if (isset($field->attributes['validate_callback']) && is_callable($field->attributes['validate_callback'])) {
$result = call_user_func($field->attributes['validate_callback'], $value, $data);
if ($result !== true) {
$this->errors[$field->name] = is_string($result) ? $result : "Pole \"{$field->label}\" zawiera nieprawidłową wartość.";
}
}
}
/**
* Waliduje sekcję językową
*/
private function validateLangSection(array $data, FormField $section, array $languages): void
{
if ($section->langFields === null) {
return;
}
foreach ($languages as $language) {
if (!($language['status'] ?? false)) {
continue;
}
$langId = $language['id'];
foreach ($section->langFields as $field) {
$fieldName = $field->name;
$value = $data[$fieldName][$langId] ?? null;
// Walidacja wymagalności
if ($field->required && $this->isEmpty($value)) {
$errorKey = "{$section->name}_{$fieldName}";
$this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) jest wymagane.";
continue;
}
// Walidacja typu dla pól językowych
if (!$this->isEmpty($value)) {
switch ($field->type) {
case FormFieldType::EMAIL:
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errorKey = "{$section->name}_{$fieldName}";
$this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) musi być poprawnym e-mailem.";
}
break;
}
}
}
}
}
/**
* Sprawdza czy wartość jest pusta
*/
private function isEmpty($value): bool
{
return $value === null || $value === '' || (is_array($value) && empty($value));
}
/**
* Sprawdza czy data jest poprawna (YYYY-MM-DD)
*/
private function isValidDate(string $date): bool
{
$d = \DateTime::createFromFormat('Y-m-d', $date);
return $d && $d->format('Y-m-d') === $date;
}
/**
* Sprawdza czy data i czas są poprawne
*/
private function isValidDateTime(string $datetime): bool
{
$d = \DateTime::createFromFormat('Y-m-d H:i:s', $datetime);
if ($d && $d->format('Y-m-d H:i:s') === $datetime) {
return true;
}
// Spróbuj bez sekund
$d = \DateTime::createFromFormat('Y-m-d H:i', $datetime);
return $d && $d->format('Y-m-d H:i') === $datetime;
}
/**
* Sprawdza czy walidacja zakończyła się sukcesem
*/
public function isValid(): bool
{
return empty($this->errors);
}
/**
* Zwraca wszystkie błędy
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* Zwraca pierwszy błąd
*/
public function getFirstError(): ?string
{
if (empty($this->errors)) {
return null;
}
$first = reset($this->errors);
if (is_array($first)) {
return reset($first);
}
return $first;
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Admin\ViewModels\Forms;
/**
* Definicja akcji formularza (przycisku)
*/
class FormAction
{
public string $name;
public string $label;
public string $type;
public string $url;
public ?string $backUrl;
public string $cssClass;
public array $attributes;
/**
* @param string $name Nazwa akcji (save, cancel, delete)
* @param string $label Etykieta przycisku
* @param string $url URL akcji (dla save)
* @param string|null $backUrl URL powrotu po zapisie
* @param string $cssClass Klasy CSS przycisku
* @param string $type Typ przycisku (submit, button, link)
* @param array $attributes Dodatkowe atrybuty HTML
*/
public function __construct(
string $name,
string $label,
string $url = '',
?string $backUrl = null,
string $cssClass = 'btn btn-primary',
string $type = 'submit',
array $attributes = []
) {
$this->name = $name;
$this->label = $label;
$this->url = $url;
$this->backUrl = $backUrl;
$this->cssClass = $cssClass;
$this->type = $type;
$this->attributes = $attributes;
}
/**
* Predefiniowana akcja Zapisz
*/
public static function save(string $url, string $backUrl = '', string $label = 'Zapisz'): self
{
return new self(
'save',
$label,
$url,
$backUrl,
'btn btn-primary',
'submit'
);
}
/**
* Predefiniowana akcja Podgląd (otwiera w nowej karcie)
*/
public static function preview(string $url, string $label = 'Podgląd'): self
{
return new self(
'preview',
$label,
$url,
null,
'btn btn-info',
'link',
['target' => '_blank']
);
}
/**
* Predefiniowana akcja Anuluj
*/
public static function cancel(string $backUrl, string $label = 'Anuluj'): self
{
return new self(
'cancel',
$label,
$backUrl,
null,
'btn btn-default',
'link'
);
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace Admin\ViewModels\Forms;
/**
* Główny model widoku formularza edycji
*/
class FormEditViewModel
{
public string $formId;
public string $title;
public string $method;
public string $action;
public ?string $backUrl;
public array $tabs;
public array $fields;
public array $hiddenFields;
public array $actions;
public bool $persist;
public array $data;
public ?array $validationErrors;
public ?array $languages;
/**
* @param string $formId Unikalny identyfikator formularza
* @param string $title Tytuł formularza
* @param array $data Dane obiektu (np. banner)
* @param array $fields Pola formularza
* @param array $tabs Zakładki formularza
* @param array $actions Akcje (przyciski)
* @param string $method Metoda HTTP (POST, GET)
* @param string $action URL akcji formularza
* @param string|null $backUrl URL powrotu
* @param bool $persist Czy zapamiętywać dane w sesji
* @param array $hiddenFields Dodatkowe ukryte pola
* @param array|null $languages Dostępne języki (dla sekcji językowych)
* @param array|null $validationErrors Błędy walidacji
*/
public function __construct(
string $formId,
string $title,
array $data = [],
array $fields = [],
array $tabs = [],
array $actions = [],
string $method = 'POST',
string $action = '',
?string $backUrl = null,
bool $persist = true,
array $hiddenFields = [],
?array $languages = null,
?array $validationErrors = null
) {
$this->formId = $formId;
$this->title = $title;
$this->data = $data;
$this->fields = $fields;
$this->tabs = $tabs;
$this->actions = $actions;
$this->method = $method;
$this->action = $action;
$this->backUrl = $backUrl;
$this->persist = $persist;
$this->hiddenFields = $hiddenFields;
$this->languages = $languages;
$this->validationErrors = $validationErrors;
}
/**
* Sprawdza czy formularz ma zakładki
*/
public function hasTabs(): bool
{
return count($this->tabs) > 0;
}
/**
* Sprawdza czy formularz ma sekcje językowe
*/
public function hasLangSections(): bool
{
foreach ($this->fields as $field) {
if ($field->type === FormFieldType::LANG_SECTION) {
return true;
}
}
return false;
}
/**
* Zwraca pola dla konkretnej zakładki
*/
public function getFieldsForTab(string $tabId): array
{
return array_filter($this->fields, function (FormField $field) use ($tabId) {
return $field->tabId === $tabId && $field->type !== FormFieldType::LANG_SECTION;
});
}
/**
* Zwraca sekcje językowe dla konkretnej zakładki
*/
public function getLangSectionsForTab(string $tabId): array
{
return array_filter($this->fields, function (FormField $field) use ($tabId) {
return $field->type === FormFieldType::LANG_SECTION &&
$field->langSectionParentTab === $tabId;
});
}
/**
* Pobiera wartość pola z danych lub sesji (persist)
*/
public function getFieldValue(FormField $field, $languageId = null, ?string $langFieldName = null)
{
$fieldName = $field->name;
// Dla sekcji językowych - pobierz wartość z data[lang_id][field_name]
if ($languageId !== null && $langFieldName !== null) {
$fieldName = $langFieldName;
return $this->data['languages'][$languageId][$fieldName] ?? null;
}
// Zwykłe pole - najpierw sprawdź sesję (persist), potem dane
if ($this->persist && isset($_SESSION['form_persist'][$this->formId][$fieldName])) {
return $_SESSION['form_persist'][$this->formId][$fieldName];
}
return $this->data[$fieldName] ?? $field->value;
}
/**
* Sprawdza czy pole ma błąd walidacji
*/
public function hasError(string $fieldName, $languageId = null): bool
{
if ($this->validationErrors === null) {
return false;
}
if ($languageId !== null) {
return isset($this->validationErrors[$fieldName][$languageId]);
}
return isset($this->validationErrors[$fieldName]);
}
/**
* Pobiera komunikat błędu dla pola
*/
public function getError(string $fieldName, $languageId = null): ?string
{
if ($languageId !== null) {
return $this->validationErrors[$fieldName][$languageId] ?? null;
}
return $this->validationErrors[$fieldName] ?? null;
}
/**
* Czyści dane persist z sesji
*/
public function clearPersist(): void
{
if (isset($_SESSION['form_persist'][$this->formId])) {
unset($_SESSION['form_persist'][$this->formId]);
}
}
/**
* Zapisuje dane do sesji (persist)
*/
public function saveToPersist(array $data): void
{
if (!isset($_SESSION['form_persist'])) {
$_SESSION['form_persist'] = [];
}
$_SESSION['form_persist'][$this->formId] = $data;
}
}

View File

@@ -0,0 +1,364 @@
<?php
namespace Admin\ViewModels\Forms;
/**
* Definicja pojedynczego pola formularza
*/
class FormField
{
public string $name;
public string $type;
public string $label;
public $value;
public string $tabId;
public bool $required;
public array $attributes;
public array $options;
public ?string $helpText;
public ?string $placeholder;
public ?string $id;
// Specyficzne dla obrazów/plików
public bool $useFilemanager;
public ?string $filemanagerUrl;
// Specyficzne dla edytora
public string $editorToolbar;
public int $editorHeight;
// Specyficzne dla lang_section
public ?array $langFields;
public ?string $langSectionParentTab;
public ?string $customHtml;
/**
* @param string $name Nazwa pola (name)
* @param string $type Typ pola (z FormFieldType)
* @param string $label Etykieta pola
* @param mixed $value Wartość domyślna
* @param string $tabId Identyfikator zakładki
* @param bool $required Czy pole wymagane
* @param array $attributes Atrybuty HTML
* @param array $options Opcje dla select
* @param string|null $helpText Tekst pomocniczy
* @param string|null $placeholder Placeholder
* @param bool $useFilemanager Czy używać filemanagera
* @param string|null $filemanagerUrl URL filemanagera
* @param string $editorToolbar Konfiguracja toolbar CKEditor
* @param int $editorHeight Wysokość edytora
* @param array|null $langFields Pola w sekcji językowej
* @param string|null $langSectionParentTab Zakładka nadrzędna dla sekcji językowej
*/
public function __construct(
string $name,
string $type = FormFieldType::TEXT,
string $label = '',
$value = null,
string $tabId = 'default',
bool $required = false,
array $attributes = [],
array $options = [],
?string $helpText = null,
?string $placeholder = null,
bool $useFilemanager = false,
?string $filemanagerUrl = null,
string $editorToolbar = 'MyTool',
int $editorHeight = 300,
?array $langFields = null,
?string $langSectionParentTab = null,
?string $customHtml = null
) {
$this->name = $name;
$this->type = $type;
$this->label = $label;
$this->value = $value;
$this->tabId = $tabId;
$this->required = $required;
$this->attributes = $attributes;
$this->options = $options;
$this->helpText = $helpText;
$this->placeholder = $placeholder;
$this->useFilemanager = $useFilemanager;
$this->filemanagerUrl = $filemanagerUrl;
$this->editorToolbar = $editorToolbar;
$this->editorHeight = $editorHeight;
$this->langFields = $langFields;
$this->langSectionParentTab = $langSectionParentTab;
$this->customHtml = $customHtml;
$this->id = $attributes['id'] ?? $name;
}
// Factory methods dla różnych typów pól
public static function text(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::TEXT,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
$config['attributes'] ?? [],
[],
$config['help'] ?? null,
$config['placeholder'] ?? null
);
}
public static function number(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::NUMBER,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
$config['attributes'] ?? [],
[],
$config['help'] ?? null
);
}
public static function email(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::EMAIL,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
$config['attributes'] ?? []
);
}
public static function password(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::PASSWORD,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
$config['attributes'] ?? []
);
}
public static function date(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::DATE,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
array_merge(['class' => 'date'], $config['attributes'] ?? [])
);
}
public static function datetime(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::DATETIME,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
array_merge(['class' => 'datetime'], $config['attributes'] ?? [])
);
}
public static function switch(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::SWITCH,
$config['label'] ?? '',
$config['value'] ?? false,
$config['tab'] ?? 'default',
false,
$config['attributes'] ?? []
);
}
public static function select(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::SELECT,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
$config['attributes'] ?? [],
$config['options'] ?? []
);
}
public static function textarea(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::TEXTAREA,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
array_merge(['rows' => $config['rows'] ?? 4], $config['attributes'] ?? [])
);
}
public static function editor(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::EDITOR,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
$config['attributes'] ?? [],
[],
null,
null,
false,
null,
$config['toolbar'] ?? 'MyTool',
$config['height'] ?? 300
);
}
public static function image(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::IMAGE,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
$config['attributes'] ?? [],
[],
null,
null,
$config['filemanager'] ?? true,
$config['filemanager_url'] ?? null
);
}
public static function file(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::FILE,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
$config['attributes'] ?? [],
[],
null,
null,
$config['filemanager'] ?? true
);
}
public static function color(string $name, array $config = []): self
{
return new self(
$name,
FormFieldType::COLOR,
$config['label'] ?? '',
$config['value'] ?? null,
$config['tab'] ?? 'default',
$config['required'] ?? false,
$config['attributes'] ?? [],
[],
$config['help'] ?? null
);
}
public static function hidden(string $name, $value = null): self
{
return new self(
$name,
FormFieldType::HIDDEN,
'',
$value,
'default'
);
}
public static function custom(string $name, string $html, array $config = []): self
{
return new self(
$name,
FormFieldType::CUSTOM,
$config['label'] ?? '',
null,
$config['tab'] ?? 'default',
false,
$config['attributes'] ?? [],
[],
null,
null,
false,
null,
'MyTool',
300,
null,
null,
$html
);
}
/**
* Sekcja językowa - grupa pól powtarzana dla każdego języka
*
* @param string $name Nazwa sekcji (prefiks dla pól)
* @param string $parentTab Identyfikator zakładki nadrzędnej
* @param array $fields Pola w sekcji językowej (tablica FormField)
*/
public static function langSection(string $name, string $parentTab, array $fields): self
{
return new self(
$name,
FormFieldType::LANG_SECTION,
'',
null,
$parentTab,
false,
[],
[],
null,
null,
false,
null,
'MyTool',
300,
$fields,
$parentTab
);
}
/**
* Zwraca nazwę pola z sufiksem dla konkretnego języka
*/
public function getLocalizedName($languageId): string
{
return "{$this->name}[{$languageId}]";
}
/**
* Zwraca ID pola z sufiksem dla konkretnego języka
*/
public function getLocalizedId($languageId): string
{
return "{$this->id}_{$languageId}";
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Admin\ViewModels\Forms;
/**
* Dostępne typy pól formularza
*/
class FormFieldType
{
public const TEXT = 'text';
public const NUMBER = 'number';
public const EMAIL = 'email';
public const PASSWORD = 'password';
public const DATE = 'date';
public const DATETIME = 'datetime';
public const SWITCH = 'switch';
public const SELECT = 'select';
public const TEXTAREA = 'textarea';
public const EDITOR = 'editor';
public const IMAGE = 'image';
public const FILE = 'file';
public const HIDDEN = 'hidden';
public const LANG_SECTION = 'lang_section';
public const CUSTOM = 'custom';
public const COLOR = 'color';
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Admin\ViewModels\Forms;
/**
* Definicja zakładki formularza
*/
class FormTab
{
public string $id;
public string $label;
public string $icon;
public ?string $parentTabId;
/**
* @param string $id Unikalny identyfikator zakładki
* @param string $label Etykieta wyświetlana
* @param string $icon Klasa FontAwesome (np. 'fa-wrench')
* @param string|null $parentTabId Identyfikator zakładki nadrzędnej (dla zagnieżdżenia)
*/
public function __construct(
string $id,
string $label,
string $icon = '',
?string $parentTabId = null
) {
$this->id = $id;
$this->label = $label;
$this->icon = $icon;
$this->parentTabId = $parentTabId;
}
}

View File

@@ -1,4 +1,4 @@
<?
<?php
namespace admin\factory;
class Authors
{
@@ -6,112 +6,31 @@ class Authors
static public function get_simple_list()
{
global $mdb;
return $mdb -> select( 'pp_authors', '*', [ 'ORDER' => [ 'author' => 'ASC' ] ] );
$repo = new \Domain\Authors\AuthorsRepository($mdb);
return $repo->simpleList();
}
// usunięcie autora
static public function delete_author( $id_author )
{
global $mdb;
$result = $mdb -> delete( 'pp_authors', [ 'id' => (int)$id_author ] );
\S::delete_cache();
return $result;
$repo = new \Domain\Authors\AuthorsRepository($mdb);
return $repo->authorDelete($id_author);
}
// zapis autora
static public function save_author( $id_author, $author, $image, $description )
{
global $mdb;
if ( !$id_author )
{
$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;
$repo = new \Domain\Authors\AuthorsRepository($mdb);
return $repo->authorSave($id_author, $author, $image, $description);
}
// szczególy autora
static public function get_single_author( $id_author )
{
global $mdb;
$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;
return $author;
$repo = new \Domain\Authors\AuthorsRepository($mdb);
return $repo->authorDetails($id_author);
}
}

View File

@@ -7,123 +7,21 @@ class Banners
public static function banner_delete( $banner_id )
{
global $mdb;
$result = $mdb -> delete( 'pp_banners', [ 'id' => (int) $banner_id ] );
\S::delete_cache();
return $result;
$repo = new \Domain\Banners\BannersRepository($mdb);
return $repo->bannerDelete($banner_id);
}
public static function banner_save( $banner_id, $name, $status, $date_start, $date_end, $home_page, $src, $url, $html, $text )
{
global $mdb;
if ( !$banner_id )
{
$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;
$repo = new \Domain\Banners\BannersRepository($mdb);
return $repo->bannerSave($banner_id, $name, $status, $date_start, $date_end, $home_page, $src, $url, $html, $text);
}
public static function banner_details( $id_banner )
{
global $mdb;
$banner = $mdb -> get( 'pp_banners', '*', [ 'id' => (int)$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;
$repo = new \Domain\Banners\BannersRepository($mdb);
return $repo->bannerDetails($id_banner);
}
}
?>

View File

@@ -6,100 +6,49 @@ class Newsletter
public static function emails_import( $emails )
{
global $mdb;
$emails = explode( PHP_EOL, $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;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->emailsImport($emails);
}
public static function is_admin_template( $template_id )
{
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 )
{
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 )
{
global $mdb;
$results = $mdb -> select( 'pp_newsletter', 'email', [ 'status' => 1 ] );
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;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->send($dates, $template, $only_once);
}
public static function email_template_detalis ($id_template)
{
global $mdb;
$result = $mdb -> get ('pp_newsletter_templates', '*', [ 'id' => (int)$id_template ] );
return $result;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->templateDetails($id_template);
}
public static function template_save($id, $name, $text)
{
global $mdb;
if ( !$id )
{
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;
}
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->templateSave($id, $name, $text);
}
public static function templates_list()
{
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

@@ -6,98 +6,63 @@ class Releases
public static function get_versions(): array
{
global $mdb;
$rows = $mdb->select('pp_update_versions', '*', ['ORDER' => ['version' => 'DESC']]);
if (!$rows) return [];
foreach ($rows as &$row)
$row['zip_exists'] = file_exists('../updates/' . self::zip_dir($row['version']) . '/ver_' . $row['version'] . '.zip');
return $rows;
$repo = new \Domain\Releases\ReleasesRepository($mdb);
return $repo->getVersions();
}
public static function promote(string $version): void
{
global $mdb;
$mdb->update('pp_update_versions',
['channel' => 'stable', 'promoted_at' => date('Y-m-d H:i:s')],
['version' => $version]
);
$repo = new \Domain\Releases\ReleasesRepository($mdb);
$repo->promote($version);
}
public static function demote(string $version): void
{
global $mdb;
$mdb->update('pp_update_versions',
['channel' => 'beta', 'promoted_at' => null],
['version' => $version]
);
$repo = new \Domain\Releases\ReleasesRepository($mdb);
$repo->demote($version);
}
public static function discover_versions(): int
{
global $mdb;
$known = array_flip($mdb->select('pp_update_versions', 'version', []) ?: []);
$zips = glob('../updates/*/ver_*.zip') ?: [];
$added = 0;
foreach ($zips as $path) {
preg_match('/ver_([0-9.]+)\.zip$/', $path, $m);
if (!$m) continue;
$ver = $m[1];
if (isset($known[$ver])) continue;
$mdb->insert('pp_update_versions', [
'version' => $ver,
'channel' => 'beta',
'created_at' => date('Y-m-d H:i:s'),
]);
$known[$ver] = true;
$added++;
}
return $added;
$repo = new \Domain\Releases\ReleasesRepository($mdb);
return $repo->discoverVersions();
}
public static function get_licenses(): array
{
global $mdb;
return $mdb->select('pp_update_licenses', '*', ['ORDER' => ['domain' => 'ASC']]) ?: [];
$repo = new \Domain\Releases\ReleasesRepository($mdb);
return $repo->getLicenses();
}
public static function get_license(int $id): array
{
global $mdb;
return $mdb->get('pp_update_licenses', '*', ['id' => $id]) ?: [];
$repo = new \Domain\Releases\ReleasesRepository($mdb);
return $repo->getLicense($id);
}
public static function save_license(array $data): void
{
global $mdb;
$row = [
'key' => trim($data['key'] ?? ''),
'domain' => trim($data['domain'] ?? ''),
'valid_to_date' => $data['valid_to_date'] ?: null,
'valid_to_version' => $data['valid_to_version'] ?: null,
'beta' => (int)(bool)($data['beta'] ?? 0),
'note' => trim($data['note'] ?? ''),
];
if (!empty($data['id']))
$mdb->update('pp_update_licenses', $row, ['id' => (int)$data['id']]);
else
$mdb->insert('pp_update_licenses', $row);
$repo = new \Domain\Releases\ReleasesRepository($mdb);
$repo->saveLicense($data);
}
public static function delete_license(int $id): void
{
global $mdb;
$mdb->delete('pp_update_licenses', ['id' => $id]);
$repo = new \Domain\Releases\ReleasesRepository($mdb);
$repo->deleteLicense($id);
}
public static function toggle_beta(int $id): void
{
global $mdb;
$license = $mdb->get('pp_update_licenses', ['id', 'beta'], ['id' => $id]);
if ($license)
$mdb->update('pp_update_licenses', ['beta' => $license['beta'] ? 0 : 1], ['id' => $id]);
}
private static function zip_dir(string $version): string
{
return substr($version, 0, strlen($version) - (strlen($version) == 5 ? 2 : 1)) . '0';
$repo = new \Domain\Releases\ReleasesRepository($mdb);
$repo->toggleBeta($id);
}
}

View File

@@ -7,115 +7,21 @@ class Scontainers
public static function container_delete( $container_id )
{
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 )
{
global $mdb;
if ( !$container_id )
{
$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;
}
$repo = new \Domain\Scontainers\ScontainersRepository($mdb);
return $repo->containerSave($container_id, $title, $text, $status, $show_title, $src, $html);
}
public static function container_details( $container_id )
{
global $mdb;
$container = $mdb -> get( 'pp_scontainers', '*', [ 'id' => (int) $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;
$repo = new \Domain\Scontainers\ScontainersRepository($mdb);
return $repo->containerDetails($container_id);
}
}

View File

@@ -5,51 +5,21 @@ class SeoAdditional
public static function element_delete( $element_id )
{
global $mdb;
return $mdb -> delete( 'pp_seo_additional', [ 'id' => (int)$element_id ] );
$repo = new \Domain\SeoAdditional\SeoAdditionalRepository($mdb);
return $repo->elementDelete($element_id);
}
public static function element_save( $id, $url, $status, $title, $keywords, $description, $text )
{
global $mdb;
if ( !$id )
{
if ( $mdb -> insert( 'pp_seo_additional', [
'url' => $url,
'status' => $status == 'on' ? 1 : 0,
'title' => $title,
'keywords' => $keywords,
'description' => $description,
'text' => $text
] ) )
{
\S::delete_cache();
return $mdb -> id();
}
}
else
{
$mdb -> update( 'pp_seo_additional', [
'url' => $url,
'status' => $status == 'on' ? 1 : 0,
'title' => $title,
'keywords' => $keywords,
'description' => $description,
'text' => $text
], [
'id' => (int)$id
] );
\S::delete_cache();
return $id;
}
$repo = new \Domain\SeoAdditional\SeoAdditionalRepository($mdb);
return $repo->elementSave($id, $url, $status, $title, $keywords, $description, $text);
}
public static function element_details( $element_id )
{
global $mdb;
$result = $mdb -> get ( 'pp_seo_additional', '*', [ 'id' => (int)$element_id ] );
return $result;
$repo = new \Domain\SeoAdditional\SeoAdditionalRepository($mdb);
return $repo->elementDetails($element_id);
}
}

View File

@@ -6,151 +6,7 @@ class Update
public static function update()
{
global $mdb, $settings;
\S::delete_session( 'new-version' );
$versions = file_get_contents( 'http://www.cmspro.project-dc.pl/updates/versions.php?key=' . $settings['update_key'] );
$versions = explode( PHP_EOL, $versions );
foreach ( $versions as $ver )
{
$ver = trim( $ver );
if ( (float)$ver > (float)\S::get_version() )
{
if ( strlen( $ver ) == 5 )
$dir = substr( $ver, 0, strlen( $ver ) - 2 ) . 0;
else
$dir = substr( $ver, 0, strlen( $ver ) - 1 ) . 0;
$baseUrl = 'http://www.cmspro.project-dc.pl/updates/' . $dir;
/* pobranie paczki ZIP */
$file = file_get_contents( $baseUrl . '/ver_' . $ver . '.zip' );
$dlHandler = fopen( 'update.zip' , 'w' );
if ( !fwrite( $dlHandler, $file ) )
return false;
fclose( $dlHandler );
if ( !file_exists( 'update.zip' ) )
return false;
/* pobranie manifestu JSON (nowy system) lub fallback na legacy _sql.txt / _files.txt */
$manifest = null;
$manifestJson = @file_get_contents( $baseUrl . '/ver_' . $ver . '_manifest.json' );
if ( $manifestJson )
{
if ( substr( $manifestJson, 0, 3 ) === "\xEF\xBB\xBF" )
$manifestJson = substr( $manifestJson, 3 );
$manifest = @json_decode( $manifestJson, true );
}
if ( is_array( $manifest ) )
{
/* weryfikacja checksum SHA256 */
if ( !empty( $manifest['checksum_zip'] ) )
{
$expectedHash = str_replace( 'sha256:', '', $manifest['checksum_zip'] );
$actualHash = hash_file( 'sha256', 'update.zip' );
if ( $expectedHash !== $actualHash )
{
unlink( 'update.zip' );
return false;
}
}
/* aktualizacja bazy danych z manifestu */
if ( !empty( $manifest['sql'] ) && is_array( $manifest['sql'] ) )
{
foreach ( $manifest['sql'] as $query )
{
$query = trim( $query );
if ( $query )
$mdb -> query( $query );
}
}
/* usuwanie plikow z manifestu */
if ( !empty( $manifest['files']['deleted'] ) && is_array( $manifest['files']['deleted'] ) )
{
foreach ( $manifest['files']['deleted'] as $filePath )
{
$fullPath = '../' . $filePath;
if ( file_exists( $fullPath ) )
unlink( $fullPath );
}
}
/* usuwanie katalogow z manifestu */
if ( !empty( $manifest['directories_deleted'] ) && is_array( $manifest['directories_deleted'] ) )
{
foreach ( $manifest['directories_deleted'] as $dirPath )
{
$fullPath = '../' . $dirPath;
if ( is_dir( $fullPath ) )
\S::delete_dir( $fullPath );
}
}
}
else
{
/* legacy: aktualizacja bazy danych z _sql.txt */
$sql = @file_get_contents( $baseUrl . '/ver_' . $ver . '_sql.txt' );
if ( $sql )
{
$sql = explode( PHP_EOL, $sql );
if ( is_array( $sql ) ) foreach ( $sql as $query )
{
$query = trim( $query );
if ( $query )
$mdb -> query( $query );
}
}
/* legacy: usuwanie zbednych plikow z _files.txt */
$lines = @file_get_contents( $baseUrl . '/ver_' . $ver . '_files.txt' );
if ( $lines )
{
$lines = explode( PHP_EOL, $lines );
if ( is_array( $lines ) ) foreach ( $lines as $line )
{
if ( strpos( $line, 'F: ' ) !== false )
{
$delFile = substr( $line, 3, strlen( $line ) );
if ( file_exists( $delFile ) )
unlink( $delFile );
}
if ( strpos( $line, 'D: ' ) !== false )
{
$delDir = substr( $line, 3, strlen( $line ) );
if ( is_dir( $delDir ) )
\S::delete_dir( $delDir );
}
}
}
}
/* wgrywanie nowych plikow */
$file_name = 'update.zip';
$path = pathinfo( realpath( $file_name ), PATHINFO_DIRNAME );
$path = substr( $path, 0, strlen( $path ) - 5 );
$zip = new \ZipArchive;
$res = $zip -> open( $file_name );
if ( $res === TRUE )
{
$zip -> extractTo( $path );
$zip -> close();
unlink( $file_name );
}
$updateThis = fopen( '../libraries/version.ini', 'w' );
fwrite( $updateThis, $ver );
fclose( $updateThis );
return true;
}
}
$repo = new \Domain\Releases\UpdateRepository($mdb, $settings);
return $repo->update();
}
}

View File

@@ -4,511 +4,21 @@ class Cron
public static function automatic_update_sites()
{
global $mdb;
$results = $mdb -> query( "SELECT id, url FROM projects WHERE automatic_update = 1 AND DATE_ADD( last_update, INTERVAL 1 WEEK ) <= '" . date( 'Y-m-d H:i:s' ) . "'" ) -> fetchAll();
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
{
$mdb -> delete( 'project_links_internal', [ 'AND' => [ 'project_id' => $row['id'], 'parent_id[!]' => null ] ] );
$mdb -> delete( 'project_links_external', [ 'project_id' => $row['id'] ] );
$mdb -> update( 'project_links_internal', [ 'visited' => 0 ], [ 'project_id' => $row['id'] ] );
$mdb -> update( 'projects', [ 'last_update' => date( 'Y-m-d H:i:s' ) ], [ 'id' => $row['id'] ] );
return [ 'status' => 'ok', 'msg' => 'Ponawiam sprawdzanie strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
return [ 'status' => 'empty' ];
$repo = new \Domain\Cron\CronRepository($mdb);
return $repo->automaticUpdateSites();
}
public static function get_site_main_links()
{
global $mdb;
$results = $mdb -> query( 'SELECT id, url FROM projects WHERE id NOT IN ( SELECT project_id FROM project_links_internal GROUP BY project_id ) AND enabled = 1 LIMIT 1' ) -> fetchAll();
if ( is_array( $results ) and !empty ( $results ) ) foreach ( $results as $row )
{
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $row['url'] );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $ch, CURLOPT_VERBOSE, 1 );
curl_setopt( $ch, CURLOPT_TIMEOUT, 60 );
curl_setopt( $ch, CURLOPT_HEADER, true );
curl_setopt( $ch, CURLOPT_CAINFO, 'cacert.pem' );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.103 Safari/537.36' );
$response = curl_exec( $ch );
curl_close ( $ch );
if ( !curl_errno( $ch ) )
{
$mdb -> insert( 'project_links_internal', [
'project_id' => $row['id'],
'url' => $row['url'],
'parent_id' => null
] );
$doc = new DOMDocument;
$doc -> loadHTML( $response );
foreach ( $doc -> getElementsByTagName( 'a' ) as $link )
{
$url = $link -> getAttribute( 'href' );
if ( \S::is_url_internal( $row['url'], $url ) )
{
if ( strpos( $url, '#' ) !== false )
$url = rtrim( substr( $url, 0, strpos( $url, '#' ) ), '?,#' );
$url = \S::modify_internal_link( $row['url'], $url );
if ( !filter_var( $url, FILTER_VALIDATE_URL ) === false and !$mdb -> count( 'project_links_internal', [ 'AND' => [ 'project_id' => $row['id'], 'url' => $url ] ] ) )
{
$mdb -> insert( 'project_links_internal', [
'project_id' => $row['id'],
'url' => $url
] );
}
}
}
return [ 'status' => 'ok', 'msg' => 'Pobieram linki dla strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
else
return [ 'status' => 'ok', 'msg' => 'Błąd podczas pobierania strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
return [ 'status' => 'empty' ];
$repo = new \Domain\Cron\CronRepository($mdb);
return $repo->getSiteMainLinks();
}
public static function get_site_other_links()
{
global $mdb;
$results = $mdb -> query( 'SELECT '
. 'pli.id, project_id, pli.url, p.url AS project_url '
. 'FROM '
. 'project_links_internal AS pli '
. 'INNER JOIN projects AS p ON p.id = pli.project_id '
. 'WHERE '
. 'visited = 0 AND enabled = 1 '
. 'LIMIT 1' ) -> fetchAll();
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
{
$url = parse_url( $row['url'] );
$ch = curl_init();
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $ch, CURLOPT_VERBOSE, 1 );
curl_setopt( $ch, CURLOPT_TIMEOUT, 60 );
curl_setopt( $ch, CURLOPT_COOKIEFILE, 'temp/cookie.txt' );
curl_setopt( $ch, CURLOPT_COOKIEJAR, 'temp/cookie.txt' );
curl_setopt( $ch, CURLOPT_CAINFO, 'cacert.pem' );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.103 Safari/537.36' );
curl_setopt( $ch, CURLOPT_URL, 'http://' . $url['host'] );
$response = curl_exec( $ch );
curl_setopt( $ch, CURLOPT_URL, $row['url'] );
$response = curl_exec( $ch );
$content_type = curl_getinfo( $ch, CURLINFO_CONTENT_TYPE );
$code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close ( $ch );
if ( !curl_errno( $ch ) and ( $code == 200 or $code == 301 ) and strpos( $content_type, 'text/html' ) !== false )
{
self::get_site_meta_title( $row['id'], $response );
self::get_site_meta_keywords( $row['id'], $response );
self::get_site_meta_description( $row['id'], $response );
self::get_site_meta_robots( $row['id'], $response );
self::get_site_meta_googlebot( $row['id'], $response );
self::get_site_code_lenght( $row['id'], $response );
self::get_site_text_lenght( $row['id'], $response );
self::get_site_canonical( $row['id'], $response );
self::get_table_exists( $row['id'], $response );
self::get_iframe_exists( $row['id'], $response );
self::get_h1_exists( $row['id'], $response );
self::get_images_without_alt( $row['id'], $response );
/* pobranie linków ze strony */
$doc = new DOMDocument;
$doc -> loadHTML( $response );
foreach ( $doc -> getElementsByTagName( 'a' ) as $link )
{
$url = $link -> getAttribute( 'href' );
/* linki wewnętrzne na danej postronie */
if ( \S::is_url_internal( $row['project_url'], $url ) )
{
if ( strpos( $url, '#' ) !== false )
$url = rtrim( substr( $url, 0, strpos( $url, '#' ) ), '?,#' );
$url = \S::modify_internal_link( $row['project_url'], $url, $row['url'] );
$info = pathinfo( $url );
if ( !filter_var( $url, FILTER_VALIDATE_URL ) === false and !in_array( strtolower( $info['extension'] ), \S::not_html_format() ) and !$mdb -> count( 'project_links_internal', [
'AND' => [
'project_id' => $row['project_id'],
'url' => $url
]
] ) )
{
$mdb -> insert( 'project_links_internal', [
'project_id' => $row['project_id'],
'url' => $url,
'visited' => 0,
'parent_id' => $row['id'],
'response' => $response
] );
}
}
/* linki zewnętrzne na danej podstronie */
else
{
$link -> getAttribute( 'rel' ) == 'nofollow' ? $nofollow = 1 : $nofollow = 0;
$mdb -> insert( 'project_links_external', [
'project_id' => $row['project_id'],
'link_id' => $row['id'],
'url' => $link -> getAttribute( 'href' ),
'nofollow' => $nofollow,
'title' => $link -> getAttribute( 'title' )
] );
}
}
$mdb -> update( 'project_links_internal', [
'visited' => 1,
'content_type' => $content_type,
'response_code' => $code,
'response' => $response
], [
'id' => $row['id']
] );
return [ 'status' => 'ok', 'msg' => 'Pobieram informacje dla strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
else if ( $code == 404 or strpos( $content_type, 'text/html' ) === false )
{
$mdb -> update( 'project_links_internal', [
'visited' => 1,
'deleted' => 1,
'content_type' => $content_type,
'response_code' => $code
], [
'id' => $row['id']
] );
return [ 'status' => 'ok', 'msg' => 'Pobieram informacje dla strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
else if ( $code !== 200 and strpos( $content_type, 'text/html' ) !== false )
{
$mdb -> update( 'project_links_internal', [
'visited' => 1,
'content_type' => $content_type,
'response_code' => $code,
'response' => $response
], [
'id' => $row['id']
] );
return [ 'status' => 'ok', 'msg' => 'Pobieram informacje dla strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
else
return [ 'status' => 'ok', 'msg' => 'Błąd podczas pobierania strony <a href="' . $row['url'] . '" target="_blank">' . $row['url'] . '</a>' ];
}
return [ 'status' => 'empty' ];
}
static public function get_images_without_alt( $url_id, $response )
{
global $mdb;
$doc = new DOMDocument;
$doc -> loadHTML( $response );
$images = $doc -> getElementsByTagName("img");
$have_images_without_alt = 0;
foreach ( $images as $img )
{
if ( !$img -> getAttribute( 'alt' ) )
$have_images_without_alt = 1;
}
$mdb -> update( 'project_links_internal', [ 'have_images_without_alt' => $have_images_without_alt ], [ 'id' => $url_id ] );
}
static public function get_table_exists( $url_id, $response )
{
global $mdb;
$doc = new DOMDocument;
$doc -> loadHTML( $response );
$count = $doc -> getElementsByTagName("table");
$mdb -> update( 'project_links_internal', [ 'have_table' => $count -> length ? 1 : 0 ], [ 'id' => $url_id ] );
}
static public function get_iframe_exists( $url_id, $response )
{
global $mdb;
$doc = new DOMDocument;
$doc -> loadHTML( $response );
$count = $doc -> getElementsByTagName("iframe");
$mdb -> update( 'project_links_internal', [ 'have_iframe' => $count -> length ? 1 : 0 ], [ 'id' => $url_id ] );
}
static public function get_h1_exists( $url_id, $response )
{
global $mdb;
$doc = new DOMDocument;
$doc -> loadHTML( $response );
$count = $doc -> getElementsByTagName("h1");
$mdb -> update( 'project_links_internal', [ 'have_h1' => $count -> length ? 1 : 0 ], [ 'id' => $url_id ] );
}
public static function get_site_meta_title( $url_id, $response )
{
global $mdb;
$title = '';
preg_match('/<title>([^>]*)<\/title>/si', $response, $match );
if ( isset( $match ) && is_array( $match ) && count( $match ) > 0 )
$title = (string)strip_tags( $match[1] );
if ( !$title )
{
preg_match_all('/<[\s]*meta[\s]*name="og:?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match);
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$title = (string)$metaTags['title']['value'];
}
}
$mdb -> update( 'project_links_internal', [ 'title' => $title ], [ 'id' => $url_id ] );
}
public static function get_site_canonical( $url_id, $response )
{
global $mdb;
$doc = new DOMDocument;
$doc -> loadHTML( $response );
foreach ( $doc -> getElementsByTagName( 'link' ) as $link )
{
$rel = $link -> getAttribute( 'rel' );
if ( $rel == 'canonical' )
{
$canonical = $link -> getAttribute( 'href' );
}
}
$mdb -> update( 'project_links_internal', [ 'canonical' => $canonical ], [ 'id' => $url_id ] );
}
public static function get_site_meta_keywords( $url_id, $response )
{
global $mdb;
$meta_keywords = '';
preg_match_all( '/<[\s]*meta[\s]*name="?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match );
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_keywords = (string)$metaTags['keywords']['value'];
}
if ( !$meta_keywords )
{
preg_match_all( '/<[\s]*meta[\s]*property="og:?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match );
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_keywords = (string)$metaTags['keywords']['value'];
}
}
$mdb -> update( 'project_links_internal', [ 'meta_keywords' => $meta_keywords ], [ 'id' => $url_id ] );
}
public static function get_site_meta_description( $url_id, $response )
{
global $mdb;
$meta_description = '';
preg_match_all('/<[\s]*meta[\s]*name="?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match);
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_description = (string)$metaTags['description']['value'];
}
if ( !$meta_description )
{
preg_match_all( '/<[\s]*meta[\s]*property="og:?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match );
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_description = (string)$metaTags['description']['value'];
}
}
$mdb -> update( 'project_links_internal', [ 'meta_description' => $meta_description ], [ 'id' => $url_id ] );
}
public static function get_site_meta_robots( $url_id, $response )
{
global $mdb;
$meta_robots = '';
preg_match_all('/<[\s]*meta[\s]*name="?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match);
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_robots = (string)$metaTags['robots']['value'];
}
$mdb -> update( 'project_links_internal', [ 'meta_robots' => $meta_robots ], [ 'id' => $url_id ] );
}
public static function get_site_meta_googlebot( $url_id, $response )
{
global $mdb;
$meta_googlebot = '';
preg_match_all('/<[\s]*meta[\s]*name="?' . '([^>"]*)"?[\s]*' . 'content="?([^>"]*)"?[\s]*[\/]?[\s]*>/si', $response, $match);
if ( isset ( $match ) && is_array( $match ) && count( $match ) == 3 )
{
$originals = $match[0];
$names = $match[1];
$values = $match[2];
if ( count( $originals ) == count( $names ) && count( $names ) == count( $values ) )
{
$metaTags = array();
for ( $i = 0, $limiti = count( $names ); $i < $limiti; $i++ )
{
$metaTags[ $names[$i] ] = array(
'html' => htmlentities( $originals[$i] ),
'value' => $values[$i]
);
}
}
$meta_googlebot = (string)$metaTags['googlebot']['value'];
}
$mdb -> update( 'project_links_internal', [ 'meta_googlebot' => $meta_googlebot ], [ 'id' => $url_id ] );
}
public static function get_site_code_lenght( $url_id, $response )
{
global $mdb;
$mdb -> update( 'project_links_internal', [ 'code_lenght' => strlen( $response ) ], [ 'id' => $url_id ] );
}
public static function get_site_text_lenght( $url_id, $response )
{
global $mdb;
$mdb -> update( 'project_links_internal', [ 'text_lenght' => strlen( \S::strip_html_tags( $response ) ) ], [ 'id' => $url_id ] );
$repo = new \Domain\Cron\CronRepository($mdb);
return $repo->getSiteOtherLinks();
}
}

View File

@@ -1,4 +1,4 @@
<?
<?php
namespace front\factory;
class Authors
{
@@ -6,17 +6,7 @@ class Authors
static public function get_single_author( $id_author )
{
global $mdb;
if ( !$author = \Cache::fetch( "get_single_author:$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;
$repo = new \Domain\Authors\AuthorsRepository($mdb);
return $repo->authorByLang($id_author);
}
}

View File

@@ -6,58 +6,14 @@ class Banners
public static function banners()
{
global $mdb, $lang;
if ( !$banners = \Cache::fetch( 'banners' ) )
{
$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;
$repo = new \Domain\Banners\BannersRepository($mdb);
return $repo->activeBanners($lang[0]);
}
public static function main_banner()
{
global $mdb, $lang;
if ( !$banner = \Cache::fetch( "main_banner:" . $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;
$repo = new \Domain\Banners\BannersRepository($mdb);
return $repo->mainBanner($lang[0]);
}
}

View File

@@ -6,113 +6,49 @@ class Newsletter
public static function newsletter_unsubscribe( $hash )
{
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 )
{
global $mdb;
if ( !$id = $mdb -> get( 'pp_newsletter', 'id', [ 'AND' => [ 'hash' => $hash, 'status' => 0 ] ] ) )
return false;
else
$mdb -> update( 'pp_newsletter', [ 'status' => 1 ], [ 'id' => $id ] );
return true;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->confirm($hash);
}
public static function newsletter_send( $limit = 5 )
{
global $mdb, $settings, $lang;
$results = $mdb -> query( 'SELECT * FROM pp_newsletter_send WHERE mailed = 0 ORDER BY id ASC LIMIT ' . $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=' . \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;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->newsletterSend($limit, $settings, $lang);
}
public static function get_hash( $email )
{
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 )
{
global $mdb, $lang, $settings;
if ( !\S::email_check( $email ) )
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;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->signin($email, $settings, $lang);
}
public static function get_template( $template_name )
{
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 )
{
global $mdb;
if ( $mdb -> get( 'pp_newsletter', 'id', [ 'email' => $email ] ) )
return $mdb -> delete( 'pp_newsletter', [ 'email' => $email ] );
return false;
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
return $repo->signout($email);
}
}

View File

@@ -6,18 +6,7 @@ class Scontainers
public static function scontainer_details( $scontainer_id )
{
global $mdb, $lang;
if ( !$scontainer = \Cache::fetch( "scontainer_details:$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;
$repo = new \Domain\Scontainers\ScontainersRepository($mdb);
return $repo->scontainerByLang($scontainer_id, $lang[0]);
}
}

View File

@@ -283,11 +283,28 @@ if (Test-Path $zipPath) {
Remove-Item $zipPath -Force
}
# Pakuj zawartosc temp dir (bez folderu temp/)
$originalLocation = Get-Location
Set-Location $tempDir
Compress-Archive -Path '*' -DestinationPath "../../$zipPath" -Force
Set-Location $originalLocation
# Pakuj z forward-slashami (Compress-Archive uzywa backslashy na Windowsie)
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$absZipPath = (Resolve-Path $updatesDir).Path + "\ver_$versionNumber.zip"
$absTempDir = (Resolve-Path $tempDir).Path
$zipStream = [System.IO.File]::Create($absZipPath)
$zip = New-Object System.IO.Compression.ZipArchive($zipStream, [System.IO.Compression.ZipArchiveMode]::Create)
Get-ChildItem $absTempDir -Recurse -File | ForEach-Object {
$entryName = $_.FullName.Substring($absTempDir.Length + 1) -replace '\\', '/'
$entry = $zip.CreateEntry($entryName)
$entryStream = $entry.Open()
$fileStream = [System.IO.File]::OpenRead($_.FullName)
$fileStream.CopyTo($entryStream)
$fileStream.Close()
$entryStream.Close()
}
$zip.Dispose()
$zipStream.Close()
Write-Ok "Utworzono ZIP: $zipPath"

View File

@@ -1,4 +1,7 @@
{
"require": {
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^10.5"
},
@@ -6,8 +9,8 @@
"psr-4": {
"Domain\\": "autoload/Domain/",
"Shared\\": "autoload/Shared/",
"Admin\\": "autoload/Admin/",
"Frontend\\": "autoload/Frontend/"
"Admin\\": "autoload/admin/",
"Frontend\\": "autoload/front/"
}
},
"autoload-dev": {

View File

@@ -60,19 +60,50 @@ Stare klasy (`class.S.php`, `class.Cache.php`, itd.) są teraz cienkimi
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 ✓ — Domain Repositories (`autoload/Domain/`) — KOMPLETNE (13/13)
```
autoload/Domain/
├── Articles/ArticlesRepository.php ← \Domain\Articles\ArticlesRepository ✓
├── Authors/AuthorsRepository.php ← \Domain\Authors\AuthorsRepository ✓
├── Banners/BannersRepository.php ← \Domain\Banners\BannersRepository ✓
├── Cron/CronRepository.php ← \Domain\Cron\CronRepository ✓
├── Languages/LanguagesRepository.php ← \Domain\Languages\LanguagesRepository ✓
├── Settings/SettingsRepository.php ← \Domain\Settings\SettingsRepository ✓
├── User/UserRepository.php ← \Domain\User\UserRepository ✓
├── Pages/PagesRepository.php ← \Domain\Pages\PagesRepository ✓
├── Layouts/LayoutsRepository.php ← \Domain\Layouts\LayoutsRepository ✓
── Articles/ArticlesRepository.php ← \Domain\Articles\ArticlesRepository
── Newsletter/NewsletterRepository.php ← \Domain\Newsletter\NewsletterRepository ✓
├── Pages/PagesRepository.php ← \Domain\Pages\PagesRepository ✓
├── Releases/ReleasesRepository.php ← \Domain\Releases\ReleasesRepository ✓
├── Releases/UpdateRepository.php ← \Domain\Releases\UpdateRepository ✓
├── Scontainers/ScontainersRepository.php ← \Domain\Scontainers\ScontainersRepository ✓
├── SeoAdditional/SeoAdditionalRepository.php ← \Domain\SeoAdditional\SeoAdditionalRepository ✓
├── Settings/SettingsRepository.php ← \Domain\Settings\SettingsRepository ✓
└── User/UserRepository.php ← \Domain\User\UserRepository ✓
```
Następne: `Domain\Scontainers`, `Domain\Banners`, `Domain\Authors`, `Domain\Newsletter`, ...
### Faza 6 ✓ — Admin\ Base Infrastructure (`autoload/admin/{ViewModels,Validation,Support}/`)
Skopiowane z shopPRO i przeniesione pod `Admin\` (PSR-4 PascalCase namespace, lowercase folder ze względu na case-insensitive Windows fs):
```
autoload/admin/ ← namespace Admin\* (nowe) + admin\* (legacy)
├── ViewModels/Forms/ ← Admin\ViewModels\Forms
│ ├── FormEditViewModel.php
│ ├── FormField.php
│ ├── FormFieldType.php
│ ├── FormTab.php
│ └── FormAction.php
├── Validation/
│ └── FormValidator.php ← Admin\Validation\FormValidator
└── Support/
├── TableListRequestFactory.php ← Admin\Support — parser list (page/per_page/sort/filter)
└── Forms/
├── FormRequestHandler.php ← Admin\Support\Forms — POST + CSRF + validate + persist
└── FormFieldRenderer.php ← Admin\Support\Forms — HTML renderer (deleguje Shared\Html\Html)
```
Decyzja: **brak BaseController** — Phase 7+ kontrolery będą POJOs z DI w konstruktorze (jak shopPRO `BannerController(BannerRepository, LanguagesRepository)`).
Następne: `Admin\` controllers (Fazy 713), `Frontend\` namespace (Fazy 1416).
---
## Katalogi

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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