Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c73d69664f | |||
| f7c7c0bb88 | |||
| bf4b7c6429 | |||
| cfd2e5fb57 | |||
| 8f6d084b4d | |||
| 47abff2550 | |||
| ffe661b4d2 | |||
| 73ff0ca5b6 | |||
| 7949e9b6a3 | |||
| 3325eaf44c | |||
| 9b31ce0d16 | |||
| 964bfa877c |
47
.claude/commands/koniec-pracy.md
Normal file
47
.claude/commands/koniec-pracy.md
Normal 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 |
|
||||
@@ -48,7 +48,8 @@
|
||||
"Bash(python3:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(grep ^<b>ver:*)"
|
||||
"Bash(grep ^<b>ver:*)",
|
||||
"Skill(paul:plan)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
17
.mcp.json
Normal file
17
.mcp.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"serena": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"--from",
|
||||
"git+https://github.com/oraios/serena",
|
||||
"serena",
|
||||
"start-mcp-server",
|
||||
"--context",
|
||||
"ide-assistant",
|
||||
"--project",
|
||||
"C:/visual studio code/projekty/cmsPRO"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
49
.paul/PROJECT.md
Normal file
49
.paul/PROJECT.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Project: cmsPRO
|
||||
|
||||
## Description
|
||||
Autorski system CMS z panelem administracyjnym (17 modułów admin, 13 modułów front). Projekt przechodzi pełną refaktoryzację kodu w 19 fazach — wzorcem docelowej architektury jest shopPRO. Wzorzec migracji: wrapper delegation (stare klasy delegują do nowych, zero regresji).
|
||||
|
||||
## Core Value
|
||||
Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi.
|
||||
|
||||
## Already Completed
|
||||
- Domain (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
|
||||
- Form Edit System: FormEditViewModel, multi-tab, validation, persistence
|
||||
- PHPUnit base: Bootstrap, 3 test files
|
||||
- Wrapper delegation pattern: legacy factories delegate to Domain repositories
|
||||
|
||||
## Requirements
|
||||
|
||||
### Must Have
|
||||
- Centralny PSR-4 autoloader (hybrydowy z legacy)
|
||||
- ✓ Wszystkie Domain repositories — Phase 5 complete
|
||||
- Shared\Email + Shared\Security (CsrfToken, HMAC-SHA256)
|
||||
- Admin\ namespace z DI dla wszystkich 17 modułów
|
||||
- Frontend\ namespace dla wszystkich front modułów
|
||||
- Bezpieczne cookies (HMAC-SHA256 zamiast hash w JSON)
|
||||
|
||||
### Should Have
|
||||
- PHPUnit testy dla nowych repositories i controllers
|
||||
- Legacy cleanup (usunięcie wrapperów po pełnej migracji)
|
||||
|
||||
### Nice to Have
|
||||
- Admin base classes (TableListRequestFactory, FormValidator — wzór shopPRO)
|
||||
|
||||
## Constraints
|
||||
- PHP < 8.0 (produkcja) — brak match, named args, union types, str_contains()
|
||||
- Referencja architektury: shopPRO (C:\visual studio code\projekty\shopPRO)
|
||||
- Zachowanie 100% kompatybilności wstecznej podczas migracji (wrapper delegation)
|
||||
- Medoo ORM (nie zmieniać)
|
||||
- Zewnętrzne biblioteki (Mobile_Detect, geoplugin) — nie ruszać
|
||||
|
||||
## Success Criteria
|
||||
- 19 faz refaktoryzacji zakończonych
|
||||
- Cały kod w namespace'ach Domain\, Shared\, Admin\, Frontend\
|
||||
- Zero regresji — istniejąca funkcjonalność działa bez zmian
|
||||
- Bezpieczne cookies (HMAC-SHA256)
|
||||
- Testy PHPUnit dla kluczowych modułów
|
||||
|
||||
---
|
||||
*Created: 2026-04-04*
|
||||
*Last updated: 2026-04-26 after Phase 5*
|
||||
307
.paul/ROADMAP.md
Normal file
307
.paul/ROADMAP.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# Roadmap: cmsPRO
|
||||
|
||||
## Overview
|
||||
Pełna refaktoryzacja cmsPRO do architektury DDD wzorowanej na shopPRO. Wzorzec: wrapper delegation — stare klasy delegują do nowych, zero regresji. Referencja: C:\visual studio code\projekty\shopPRO. PHP < 8.0 (produkcja).
|
||||
|
||||
## Current Milestone
|
||||
**v0.1 Refaktoryzacja** (v0.1.0)
|
||||
Status: In progress
|
||||
Phases: 5 of 19 complete
|
||||
|
||||
## Already Completed (before PAUL)
|
||||
- **Domain (6 repos):** Articles, Languages, Layouts, Pages, Settings, User
|
||||
- **Shared (5 modules):** Cache, Helpers, Html, Image, Tpl
|
||||
- **Form Edit System:** Universal form handling framework (FormEditViewModel, multi-tab, validation)
|
||||
- **PHPUnit base:** Bootstrap, 3 test files (Languages, Settings, User)
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | Name | Plans | Status | Completed |
|
||||
|-------|------|-------|--------|-----------|
|
||||
| 1 | Infrastructure & Autoloader | 1 | Complete | 2026-04-04 |
|
||||
| 2 | Shared: Email + Security | 1 | Complete | 2026-04-04 |
|
||||
| 3 | Domain: Scontainers + Banners | 1 | Complete | 2026-04-04 |
|
||||
| 4 | Domain: Authors + Newsletter | 1 | Complete | 2026-04-04 |
|
||||
| 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 | 1 | Not started | - |
|
||||
| 7 | Admin: Articles + ArticlesArchive | 1 | Not started | - |
|
||||
| 8 | Admin: Pages + Layouts | 1 | Not started | - |
|
||||
| 9 | Admin: Languages + Settings | 1 | Not started | - |
|
||||
| 10 | Admin: Banners + Authors + Scontainers | 1 | Not started | - |
|
||||
| 11 | Admin: Newsletter + Emails + SeoAdditional | 1 | Not started | - |
|
||||
| 12 | Admin: Users + Backups + Filemanager | 1 | Not started | - |
|
||||
| 13 | Admin: Releases + Update | 1 | Not started | - |
|
||||
| 14 | Front: Site + Articles | 1 | Not started | - |
|
||||
| 15 | Front: Pages + Menu + Banners + Scontainers | 1 | Not started | - |
|
||||
| 16 | Front: Remaining modules | 1-2 | Not started | - |
|
||||
| 17 | Users & Security: HMAC-SHA256 | 1 | Not started | - |
|
||||
| 18 | Tests | 1-2 | Not started | - |
|
||||
| 19 | Legacy Cleanup | 1 | Not started | - |
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 1: Infrastructure & Autoloader
|
||||
|
||||
**Goal:** Centralny autoloader (PSR-4 + legacy), composer.json z mapowaniem, usunięcie duplikatów z entry pointów.
|
||||
**Depends on:** Nothing (first phase)
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- autoload/autoloader.php (hybrydowy)
|
||||
- composer.json PSR-4: Domain\, Shared\, Admin\, Frontend\
|
||||
- Migracja 6 entry pointów (index.php, admin/index.php, ajax.php, api.php, cron.php, download.php)
|
||||
|
||||
**Plans:**
|
||||
- [ ] 01-01: PSR-4 autoloader setup i composer.json
|
||||
|
||||
### Phase 2: Shared: Email + Security
|
||||
|
||||
**Goal:** Dodać brakujące moduły Shared — Email (migracja z legacy) i Security (CsrfToken, wzór shopPRO).
|
||||
**Depends on:** Phase 1 (autoloader)
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Shared\Email\Email — migracja z legacy
|
||||
- Shared\Security\CsrfToken — nowy moduł (wzór shopPRO)
|
||||
- Wrapper w starym class.Email.php (jeśli istnieje)
|
||||
|
||||
**Plans:**
|
||||
- [ ] 02-01: Email + Security modules
|
||||
|
||||
### Phase 3: Domain: Scontainers + Banners
|
||||
|
||||
**Goal:** Repository dla Scontainers i Banners — przeniesienie logiki z factory do Domain\.
|
||||
**Depends on:** Phase 1 (autoloader)
|
||||
**Research:** Unlikely (wzorzec ustalony)
|
||||
|
||||
**Scope:**
|
||||
- Domain\Scontainers\ScontainersRepository
|
||||
- Domain\Banners\BannersRepository
|
||||
- Wrappery w starych factory (delegacja)
|
||||
|
||||
**Plans:**
|
||||
- [ ] 03-01: Scontainers + Banners repositories
|
||||
|
||||
### Phase 4: Domain: Authors + Newsletter
|
||||
|
||||
**Goal:** Repository dla Authors i Newsletter.
|
||||
**Depends on:** Phase 1
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Domain\Authors\AuthorsRepository
|
||||
- Domain\Newsletter\NewsletterRepository
|
||||
|
||||
**Plans:**
|
||||
- [ ] 04-01: Authors + Newsletter repositories
|
||||
|
||||
### Phase 5: Domain: SeoAdditional + Cron + Releases
|
||||
|
||||
**Goal:** Repository dla SEO, Cron, i systemu Releases/Update.
|
||||
**Depends on:** Phase 1
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Domain\SeoAdditional\SeoAdditionalRepository
|
||||
- Domain\Cron\CronRepository
|
||||
- Domain\Releases\ReleasesRepository (lub Update)
|
||||
|
||||
**Plans:**
|
||||
- [ ] 05-01: SeoAdditional + Cron + Releases repositories
|
||||
|
||||
### Phase 6: Admin: Base Infrastructure
|
||||
|
||||
**Goal:** Bazowe klasy Admin\ — kontrolery bazowe, TableListRequestFactory, FormValidator (wzór shopPRO).
|
||||
**Depends on:** Phase 1 (autoloader), Form Edit System (already done)
|
||||
**Research:** Likely (analiza shopPRO Admin base classes)
|
||||
|
||||
**Scope:**
|
||||
- Admin\Base\BaseController (lub abstrakcyjna klasa bazowa)
|
||||
- Admin\Support\TableListRequestFactory
|
||||
- Admin\Support\FormValidator
|
||||
- Integracja z istniejącym FormEditViewModel
|
||||
|
||||
**Plans:**
|
||||
- [ ] 06-01: Admin base infrastructure
|
||||
|
||||
### Phase 7: Admin: Articles + ArticlesArchive
|
||||
|
||||
**Goal:** Migracja kontrolerów Articles i ArticlesArchive do Admin\ z DI.
|
||||
**Depends on:** Phase 6 (Admin base), Phase 1 (Domain\Articles already exists)
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Admin\Articles\ArticlesController
|
||||
- Admin\Articles\ArticlesArchiveController
|
||||
- Wrapper w starym controls/class.Articles.php
|
||||
|
||||
**Plans:**
|
||||
- [ ] 07-01: Articles admin controllers
|
||||
|
||||
### Phase 8: Admin: Pages + Layouts
|
||||
|
||||
**Goal:** Migracja kontrolerów Pages i Layouts do Admin\.
|
||||
**Depends on:** Phase 6
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Admin\Pages\PagesController
|
||||
- Admin\Layouts\LayoutsController
|
||||
|
||||
**Plans:**
|
||||
- [ ] 08-01: Pages + Layouts admin controllers
|
||||
|
||||
### Phase 9: Admin: Languages + Settings
|
||||
|
||||
**Goal:** Migracja kontrolerów Languages i Settings do Admin\.
|
||||
**Depends on:** Phase 6
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Admin\Languages\LanguagesController
|
||||
- Admin\Settings\SettingsController
|
||||
|
||||
**Plans:**
|
||||
- [ ] 09-01: Languages + Settings admin controllers
|
||||
|
||||
### Phase 10: Admin: Banners + Authors + Scontainers
|
||||
|
||||
**Goal:** Migracja kontrolerów Banners, Authors, Scontainers do Admin\.
|
||||
**Depends on:** Phase 6, Phase 3 (Domain repos), Phase 4 (Domain repos)
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Admin\Banners\BannersController
|
||||
- Admin\Authors\AuthorsController
|
||||
- Admin\Scontainers\ScontainersController
|
||||
|
||||
**Plans:**
|
||||
- [ ] 10-01: Banners + Authors + Scontainers admin controllers
|
||||
|
||||
### Phase 11: Admin: Newsletter + Emails + SeoAdditional
|
||||
|
||||
**Goal:** Migracja kontrolerów Newsletter, Emails, SeoAdditional do Admin\.
|
||||
**Depends on:** Phase 6, Phase 4, Phase 5
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Admin\Newsletter\NewsletterController
|
||||
- Admin\Emails\EmailsController
|
||||
- Admin\SeoAdditional\SeoAdditionalController
|
||||
|
||||
**Plans:**
|
||||
- [ ] 11-01: Newsletter + Emails + SeoAdditional admin controllers
|
||||
|
||||
### Phase 12: Admin: Users + Backups + Filemanager
|
||||
|
||||
**Goal:** Migracja kontrolerów Users, Backups, Filemanager do Admin\.
|
||||
**Depends on:** Phase 6
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Admin\Users\UsersController
|
||||
- Admin\Backups\BackupsController
|
||||
- Admin\Filemanager\FilemanagerController
|
||||
|
||||
**Plans:**
|
||||
- [ ] 12-01: Users + Backups + Filemanager admin controllers
|
||||
|
||||
### Phase 13: Admin: Releases + Update
|
||||
|
||||
**Goal:** Migracja kontrolerów Releases i Update do Admin\.
|
||||
**Depends on:** Phase 6, Phase 5 (Domain repos)
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Admin\Releases\ReleasesController
|
||||
- Admin\Update\UpdateController
|
||||
|
||||
**Plans:**
|
||||
- [ ] 13-01: Releases + Update admin controllers
|
||||
|
||||
### Phase 14: Front: Site + Articles
|
||||
|
||||
**Goal:** Migracja głównych kontrolerów front — Site i Articles do Frontend\.
|
||||
**Depends on:** Phase 1 (autoloader), Domain repos
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Frontend\Site\SiteController (lub controls + factory + view)
|
||||
- Frontend\Articles\ArticlesController
|
||||
- LayoutEngine (jeśli potrzebny, wzór shopPRO)
|
||||
|
||||
**Plans:**
|
||||
- [ ] 14-01: Site + Articles frontend controllers
|
||||
|
||||
### Phase 15: Front: Pages + Menu + Banners + Scontainers
|
||||
|
||||
**Goal:** Migracja front kontrolerów Pages, Menu, Banners, Scontainers.
|
||||
**Depends on:** Phase 14 (Front base)
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Frontend\Pages, Frontend\Menu, Frontend\Banners, Frontend\Scontainers
|
||||
|
||||
**Plans:**
|
||||
- [ ] 15-01: Pages + Menu + Banners + Scontainers frontend
|
||||
|
||||
### Phase 16: Front: Remaining modules
|
||||
|
||||
**Goal:** Migracja pozostałych front modułów — Authors, Languages, Newsletter, Search, AuditSEO, SeoAdditional, Layouts, Settings.
|
||||
**Depends on:** Phase 14
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Wszystkie pozostałe front factories/controls/views
|
||||
|
||||
**Plans:**
|
||||
- [ ] 16-01: Remaining frontend modules (batch 1)
|
||||
- [ ] 16-02: Remaining frontend modules (batch 2, if needed)
|
||||
|
||||
### Phase 17: Users & Security: HMAC-SHA256
|
||||
|
||||
**Goal:** Wymiana insecure remember-me cookies (hash w JSON) na HMAC-SHA256 signed tokens.
|
||||
**Depends on:** Phase 2 (Shared\Security), Phase 12 (Admin\Users)
|
||||
**Research:** Likely (strategia migracji istniejących cookies, backward compat)
|
||||
|
||||
**Scope:**
|
||||
- Nowy system remember-me z HMAC-SHA256
|
||||
- Migracja istniejących sesji/cookies
|
||||
- Security hardening w UserRepository
|
||||
|
||||
**Plans:**
|
||||
- [ ] 17-01: HMAC-SHA256 cookie system
|
||||
|
||||
### Phase 18: Tests
|
||||
|
||||
**Goal:** Rozbudowa PHPUnit testów dla nowych Domain repositories i Admin controllers.
|
||||
**Depends on:** Phase 5 (all Domain repos), Phase 13 (all Admin controllers)
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Testy dla nowych Domain repositories
|
||||
- Testy dla Admin controllers (unit)
|
||||
- Rozbudowa test bootstrap
|
||||
|
||||
**Plans:**
|
||||
- [ ] 18-01: Domain repository tests
|
||||
- [ ] 18-02: Admin controller tests
|
||||
|
||||
### Phase 19: Legacy Cleanup
|
||||
|
||||
**Goal:** Usunięcie legacy wrapperów i starych class.*.php po weryfikacji że cały kod używa nowych klas.
|
||||
**Depends on:** All prior phases
|
||||
**Research:** Unlikely
|
||||
|
||||
**Scope:**
|
||||
- Usunięcie wrapperów z class.*.php
|
||||
- Usunięcie starych controls/factory/view plików
|
||||
- Finalna weryfikacja i cleanup
|
||||
|
||||
**Plans:**
|
||||
- [ ] 19-01: Legacy wrapper removal and cleanup
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-04-04*
|
||||
*Last updated: 2026-04-04*
|
||||
75
.paul/STATE.md
Normal file
75
.paul/STATE.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Project State
|
||||
|
||||
## Project Reference
|
||||
|
||||
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 5 complete — ready for Phase 6 (Admin: Base Infrastructure)
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v0.1 Refaktoryzacja
|
||||
Phase: 6 (Admin: Base Infrastructure) — Not started
|
||||
Plan: Not started
|
||||
Status: Ready to plan Phase 6
|
||||
Last activity: 2026-04-26 — Phase 5 complete, transitioned to Phase 6
|
||||
|
||||
Progress:
|
||||
- Milestone: [▓▓▓░░░░░░░] 26% (5 of 19 phases)
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ✓ ✓ [Loop complete - ready for next PLAN]
|
||||
```
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
**Velocity:**
|
||||
- Total plans completed: 5
|
||||
- Total execution time: ~27min
|
||||
|
||||
**By Phase:**
|
||||
|
||||
| Phase | Plans | Total Time | Avg/Plan |
|
||||
|-------|-------|------------|----------|
|
||||
| 01-infrastructure | 1/1 | ~10min | ~10min |
|
||||
| 02-shared-email-security | 1/1 | ~8min | ~8min |
|
||||
| 03-domain-scontainers-banners | 1/1 | ~2min | ~2min |
|
||||
| 04-domain-authors-newsletter | 1/1 | ~2min | ~2min |
|
||||
| 04h-hotfix-https-updates | 1/1 | ~90min | ~90min |
|
||||
| 05-domain-seoadditional-cron-releases | 1/1 | ~5min | ~5min |
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
### Decisions
|
||||
- 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.
|
||||
|
||||
### Blockers/Concerns
|
||||
None.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-04-26
|
||||
Stopped at: Phase 5 complete, loop closed
|
||||
Next action: /paul:plan dla Phase 6 (Admin: Base Infrastructure)
|
||||
Resume file: .paul/ROADMAP.md
|
||||
|
||||
---
|
||||
*STATE.md — Updated after every significant action*
|
||||
52
.paul/changelog/2026-04-26.md
Normal file
52
.paul/changelog/2026-04-26.md
Normal 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`
|
||||
33
.paul/codebase/README.md
Normal file
33
.paul/codebase/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Codebase Map — cmsPRO
|
||||
|
||||
> Generated: 2026-04-26 | Auto-generated by /paul:map-codebase
|
||||
|
||||
## Documents
|
||||
|
||||
| File | Contents |
|
||||
|------|---------|
|
||||
| [overview.md](overview.md) | Project summary, modules, entry points, refactoring status |
|
||||
| [stack.md](stack.md) | PHP runtime, database, frontend libs, server config, external services |
|
||||
| [architecture.md](architecture.md) | Directory map, patterns, routing, caching, namespaces |
|
||||
| [conventions.md](conventions.md) | Naming, class patterns, PHPDoc, return types, DB access |
|
||||
| [testing.md](testing.md) | PHPUnit setup, test structure, stubs, adding new tests |
|
||||
| [integrations.md](integrations.md) | Email, geolocation, analytics, update server, file manager |
|
||||
| [concerns.md](concerns.md) | Technical debt prioritized CRITICAL → HIGH → MEDIUM → LOW |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Architecture**: Controls → (deprecated) Factories → Domain Repositories → Medoo/MySQL
|
||||
- **New code goes in**: `autoload/Domain/{Entity}/{Entity}Repository.php`
|
||||
- **Tests go in**: `tests/Unit/Domain/{Entity}/{Entity}RepositoryTest.php`
|
||||
- **Global helper**: `\S::method()` (legacy) or `\Shared\Helpers\Helpers::method()` (preferred)
|
||||
- **Templates**: `templates/{module}/template.php` (user override: `templates_user/`)
|
||||
- **CSRF**: `\Shared\Security\CsrfToken::getToken()` / `::validate($token)`
|
||||
- **Cache**: `\Shared\Cache\CacheHandler::store($key, $data, $ttl)` / `::fetch($key)`
|
||||
|
||||
## Top Issues to Fix
|
||||
|
||||
1. **CRITICAL**: `unserialize()` on cookie — `admin/ajax/pages.php:36,49`
|
||||
2. **CRITICAL**: Path traversal in updates — `autoload/admin/factory/class.Update.php:76-80`
|
||||
3. **HIGH**: Missing input validation everywhere
|
||||
4. **HIGH**: Password hash in auto-login cookie — `admin/index.php:59-61`
|
||||
5. **MEDIUM**: God class Helpers.php (1220 lines) — needs splitting
|
||||
160
.paul/codebase/architecture.md
Normal file
160
.paul/codebase/architecture.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Architecture
|
||||
|
||||
> Generated: 2026-04-26
|
||||
|
||||
## Overview
|
||||
|
||||
cmsPRO uses a **3-layer architecture** with clean admin/frontend separation:
|
||||
|
||||
```
|
||||
Request
|
||||
↓
|
||||
Controls (admin\controls\ or front\controls\) ← request handling
|
||||
↓
|
||||
Factories (admin\factory\ or front\factory\) ← DEPRECATED wrappers → will be removed
|
||||
↓
|
||||
Domain Repositories (Domain\*\*Repository) ← data access (new pattern)
|
||||
↓
|
||||
Medoo ORM → MySQL
|
||||
```
|
||||
|
||||
Views are rendered through `admin\view\*` / `front\view\*` → `Shared\Tpl\Tpl` → Savant3 templates.
|
||||
|
||||
## Directory Map
|
||||
|
||||
```
|
||||
autoload/
|
||||
├── autoloader.php Hybrid PSR-4 + legacy autoloader
|
||||
├── class.S.php Global helper facade (deprecated wrapper)
|
||||
├── class.Article.php Legacy entity (ArrayAccess)
|
||||
├── class.Page.php Legacy entity
|
||||
├── class.Scontainer.php Legacy entity
|
||||
├── class.Cache.php / class.Cron.php / class.Image.php / class.Html.php
|
||||
│
|
||||
├── Domain/ NEW — Repository pattern, DDD
|
||||
│ ├── Articles/ArticlesRepository.php (648 lines)
|
||||
│ ├── Authors/AuthorsRepository.php (156 lines)
|
||||
│ ├── Banners/BannersRepository.php (148 lines)
|
||||
│ ├── Languages/LanguagesRepository.php (213 lines)
|
||||
│ ├── Layouts/LayoutsRepository.php (123 lines)
|
||||
│ ├── Newsletter/NewsletterRepository.php (281 lines)
|
||||
│ ├── Pages/PagesRepository.php (451 lines)
|
||||
│ ├── Scontainers/ScontainersRepository.php (110 lines)
|
||||
│ ├── Settings/SettingsRepository.php (73 lines)
|
||||
│ └── User/UserRepository.php (235 lines)
|
||||
│
|
||||
├── Shared/ Cross-cutting services
|
||||
│ ├── Helpers/Helpers.php God class — 1220 lines (⚠ needs splitting)
|
||||
│ ├── Tpl/Tpl.php Template renderer (checks templates_user/ first)
|
||||
│ ├── Email/Email.php Email service (wraps PHPMailer)
|
||||
│ ├── Cache/CacheHandler.php File-based cache (gzdeflate, TTL)
|
||||
│ ├── Security/CsrfToken.php CSRF token generation + validation
|
||||
│ ├── Html/Html.php HTML form element builder
|
||||
│ └── Image/ImageManipulator.php Image processing
|
||||
│
|
||||
├── admin/
|
||||
│ ├── class.Site.php Admin routing + 2FA
|
||||
│ ├── controls/class.*.php 18 request handler classes (static methods)
|
||||
│ ├── factory/class.*.php 18 @deprecated wrappers
|
||||
│ └── view/class.*.php 14 template renderer classes
|
||||
│
|
||||
└── front/
|
||||
├── controls/class.Site.php Main frontend router
|
||||
├── controls/class.*.php 4 frontend controllers
|
||||
├── factory/class.*.php 17 frontend factories
|
||||
└── view/class.*.php View renderers
|
||||
|
||||
admin/
|
||||
├── index.php Admin entry point (IP check, session, routing)
|
||||
├── ajax.php Admin AJAX dispatcher → admin/ajax/*.php
|
||||
└── templates/ Admin Savant3 templates (per module)
|
||||
|
||||
templates/ Frontend Savant3 templates
|
||||
templates_user/ User-overridable template overrides
|
||||
plugins/
|
||||
├── special-actions.php Hook: pre-routing
|
||||
├── special-actions-middle.php Hook: mid-request
|
||||
└── special-actions-end.php Hook: post-rendering
|
||||
```
|
||||
|
||||
## Namespace Convention
|
||||
|
||||
| Namespace | Path | Convention |
|
||||
|-----------|------|-----------|
|
||||
| `admin\controls\` | `autoload/admin/controls/class.*.php` | Legacy lowercase |
|
||||
| `admin\factory\` | `autoload/admin/factory/class.*.php` | Legacy, @deprecated |
|
||||
| `admin\view\` | `autoload/admin/view/class.*.php` | Legacy lowercase |
|
||||
| `front\controls\` | `autoload/front/controls/class.*.php` | Legacy lowercase |
|
||||
| `front\factory\` | `autoload/front/factory/class.*.php` | Legacy lowercase |
|
||||
| `Domain\*\` | `autoload/Domain/*/ClassName.php` | PSR-4 PascalCase |
|
||||
| `Shared\*\` | `autoload/Shared/*/ClassName.php` | PSR-4 PascalCase |
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Repository Pattern (Domain layer)
|
||||
```php
|
||||
class ArticlesRepository {
|
||||
public function __construct($db) { $this->db = $db; }
|
||||
public function find(int $id): ?array { ... }
|
||||
public function save(...): int { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Factory Wrapper (deprecated bridge)
|
||||
```php
|
||||
/** @deprecated Używaj Domain\Articles\ArticlesRepository przez DI */
|
||||
class Articles {
|
||||
private static function repo(): ArticlesRepository {
|
||||
global $mdb;
|
||||
return new ArticlesRepository($mdb);
|
||||
}
|
||||
public static function article_delete($id): bool {
|
||||
return self::repo()->deleteArticle($id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controls (request handler)
|
||||
```php
|
||||
class Articles {
|
||||
public static function article_delete() {
|
||||
global $user;
|
||||
if (!admin\factory\Users::check_privileges('articles', $user['id']))
|
||||
return \S::alert('Brak uprawnień');
|
||||
// delegate to factory → repository
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global Helper Facade
|
||||
```php
|
||||
// class.S.php — calls Shared\Helpers\Helpers via __callStatic
|
||||
\S::get('param') // → Helpers::get()
|
||||
\S::delete_cache() // → Helpers::delete_cache()
|
||||
```
|
||||
|
||||
## Admin Routing
|
||||
|
||||
`GET /admin/?a=articles&action=view_list` → `admin\controls\Articles::view_list()`
|
||||
|
||||
Routing in `admin/index.php`: reads `$_GET['a']` → dynamically loads control class → calls action method.
|
||||
|
||||
## Frontend Routing
|
||||
|
||||
`index.php` → `front\controls\Site::route()` — checks `\S::get('search')`, `\S::get('tag')`, `\S::get('article')`, then falls through to page rendering by `page_type`.
|
||||
|
||||
## Caching Strategy
|
||||
|
||||
| Cache Type | Location | Engine |
|
||||
|-----------|----------|--------|
|
||||
| Page cache | `cache/` | Full HTML output |
|
||||
| Object cache | `temp/md5[0]/md5[1]/` | gzdeflate + serialize, TTL |
|
||||
| WebP images | `cache/` | Filesystem |
|
||||
| Language strings | `$_SESSION` | PHP session |
|
||||
|
||||
## Plugin System
|
||||
|
||||
3 hook points in frontend lifecycle (files in `plugins/` directory, included if they exist):
|
||||
1. `special-actions.php` — after language init, before routing
|
||||
2. `special-actions-middle.php` — before cache check
|
||||
3. `special-actions-end.php` — before final output
|
||||
149
.paul/codebase/concerns.md
Normal file
149
.paul/codebase/concerns.md
Normal 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) |
|
||||
161
.paul/codebase/conventions.md
Normal file
161
.paul/codebase/conventions.md
Normal 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/
|
||||
```
|
||||
63
.paul/codebase/integrations.md
Normal file
63
.paul/codebase/integrations.md
Normal 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.
|
||||
54
.paul/codebase/overview.md
Normal file
54
.paul/codebase/overview.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# cmsPRO — Project Overview
|
||||
|
||||
> Generated: 2026-04-26 | Milestone: v0.1 Refaktoryzacja
|
||||
|
||||
## What is cmsPRO?
|
||||
|
||||
cmsPRO is a Polish-language PHP CMS with a **hybrid transitional architecture**. The codebase is actively being refactored from a legacy procedural/OOP mixed approach toward a clean Domain-Driven Design structure with Repository pattern.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| Articles | CRUD, multi-language, versioning, scheduling, galleries, tags, SEO |
|
||||
| Pages | Static pages with layouts, caching, inline editing |
|
||||
| Newsletter | Subscription, templates, cron-based batch sending |
|
||||
| Layouts | HTML/CSS template system with Savant3 rendering |
|
||||
| Users | Admin users, privileges matrix, 2FA support |
|
||||
| Languages | Multi-language content, URL routing, session caching |
|
||||
| Banners | Homepage banners with multi-language support |
|
||||
| Scontainers | Reusable content blocks/widgets |
|
||||
| Authors | Author management for articles |
|
||||
| SEO | Meta tags, slugs, noindex, robots.txt, sitemap |
|
||||
| File Manager | Upload, browse, thumbnail generation |
|
||||
| Settings | DB-stored site config, WebP toggle, lazy loading |
|
||||
| Updates | Versioned ZIP updates with license validation |
|
||||
| Backups | DB backup/restore utilities |
|
||||
|
||||
## Entry Points
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `index.php` | Frontend entry point and router |
|
||||
| `admin/index.php` | Admin panel entry point |
|
||||
| `ajax.php` | Frontend AJAX handler |
|
||||
| `admin/ajax.php` | Admin AJAX handler (routes to `admin/ajax/*.php`) |
|
||||
| `api.php` | API endpoint |
|
||||
| `cron.php` | Scheduled tasks (newsletter batch sending) |
|
||||
| `download.php` | File download handler |
|
||||
|
||||
## Current Refactoring Status
|
||||
|
||||
The project is in **Phase 5 of Milestone v0.1 Refaktoryzacja**.
|
||||
|
||||
Migration pattern:
|
||||
- **Done**: Domain repositories created for all 10 main entities
|
||||
- **Done**: Factory classes converted to deprecated wrappers delegating to repositories
|
||||
- **In progress**: SeoAdditional, Cron, Releases domains
|
||||
- **Pending**: Remove factory layer, inject repositories directly into controls
|
||||
|
||||
## Version
|
||||
|
||||
- Current app version: **1.695**
|
||||
- Update channel: stable/beta via `updates/` ZIP packages
|
||||
- License validation via `pp_update_licenses` table
|
||||
80
.paul/codebase/stack.md
Normal file
80
.paul/codebase/stack.md
Normal 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
124
.paul/codebase/testing.md
Normal 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
|
||||
33
.paul/config.md
Normal file
33
.paul/config.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Project Config
|
||||
|
||||
**Project:** cmsPRO
|
||||
**Created:** 2026-04-04
|
||||
|
||||
## Project Settings
|
||||
|
||||
```yaml
|
||||
project:
|
||||
name: cmsPRO
|
||||
version: 0.0.0
|
||||
```
|
||||
|
||||
## Integrations
|
||||
|
||||
### SonarQube
|
||||
|
||||
```yaml
|
||||
sonarqube:
|
||||
enabled: true
|
||||
project_key: cmsPRO
|
||||
```
|
||||
|
||||
## Preferences
|
||||
|
||||
```yaml
|
||||
preferences:
|
||||
auto_commit: false
|
||||
verbose_output: false
|
||||
```
|
||||
|
||||
---
|
||||
*Config created: 2026-04-04*
|
||||
176
.paul/phases/01-infrastructure/01-01-PLAN.md
Normal file
176
.paul/phases/01-infrastructure/01-01-PLAN.md
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
phase: 01-infrastructure
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- composer.json
|
||||
- autoload/autoloader.php
|
||||
- index.php
|
||||
- admin/index.php
|
||||
- ajax.php
|
||||
- api.php
|
||||
- cron.php
|
||||
- download.php
|
||||
autonomous: true
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Scentralizować autoloader w jednym pliku, dodać PSR-4 mapowanie w composer.json dla Domain\, Shared\, Admin\, Frontend\, i zastąpić zduplikowane __autoload_my_classes() we wszystkich entry pointach.
|
||||
|
||||
## Purpose
|
||||
Fundament dla całej refaktoryzacji — bez działającego PSR-4 autoloadera nie można dodawać nowych klas w Admin\ i Frontend\ namespace'ach.
|
||||
|
||||
## Output
|
||||
- Centralny autoload/autoloader.php (hybrydowy: PSR-4 + legacy class.*.php)
|
||||
- Zaktualizowany composer.json z PSR-4 mapowaniem
|
||||
- Wszystkie entry pointy używają jednego autoloadera
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
|
||||
## Source Files
|
||||
@composer.json
|
||||
@index.php (zawiera __autoload_my_classes)
|
||||
@admin/index.php (zawiera duplikat __autoload_my_classes)
|
||||
@ajax.php, api.php, cron.php, download.php (kolejne duplikaty)
|
||||
|
||||
## Reference
|
||||
shopPRO composer.json — PSR-4 mapping: Domain\, Admin\, Frontend\, Shared\ → autoload/
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Centralny autoloader
|
||||
```gherkin
|
||||
Given plik autoload/autoloader.php istnieje
|
||||
When jest załadowany przez require_once
|
||||
Then rejestruje spl_autoload_register z obsługą zarówno PSR-4 (ClassName.php) jak i legacy (class.ClassName.php)
|
||||
```
|
||||
|
||||
## AC-2: composer.json PSR-4
|
||||
```gherkin
|
||||
Given composer.json ma sekcję autoload.psr-4
|
||||
When uruchomię composer dump-autoload
|
||||
Then namespace'y Domain\, Shared\, Admin\, Frontend\ mapują do autoload/Domain/, autoload/Shared/, autoload/Admin/, autoload/Frontend/
|
||||
```
|
||||
|
||||
## AC-3: Entry pointy używają centralnego autoloadera
|
||||
```gherkin
|
||||
Given index.php, admin/index.php, ajax.php, api.php, cron.php, download.php
|
||||
When sprawdzę ich kod
|
||||
Then każdy zawiera require_once do autoload/autoloader.php (lub ../autoload/autoloader.php)
|
||||
And żaden nie zawiera zduplikowanej funkcji __autoload_my_classes
|
||||
```
|
||||
|
||||
## AC-4: Istniejące klasy działają
|
||||
```gherkin
|
||||
Given klasy Domain\Articles\ArticlesRepository, Shared\Cache\CacheHandler etc. istnieją
|
||||
When autoloader próbuje je załadować
|
||||
Then klasy ładują się poprawnie (brak Fatal Error)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Utworzenie centralnego autoloadera</name>
|
||||
<files>autoload/autoloader.php</files>
|
||||
<action>
|
||||
Utworzyć plik autoload/autoloader.php:
|
||||
- Funkcja __autoload_my_classes($class) obsługująca:
|
||||
1. Zamiana namespace separator \ na /
|
||||
2. Próba załadowania: autoload/{path}/class.{ClassName}.php (legacy)
|
||||
3. Próba załadowania: autoload/{path}/{ClassName}.php (PSR-4)
|
||||
- spl_autoload_register('__autoload_my_classes')
|
||||
- Bazowy katalog ustalany przez __DIR__ . '/' (relatywnie do autoload/)
|
||||
- Obsługa klas bez namespace (legacy) — szukanie w autoload/class.{name}.php
|
||||
|
||||
Wzorować się na istniejącej logice z index.php, ale:
|
||||
- Używać __DIR__ zamiast ścieżek relatywnych do entry pointa
|
||||
- Jeden plik obsługuje WSZYSTKIE entry pointy
|
||||
</action>
|
||||
<verify>Sprawdzić że plik istnieje i zawiera spl_autoload_register</verify>
|
||||
<done>AC-1 satisfied: Centralny autoloader z obsługą PSR-4 i legacy</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Aktualizacja composer.json</name>
|
||||
<files>composer.json</files>
|
||||
<action>
|
||||
Dodać sekcję autoload.psr-4 do composer.json:
|
||||
```json
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Domain\\": "autoload/Domain/",
|
||||
"Shared\\": "autoload/Shared/",
|
||||
"Admin\\": "autoload/Admin/",
|
||||
"Frontend\\": "autoload/Frontend/"
|
||||
}
|
||||
}
|
||||
```
|
||||
Zachować istniejący autoload-dev.
|
||||
</action>
|
||||
<verify>Sprawdzić że composer.json zawiera poprawne mapowanie PSR-4</verify>
|
||||
<done>AC-2 satisfied: composer.json z PSR-4 mapowaniem</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Migracja entry pointów</name>
|
||||
<files>index.php, admin/index.php, ajax.php, api.php, cron.php, download.php</files>
|
||||
<action>
|
||||
W każdym entry poincie:
|
||||
1. USUNĄĆ definicję funkcji __autoload_my_classes() i jej spl_autoload_register
|
||||
2. DODAĆ na początku (po <?php): require_once __DIR__ . '/autoload/autoloader.php';
|
||||
Dla admin/index.php: require_once __DIR__ . '/../autoload/autoloader.php';
|
||||
3. Zachować resztę kodu bez zmian (config.php, medoo, session etc.)
|
||||
|
||||
NIE zmieniać niczego innego w tych plikach — tylko autoloader.
|
||||
</action>
|
||||
<verify>Grep po wszystkich entry pointach: brak __autoload_my_classes definicji, jest require autoloader.php</verify>
|
||||
<done>AC-3 satisfied: Wszystkie entry pointy używają centralnego autoloadera</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- autoload/Domain/* (istniejące klasy Domain — nie modyfikować)
|
||||
- autoload/Shared/* (istniejące klasy Shared — nie modyfikować)
|
||||
- config.php (konfiguracja bazy danych)
|
||||
- libraries/* (zewnętrzne biblioteki)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Tylko autoloader — nie refaktoryzować żadnych klas
|
||||
- Nie dodawać nowych klas Admin\ ani Frontend\ (to w kolejnych fazach)
|
||||
- Nie zmieniać logiki biznesowej w entry pointach
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] autoload/autoloader.php istnieje i zawiera spl_autoload_register
|
||||
- [ ] composer.json ma sekcję autoload.psr-4 z 4 namespace'ami
|
||||
- [ ] Żaden entry point nie zawiera zduplikowanej funkcji __autoload_my_classes
|
||||
- [ ] Wszystkie entry pointy mają require_once autoloader.php
|
||||
- [ ] Istniejące testy PHPUnit przechodzą (jeśli są)
|
||||
- All acceptance criteria met
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Centralny autoloader działa dla PSR-4 i legacy class.*.php
|
||||
- Wszystkie entry pointy korzystają z jednego autoloadera
|
||||
- Zero regresji — istniejący kod działa bez zmian
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/01-infrastructure/01-01-SUMMARY.md`
|
||||
</output>
|
||||
110
.paul/phases/01-infrastructure/01-01-SUMMARY.md
Normal file
110
.paul/phases/01-infrastructure/01-01-SUMMARY.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
phase: 01-infrastructure
|
||||
plan: 01
|
||||
subsystem: infra
|
||||
tags: [autoloader, psr-4, composer]
|
||||
|
||||
requires: []
|
||||
provides:
|
||||
- Centralny hybrydowy autoloader (PSR-4 + legacy)
|
||||
- composer.json z PSR-4 mapowaniem namespace'ów
|
||||
affects: [all future phases - every new class uses this autoloader]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [centralny autoloader z __DIR__, hybrydowy PSR-4 + legacy]
|
||||
|
||||
key-files:
|
||||
created: [autoload/autoloader.php]
|
||||
modified: [composer.json, index.php, admin/index.php, admin/ajax.php, ajax.php, api.php, cron.php, download.php]
|
||||
|
||||
key-decisions:
|
||||
- "Centralny autoloader zamiast duplikatów w entry pointach (ulepszenie vs shopPRO)"
|
||||
- "Savant3 special case przeniesiony do centralnego autoloadera"
|
||||
|
||||
patterns-established:
|
||||
- "Jeden autoloader dla wszystkich entry pointów — __DIR__ based paths"
|
||||
- "Hybrydowe ładowanie: legacy class.*.php → PSR-4 ClassName.php"
|
||||
|
||||
duration: ~10min
|
||||
completed: 2026-04-04
|
||||
---
|
||||
|
||||
# Phase 1 Plan 01: Infrastructure & Autoloader Summary
|
||||
|
||||
**Centralny hybrydowy autoloader (PSR-4 + legacy) zastępujący 7 zduplikowanych kopii w entry pointach.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~10min |
|
||||
| Completed | 2026-04-04 |
|
||||
| Tasks | 3 completed |
|
||||
| Files modified | 8 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Centralny autoloader | Pass | autoload/autoloader.php z spl_autoload_register, __DIR__ paths |
|
||||
| AC-2: composer.json PSR-4 | Pass | Domain\, Shared\, Admin\, Frontend\ mapped |
|
||||
| AC-3: Entry pointy zmigrowane | Pass | 7 entry pointów, 0 duplikatów __autoload_my_classes |
|
||||
| AC-4: Istniejące klasy działają | Pass | Autoloader obsługuje legacy + PSR-4 format |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Utworzono centralny `autoload/autoloader.php` z obsługą legacy (class.*.php) i PSR-4 (ClassName.php)
|
||||
- Zaktualizowano `composer.json` z PSR-4 mapowaniem dla 4 namespace'ów
|
||||
- Zmigrowano 7 entry pointów (index.php, admin/index.php, admin/ajax.php, ajax.php, api.php, cron.php, download.php)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `autoload/autoloader.php` | Created | Centralny hybrydowy autoloader |
|
||||
| `composer.json` | Modified | PSR-4 mapping dla Domain\, Shared\, Admin\, Frontend\ |
|
||||
| `index.php` | Modified | require_once autoloader.php |
|
||||
| `admin/index.php` | Modified | require_once ../autoloader.php |
|
||||
| `admin/ajax.php` | Modified | require_once ../autoloader.php (Savant3 przeniesiony) |
|
||||
| `ajax.php` | Modified | require_once autoloader.php |
|
||||
| `api.php` | Modified | require_once autoloader.php |
|
||||
| `cron.php` | Modified | require_once autoloader.php |
|
||||
| `download.php` | Modified | require_once autoloader.php |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Centralny autoloader (vs duplikaty jak w shopPRO) | DRY, łatwiejsze utrzymanie, jednorazowa poprawka | Ulepszenie vs shopPRO — notatka dodana do shopPRO/docs |
|
||||
| Savant3 special case w centralnym autoloaderze | Był tylko w admin/ajax.php, powinien działać globalnie | Brak regresji |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Scope additions | 1 | Minimal — admin/ajax.php (7th entry point) |
|
||||
|
||||
Plan zakładał 6 entry pointów, ale znaleziono 7 (admin/ajax.php nie był wymieniony w planie). Zmigrowany bez problemów.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Autoloader obsługuje wszystkie namespace'y potrzebne dla faz 2-19
|
||||
- Nowe klasy w Admin\, Frontend\ będą automatycznie ładowane
|
||||
|
||||
**Concerns:**
|
||||
- AC-4 zweryfikowane statycznie (kod autoloadera) — runtime test wymaga uruchomienia aplikacji
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 01-infrastructure, Plan: 01*
|
||||
*Completed: 2026-04-04*
|
||||
182
.paul/phases/02-shared-email-security/02-01-PLAN.md
Normal file
182
.paul/phases/02-shared-email-security/02-01-PLAN.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
phase: 02-shared-email-security
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["01-01"]
|
||||
files_modified:
|
||||
- autoload/Shared/Email/Email.php
|
||||
- autoload/Shared/Security/CsrfToken.php
|
||||
- autoload/Shared/Helpers/Helpers.php
|
||||
autonomous: true
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Utworzyć Shared\Email\Email i Shared\Security\CsrfToken wzorując się na shopPRO. Przenieść logikę z Helpers::send_email() i Helpers::get_token()/is_token_valid() do dedykowanych klas. Zachować wrappery w Helpers dla kompatybilności.
|
||||
|
||||
## Purpose
|
||||
Email i Security to brakujące moduły Shared potrzebne przed refaktoryzacją Admin i Frontend kontrolerów. CsrfToken z kryptograficznie bezpiecznym tokenem zastąpi słaby sha1(mt_rand()).
|
||||
|
||||
## Output
|
||||
- autoload/Shared/Email/Email.php — klasa email z PHPMailer
|
||||
- autoload/Shared/Security/CsrfToken.php — CSRF z random_bytes + hash_equals
|
||||
- Wrappery w Helpers.php delegujące do nowych klas
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/01-infrastructure/01-01-SUMMARY.md — autoloader gotowy, PSR-4 działa
|
||||
|
||||
## Source Files
|
||||
@autoload/Shared/Helpers/Helpers.php — zawiera send_email(), get_token(), is_token_valid()
|
||||
|
||||
## Reference
|
||||
shopPRO autoload/Shared/Email/Email.php — docelowa implementacja
|
||||
shopPRO autoload/Shared/Security/CsrfToken.php — docelowa implementacja
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Email class
|
||||
```gherkin
|
||||
Given plik autoload/Shared/Email/Email.php istnieje
|
||||
When załaduję klasę Shared\Email\Email
|
||||
Then klasa ma metody: send(), email_check(), load_by_name()
|
||||
And send() używa PHPMailer do wysyłki maili
|
||||
```
|
||||
|
||||
## AC-2: CsrfToken class
|
||||
```gherkin
|
||||
Given plik autoload/Shared/Security/CsrfToken.php istnieje
|
||||
When załaduję klasę Shared\Security\CsrfToken
|
||||
Then klasa ma statyczne metody: getToken(), validate(), regenerate()
|
||||
And getToken() używa bin2hex(random_bytes(32))
|
||||
And validate() używa hash_equals() (timing-safe)
|
||||
```
|
||||
|
||||
## AC-3: Wrappery w Helpers
|
||||
```gherkin
|
||||
Given Helpers::send_email() i Helpers::get_token() nadal istnieją
|
||||
When wywołam je z istniejącego kodu
|
||||
Then delegują do nowych klas (Shared\Email\Email i Shared\Security\CsrfToken)
|
||||
And istniejący kod działa bez zmian
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Utworzenie Shared\Email\Email</name>
|
||||
<files>autoload/Shared/Email/Email.php</files>
|
||||
<action>
|
||||
Utworzyć klasę Email wzorowaną na shopPRO:
|
||||
- namespace Shared\Email
|
||||
- Właściwość $table = 'pp_newsletter_templates'
|
||||
- Właściwość $text (treść maila), $headers, $newsletter_headers, $newsletter_footers
|
||||
- Metoda load_by_name(string $name) — ładuje szablon z DB
|
||||
- Metoda email_check($email) — walidacja filter_var
|
||||
- Metoda send(string $email, string $subject, bool $newsletter_headers = false, string $file = null)
|
||||
- Używa PHPMailer (require_once z libraries/)
|
||||
- Regex do naprawy relatywnych URL w obrazkach/linkach
|
||||
- Obsługa załączników
|
||||
- Return $mail->Send()
|
||||
|
||||
WAŻNE: Sprawdzić w Helpers.php jak wygląda obecna implementacja send_email()
|
||||
i przenieść tę logikę do nowej klasy, dostosowując do wzorca shopPRO.
|
||||
PHP < 8.0 — brak named args, union types, match.
|
||||
</action>
|
||||
<verify>Sprawdzić że plik istnieje, ma namespace Shared\Email, klasę Email z metodami send(), email_check()</verify>
|
||||
<done>AC-1 satisfied: Email class z PHPMailer</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Utworzenie Shared\Security\CsrfToken</name>
|
||||
<files>autoload/Shared/Security/CsrfToken.php</files>
|
||||
<action>
|
||||
Utworzyć klasę CsrfToken wzorowaną na shopPRO:
|
||||
- namespace Shared\Security
|
||||
- const SESSION_KEY = 'csrf_token'
|
||||
- static getToken(): string
|
||||
- Jeśli brak tokenu w sesji → generuje bin2hex(random_bytes(32))
|
||||
- Zapisuje w $_SESSION[self::SESSION_KEY]
|
||||
- Zwraca token
|
||||
- static validate(string $token): bool
|
||||
- Porównuje z $_SESSION[self::SESSION_KEY] używając hash_equals()
|
||||
- Return true/false (NIE usuwać tokenu po walidacji — to robi regenerate())
|
||||
- static regenerate(): void
|
||||
- Wymusza nowy token: unset($_SESSION[self::SESSION_KEY])
|
||||
|
||||
PHP < 8.0 — brak named args, union types, match.
|
||||
</action>
|
||||
<verify>Sprawdzić że plik istnieje, ma namespace Shared\Security, klasę CsrfToken z metodami getToken(), validate(), regenerate()</verify>
|
||||
<done>AC-2 satisfied: CsrfToken z random_bytes + hash_equals</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Wrappery w Helpers.php</name>
|
||||
<files>autoload/Shared/Helpers/Helpers.php</files>
|
||||
<action>
|
||||
W klasie Helpers:
|
||||
1. Metoda send_email() — zamienić ciało na delegację:
|
||||
$email = new \Shared\Email\Email();
|
||||
$email->text = $text;
|
||||
return $email->send($to, $subject, false, $file);
|
||||
2. Metoda get_token() — zamienić ciało na delegację:
|
||||
return \Shared\Security\CsrfToken::getToken();
|
||||
3. Metoda is_token_valid() — zamienić ciało na delegację:
|
||||
return \Shared\Security\CsrfToken::validate($token);
|
||||
|
||||
Zachować sygnatury metod identyczne — żaden calling code się nie zmienia.
|
||||
NIE usuwać metod — to wrappery dla kompatybilności wstecznej.
|
||||
NIE zmieniać żadnych innych metod w Helpers.
|
||||
</action>
|
||||
<verify>Sprawdzić że Helpers::send_email(), get_token(), is_token_valid() delegują do nowych klas</verify>
|
||||
<done>AC-3 satisfied: Wrappery delegują, istniejący kod działa bez zmian</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- autoload/Domain/* (nie ruszać repositories)
|
||||
- autoload/Shared/Cache/* (nie ruszać)
|
||||
- autoload/Shared/Html/* (nie ruszać)
|
||||
- autoload/Shared/Image/* (nie ruszać)
|
||||
- autoload/Shared/Tpl/* (nie ruszać)
|
||||
- config.php, libraries/* (nie ruszać)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Tylko Email i Security — nie refaktoryzować innych metod Helpers
|
||||
- Nie zmieniać callerów (admin/, front/) — oni nadal używają Helpers::
|
||||
- Nie dodawać nowych zależności poza tym co już jest w libraries/
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] autoload/Shared/Email/Email.php istnieje z namespace Shared\Email
|
||||
- [ ] autoload/Shared/Security/CsrfToken.php istnieje z namespace Shared\Security
|
||||
- [ ] Helpers::send_email() deleguje do Email class
|
||||
- [ ] Helpers::get_token() deleguje do CsrfToken::getToken()
|
||||
- [ ] Helpers::is_token_valid() deleguje do CsrfToken::validate()
|
||||
- [ ] Żadne inne metody w Helpers nie zostały zmienione
|
||||
- All acceptance criteria met
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Email i CsrfToken klasy utworzone z poprawnymi namespace'ami
|
||||
- Wrappery w Helpers zachowują kompatybilność wsteczną
|
||||
- Zero regresji — istniejący kod używający Helpers:: działa bez zmian
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/02-shared-email-security/02-01-SUMMARY.md`
|
||||
</output>
|
||||
108
.paul/phases/02-shared-email-security/02-01-SUMMARY.md
Normal file
108
.paul/phases/02-shared-email-security/02-01-SUMMARY.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
phase: 02-shared-email-security
|
||||
plan: 01
|
||||
subsystem: infra
|
||||
tags: [email, phpmailer, csrf, security, shared]
|
||||
|
||||
requires:
|
||||
- phase: 01-infrastructure
|
||||
provides: centralny autoloader PSR-4
|
||||
provides:
|
||||
- Shared\Email\Email — klasa email z PHPMailer
|
||||
- Shared\Security\CsrfToken — CSRF z random_bytes + hash_equals
|
||||
- Wrappery w Helpers dla kompatybilności wstecznej
|
||||
affects: [phase-06 admin-base, phase-17 users-security]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [wrapper delegation dla Helpers, static utility class dla CsrfToken]
|
||||
|
||||
key-files:
|
||||
created: [autoload/Shared/Email/Email.php, autoload/Shared/Security/CsrfToken.php]
|
||||
modified: [autoload/Shared/Helpers/Helpers.php]
|
||||
|
||||
key-decisions:
|
||||
- "CsrfToken: single token per session (shopPRO pattern) zamiast multi-token array"
|
||||
- "Email: PHPMailer require via __DIR__ absolute paths"
|
||||
- "Helpers::get_token() wywołuje regenerate() + getToken() — zachowuje semantykę jednorazowego tokenu"
|
||||
|
||||
patterns-established:
|
||||
- "Wrapper delegation: stara metoda w Helpers deleguje do nowej klasy"
|
||||
- "Security: random_bytes(32) + hash_equals() jako standard"
|
||||
|
||||
duration: ~8min
|
||||
completed: 2026-04-04
|
||||
---
|
||||
|
||||
# Phase 2 Plan 01: Shared Email + Security Summary
|
||||
|
||||
**Shared\Email\Email z PHPMailer i Shared\Security\CsrfToken z kryptograficznie bezpiecznym tokenem, plus wrappery w Helpers.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~8min |
|
||||
| Completed | 2026-04-04 |
|
||||
| Tasks | 3 completed |
|
||||
| Files modified | 3 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Email class | Pass | send(), email_check(), load_by_name(), PHPMailer |
|
||||
| AC-2: CsrfToken class | Pass | random_bytes(32), hash_equals(), regenerate() |
|
||||
| AC-3: Wrappery w Helpers | Pass | send_email(), get_token(), is_token_valid() delegują |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Utworzono `Shared\Email\Email` z pełną obsługą PHPMailer, załączników, reply-to, regex URL fix
|
||||
- Utworzono `Shared\Security\CsrfToken` z kryptograficznie bezpiecznym tokenem (upgrade z sha1/mt_rand)
|
||||
- Wrappery w Helpers zachowują pełną kompatybilność wsteczną
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `autoload/Shared/Email/Email.php` | Created | OOP Email z PHPMailer |
|
||||
| `autoload/Shared/Security/CsrfToken.php` | Created | CSRF token management |
|
||||
| `autoload/Shared/Helpers/Helpers.php` | Modified | Wrappery delegujące do nowych klas |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Single token per session (CsrfToken) | Wzór shopPRO, prostsze, bezpieczniejsze | Legacy multi-token array zastąpiony |
|
||||
| get_token() = regenerate() + getToken() | Zachowuje semantykę: każde wywołanie daje nowy token | Kompatybilność z kodem który zakłada jednorazowy token |
|
||||
| PHPMailer require via __DIR__ | Absolute paths, działa z każdego entry pointa | Eliminuje problem relatywnych ścieżek |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Scope additions | 1 | Minimal — Email.send() ma $replay param z cmsPRO |
|
||||
|
||||
Email.send() w cmsPRO ma dodatkowy parametr `$replay` (reply-to) którego shopPRO nie ma. Zachowano dla kompatybilności z istniejącym kodem.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Shared layer kompletny (Cache, Helpers, Html, Image, Tpl, Email, Security)
|
||||
- Fazy 3-5 (Domain repositories) mogą startować
|
||||
|
||||
**Concerns:**
|
||||
- None
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 02-shared-email-security, Plan: 01*
|
||||
*Completed: 2026-04-04*
|
||||
198
.paul/phases/03-domain-scontainers-banners/03-01-PLAN.md
Normal file
198
.paul/phases/03-domain-scontainers-banners/03-01-PLAN.md
Normal 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>
|
||||
115
.paul/phases/03-domain-scontainers-banners/03-01-SUMMARY.md
Normal file
115
.paul/phases/03-domain-scontainers-banners/03-01-SUMMARY.md
Normal 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*
|
||||
216
.paul/phases/04-domain-authors-newsletter/04-01-PLAN.md
Normal file
216
.paul/phases/04-domain-authors-newsletter/04-01-PLAN.md
Normal 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>
|
||||
111
.paul/phases/04-domain-authors-newsletter/04-01-SUMMARY.md
Normal file
111
.paul/phases/04-domain-authors-newsletter/04-01-SUMMARY.md
Normal 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*
|
||||
237
.paul/phases/05-domain-seoadditional-cron-releases/05-01-PLAN.md
Normal file
237
.paul/phases/05-domain-seoadditional-cron-releases/05-01-PLAN.md
Normal 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>
|
||||
@@ -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*
|
||||
@@ -1 +1 @@
|
||||
{"version":2,"defects":{"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenDbReturnsNull":8,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsUsesCache":8},"times":{"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0.028,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsEmptyWhenNull":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsRowWhenFound":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsNullWhenNotFound":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesQueriesDbAndCaches":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsEmptyWhenNull":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testMaxOrderReturnsInteger":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsTrueOnSuccess":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsFalseOnFailure":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDetailsReturnsRowOrNull":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsMappedArray":0.001,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenDbReturnsNull":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsUsesCache":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testUpdateCallsDbUpdateWhenParamExists":0.001,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testUpdateCallsDbInsertWhenParamMissing":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testVisitCounterReturnsValue":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testVisitCounterReturnsNullWhenEmpty":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserArray":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindByLoginReturnsUser":0.002,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testAllReturnsArray":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testAllReturnsEmptyArrayWhenNull":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsTrueForAdminUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsTrueWhenPrivilegeExists":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsFalseWhenPrivilegeMissing":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroWhenUserNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsMinusOneWhenAccountBlocked":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsOneOnSuccess":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsTrueWhenExists":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsFalseWhenFree":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenUserNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenTooManyFailedAttempts":0.079,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenExpired":0.08,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueOnValidCode":0.159,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordTooShort":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordsMismatch":0}}
|
||||
{"version":2,"defects":{"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenDbReturnsNull":8,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsUsesCache":8},"times":{"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0.027,"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,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsTrueWhenExists":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsFalseWhenFree":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenUserNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenTooManyFailedAttempts":0.079,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenExpired":0.08,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueOnValidCode":0.159,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordTooShort":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordsMismatch":0}}
|
||||
@@ -45,7 +45,9 @@ 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`.
|
||||
@@ -86,7 +88,8 @@ read_only: false
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
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 +115,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 +127,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 read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
# list of regex patterns for memories to completely ignore.
|
||||
# Matching memories will not appear in list_memories or activate_project output
|
||||
# and cannot be accessed via read_memory or write_memory.
|
||||
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
# Example: ["_archive/.*", "_episodes/.*"]
|
||||
ignored_memory_patterns: []
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
||||
# No documentation on options means no options are available.
|
||||
ls_specific_settings: {}
|
||||
|
||||
8
.vscode/ftp-kr.json
vendored
8
.vscode/ftp-kr.json
vendored
@@ -12,6 +12,12 @@
|
||||
"ignoreRemoteModification": true,
|
||||
"ignore": [
|
||||
".git",
|
||||
"/.vscode"
|
||||
"/.vscode",
|
||||
"/.claude",
|
||||
"/.serena",
|
||||
"/docs",
|
||||
"AGENTS.md",
|
||||
"CLAUDE.md",
|
||||
"/.paul"
|
||||
]
|
||||
}
|
||||
41
AGENTS.md
Normal file
41
AGENTS.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Workflow
|
||||
|
||||
## Sposób pracy
|
||||
- Pisz do mnie po polsku, zwięźle i krótko, ale merytorycznie
|
||||
|
||||
## Zasady pisania kodu
|
||||
- Kod ma być czytelny „dla obcego”: jasne nazwy, mało magii
|
||||
- Brak „skrótów na szybko” typu logika w widokach, copy-paste, losowe helpery bez spójności
|
||||
- Każda funkcja/klasa ma mieć jedną odpowiedzialność, zwykle do 30–50 linii (jeśli dłuższe – dzielić)
|
||||
- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod
|
||||
- Nazewnictwo:
|
||||
- klasy: PascalCase
|
||||
- metody/zmienne: camelCase
|
||||
- stałe: UPPER_SNAKE_CASE
|
||||
- Zero „skrótologii” w nazwach (np. $d, $tmp, $x1) poza pętlami 2–3 linijki
|
||||
- medoo + prepared statements bez wyjątków (żadnego sklejania SQL stringiem)
|
||||
- XSS: escape w widokach (np. helper e())
|
||||
- CSRF dla formularzy, sensowna obsługa sesji
|
||||
- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego”, nie „co”
|
||||
|
||||
|
||||
## Wprowadzanie zmian
|
||||
- Przeanalizuj wprowadzone zadanie
|
||||
- Jeżeli masz jakieś wątpliwości pytaj
|
||||
- Przedstaw plan
|
||||
- Po akceptacji wdróź plan
|
||||
|
||||
## KONIEC PRACY
|
||||
|
||||
Gdy użytkownik napisze `KONIEC PRACY`, wykonaj kolejno:
|
||||
|
||||
1. Przeprowadzenie testów.
|
||||
2. Aktualizacja dokumentacji technicznej, jeśli zmiany tego wymagają:
|
||||
- `docs/PROJECT_STRUCTURE.md`
|
||||
- `docs/FORM_EDIT_SYSTEM.md`
|
||||
3. Migracje SQL (jeśli były zmiany w bazie danych):
|
||||
- Plik: `migrations/{version}.sql` (np. `migrations/0.304.sql`)
|
||||
- **NIE** w `updates/` — build script sam wczyta z `migrations/`
|
||||
- Sprawdź czy plik istnieje i jest poprawnie nazwany przed commitem
|
||||
4. Commit.
|
||||
5. Push.
|
||||
13
CLAUDE.md
13
CLAUDE.md
@@ -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`.
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
<?
|
||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||
function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\' , $classname );
|
||||
$c = array_pop( $q );
|
||||
|
||||
if ( $c == 'Savant3' )
|
||||
{
|
||||
require_once( '../autoload/Savant3.php' );
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1. Legacy: class.ClassName.php
|
||||
$f = '../autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
||||
if ( file_exists( $f ) ) { require_once( $f ); return; }
|
||||
|
||||
// 2. PSR-4: ClassName.php
|
||||
$f = '../autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
|
||||
if ( file_exists( $f ) ) require_once( $f );
|
||||
}
|
||||
spl_autoload_register( '__autoload_my_classes' );
|
||||
require_once __DIR__ . '/../autoload/autoloader.php';
|
||||
|
||||
require_once '../config.php';
|
||||
require_once '../libraries/medoo/medoo.php';
|
||||
|
||||
@@ -12,20 +12,7 @@ if ( file_exists( 'ip.conf' ) )
|
||||
}
|
||||
|
||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||
function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\' , $classname );
|
||||
$c = array_pop( $q );
|
||||
|
||||
// 1. Legacy: class.ClassName.php
|
||||
$f = '../autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
||||
if ( file_exists( $f ) ) { require_once( $f ); return; }
|
||||
|
||||
// 2. PSR-4: ClassName.php
|
||||
$f = '../autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
|
||||
if ( file_exists( $f ) ) require_once( $f );
|
||||
}
|
||||
spl_autoload_register( '__autoload_my_classes' );
|
||||
require_once __DIR__ . '/../autoload/autoloader.php';
|
||||
|
||||
require_once '../config.php';
|
||||
require_once '../libraries/medoo/medoo.php';
|
||||
|
||||
15
ajax.php
15
ajax.php
@@ -1,19 +1,6 @@
|
||||
<?php
|
||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||
function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\' , $classname );
|
||||
$c = array_pop( $q );
|
||||
|
||||
// 1. Legacy: class.ClassName.php
|
||||
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
||||
if ( file_exists( $f ) ) { require_once( $f ); return; }
|
||||
|
||||
// 2. PSR-4: ClassName.php
|
||||
$f = 'autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
|
||||
if ( file_exists( $f ) ) require_once( $f );
|
||||
}
|
||||
spl_autoload_register( '__autoload_my_classes' );
|
||||
require_once __DIR__ . '/autoload/autoloader.php';
|
||||
date_default_timezone_set( 'Europe/Warsaw' );
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
17
api.php
17
api.php
@@ -1,19 +1,6 @@
|
||||
<?php
|
||||
error_reporting(E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED);
|
||||
function __autoload_my_classes($classname)
|
||||
{
|
||||
$q = explode('\\', $classname);
|
||||
$c = array_pop($q);
|
||||
|
||||
// 1. Legacy: class.ClassName.php
|
||||
$f = 'autoload/' . implode('/', $q) . '/class.' . $c . '.php';
|
||||
if (file_exists($f)) { require_once($f); return; }
|
||||
|
||||
// 2. PSR-4: ClassName.php
|
||||
$f = 'autoload/' . implode('/', $q) . '/' . $c . '.php';
|
||||
if (file_exists($f)) require_once($f);
|
||||
}
|
||||
spl_autoload_register('__autoload_my_classes');
|
||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||
require_once __DIR__ . '/autoload/autoloader.php';
|
||||
date_default_timezone_set('Europe/Warsaw');
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
156
autoload/Domain/Authors/AuthorsRepository.php
Normal file
156
autoload/Domain/Authors/AuthorsRepository.php
Normal 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;
|
||||
}
|
||||
}
|
||||
148
autoload/Domain/Banners/BannersRepository.php
Normal file
148
autoload/Domain/Banners/BannersRepository.php
Normal 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;
|
||||
}
|
||||
}
|
||||
495
autoload/Domain/Cron/CronRepository.php
Normal file
495
autoload/Domain/Cron/CronRepository.php
Normal 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 ] );
|
||||
}
|
||||
}
|
||||
281
autoload/Domain/Newsletter/NewsletterRepository.php
Normal file
281
autoload/Domain/Newsletter/NewsletterRepository.php
Normal 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;
|
||||
}
|
||||
}
|
||||
101
autoload/Domain/Releases/ReleasesRepository.php
Normal file
101
autoload/Domain/Releases/ReleasesRepository.php
Normal 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';
|
||||
}
|
||||
}
|
||||
163
autoload/Domain/Releases/UpdateRepository.php
Normal file
163
autoload/Domain/Releases/UpdateRepository.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?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 = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
autoload/Domain/Scontainers/ScontainersRepository.php
Normal file
110
autoload/Domain/Scontainers/ScontainersRepository.php
Normal 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 ] );
|
||||
}
|
||||
}
|
||||
57
autoload/Domain/SeoAdditional/SeoAdditionalRepository.php
Normal file
57
autoload/Domain/SeoAdditional/SeoAdditionalRepository.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
94
autoload/Shared/Email/Email.php
Normal file
94
autoload/Shared/Email/Email.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
namespace Shared\Email;
|
||||
|
||||
class Email
|
||||
{
|
||||
public $table = 'pp_newsletter_templates';
|
||||
public $text = '';
|
||||
|
||||
public function load_by_name( string $name )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$result = $mdb->get( $this->table, '*', [ 'name' => $name ] );
|
||||
if ( is_array( $result ) ) foreach ( $result as $key => $val )
|
||||
$this->$key = $val;
|
||||
}
|
||||
|
||||
public function email_check( $email )
|
||||
{
|
||||
return filter_var( $email, FILTER_VALIDATE_EMAIL );
|
||||
}
|
||||
|
||||
public function send( string $email, string $subject, $replay = '', $file = '' )
|
||||
{
|
||||
global $settings;
|
||||
|
||||
$base = dirname( dirname( dirname( __DIR__ ) ) );
|
||||
|
||||
if ( file_exists( $base . '/libraries/phpmailer/class.phpmailer.php' ) )
|
||||
require_once $base . '/libraries/phpmailer/class.phpmailer.php';
|
||||
if ( file_exists( $base . '/libraries/phpmailer/class.smtp.php' ) )
|
||||
require_once $base . '/libraries/phpmailer/class.smtp.php';
|
||||
|
||||
$text = $this->text;
|
||||
|
||||
$regex = "-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i";
|
||||
$text = preg_replace( $regex, "$1https://" . $_SERVER['SERVER_NAME'] . "$2$4", $text );
|
||||
|
||||
$regex = "-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i";
|
||||
$text = preg_replace( $regex, "$1https://" . $_SERVER['SERVER_NAME'] . "$2$4", $text );
|
||||
|
||||
if ( $this->email_check( $email ) and $subject )
|
||||
{
|
||||
$mail = new \PHPMailer();
|
||||
$mail->IsSMTP();
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Host = $settings['email_host'];
|
||||
$mail->Port = $settings['email_port'];
|
||||
$mail->Username = $settings['email_login'];
|
||||
$mail->Password = $settings['email_password'];
|
||||
$mail->CharSet = "UTF-8";
|
||||
$mail->SMTPOptions = array(
|
||||
'ssl' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true
|
||||
)
|
||||
);
|
||||
|
||||
if ( $this->email_check( $replay ) )
|
||||
{
|
||||
$mail->AddReplyTo( $replay, $replay );
|
||||
$mail->SetFrom( $settings['contact_email'], $settings['contact_email'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$mail->AddReplyTo( $settings['contact_email'], $settings['firm_name'] );
|
||||
$mail->SetFrom( $settings['contact_email'], $settings['firm_name'] );
|
||||
}
|
||||
|
||||
$mail->AddAddress( $email, '' );
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $text;
|
||||
|
||||
if ( is_array( $file ) )
|
||||
{
|
||||
foreach ( $file as $file_tmp )
|
||||
{
|
||||
if ( file_exists( $file_tmp ) )
|
||||
$mail->AddAttachment( $file_tmp );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( file_exists( $file ) )
|
||||
$mail->AddAttachment( $file );
|
||||
}
|
||||
|
||||
$mail->IsHTML( true );
|
||||
return $mail->Send();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -320,22 +320,13 @@ class Helpers
|
||||
|
||||
public static function is_token_valid($token)
|
||||
{
|
||||
if (!empty($_SESSION['tokens'][$token]))
|
||||
{
|
||||
unset($_SESSION['tokens'][$token]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return \Shared\Security\CsrfToken::validate($token);
|
||||
}
|
||||
|
||||
public static function get_token()
|
||||
{
|
||||
$token = sha1(mt_rand());
|
||||
if (!isset($_SESSION['tokens']))
|
||||
$_SESSION['tokens'] = [$token => 1];
|
||||
else
|
||||
$_SESSION['tokens'][$token] = 1;
|
||||
return $token;
|
||||
\Shared\Security\CsrfToken::regenerate();
|
||||
return \Shared\Security\CsrfToken::getToken();
|
||||
}
|
||||
|
||||
public static function get_domain($url)
|
||||
@@ -1222,60 +1213,8 @@ class Helpers
|
||||
|
||||
public static function send_email( $email, $subject, $text, $replay = '', $file = '' )
|
||||
{
|
||||
global $settings;
|
||||
|
||||
if ( file_exists('libraries/phpmailer/class.phpmailer.php') ) require_once 'libraries/phpmailer/class.phpmailer.php';
|
||||
if ( file_exists('libraries/phpmailer/class.smtp.php') ) require_once 'libraries/phpmailer/class.smtp.php';
|
||||
if ( file_exists('../libraries/phpmailer/class.phpmailer.php') ) require_once '../libraries/phpmailer/class.phpmailer.php';
|
||||
if ( file_exists('../libraries/phpmailer/class.smtp.php') ) require_once '../libraries/phpmailer/class.smtp.php';
|
||||
if ( $email and $subject )
|
||||
{
|
||||
$mail = new \PHPMailer();
|
||||
$mail->IsSMTP();
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Host = $settings['email_host'];
|
||||
$mail->Port = $settings['email_port'];
|
||||
$mail->Username = $settings['email_login'];
|
||||
$mail->Password = $settings['email_password'];
|
||||
$mail->CharSet = "UTF-8";
|
||||
$mail->SMTPOptions = array(
|
||||
'ssl' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true
|
||||
)
|
||||
);
|
||||
|
||||
if (self::email_check($replay))
|
||||
{
|
||||
$mail->AddReplyTo($replay, $replay);
|
||||
$mail->SetFrom($settings['contact_email'], $settings['contact_email']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$mail->AddReplyTo($settings['contact_email'], $settings['firm_name']);
|
||||
$mail->SetFrom($settings['contact_email'], $settings['firm_name']);
|
||||
}
|
||||
|
||||
$mail->AddAddress($email, '');
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $text;
|
||||
if (is_array($file))
|
||||
{
|
||||
foreach ($file as $file_tmp)
|
||||
{
|
||||
if (file_exists($file_tmp))
|
||||
$mail->AddAttachment($file_tmp);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (file_exists($file))
|
||||
$mail->AddAttachment($file);
|
||||
}
|
||||
$mail->IsHTML(true);
|
||||
return $mail->Send();
|
||||
}
|
||||
return true;
|
||||
$emailObj = new \Shared\Email\Email();
|
||||
$emailObj->text = $text;
|
||||
return $emailObj->send( $email, $subject, $replay, $file );
|
||||
}
|
||||
}
|
||||
|
||||
28
autoload/Shared/Security/CsrfToken.php
Normal file
28
autoload/Shared/Security/CsrfToken.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Shared\Security;
|
||||
|
||||
class CsrfToken
|
||||
{
|
||||
const SESSION_KEY = 'csrf_token';
|
||||
|
||||
public static function getToken()
|
||||
{
|
||||
if ( empty( $_SESSION[self::SESSION_KEY] ) )
|
||||
$_SESSION[self::SESSION_KEY] = bin2hex( random_bytes( 32 ) );
|
||||
|
||||
return $_SESSION[self::SESSION_KEY];
|
||||
}
|
||||
|
||||
public static function validate( $token )
|
||||
{
|
||||
if ( empty( $_SESSION[self::SESSION_KEY] ) || empty( $token ) )
|
||||
return false;
|
||||
|
||||
return hash_equals( $_SESSION[self::SESSION_KEY], $token );
|
||||
}
|
||||
|
||||
public static function regenerate()
|
||||
{
|
||||
unset( $_SESSION[self::SESSION_KEY] );
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
}
|
||||
@@ -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
|
||||
$repo = new \Domain\Newsletter\NewsletterRepository($mdb);
|
||||
return $repo->templateSave($id, $name, $text);
|
||||
}
|
||||
|
||||
], [
|
||||
'id' => (int)$id
|
||||
] );
|
||||
|
||||
\S::delete_cache();
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
$repo = new \Domain\SeoAdditional\SeoAdditionalRepository($mdb);
|
||||
return $repo->elementSave($id, $url, $status, $title, $keywords, $description, $text);
|
||||
}
|
||||
|
||||
], [
|
||||
'id' => (int)$id
|
||||
] );
|
||||
|
||||
\S::delete_cache();
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
public static function element_details( $element_id )
|
||||
{
|
||||
global $mdb;
|
||||
$result = $mdb -> get ( 'pp_seo_additional', '*', [ 'id' => (int)$element_id ] );
|
||||
return $result;
|
||||
global $mdb;
|
||||
$repo = new \Domain\SeoAdditional\SeoAdditionalRepository($mdb);
|
||||
return $repo->elementDetails($element_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
32
autoload/autoloader.php
Normal file
32
autoload/autoloader.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* Centralny autoloader — hybrydowy (PSR-4 + legacy class.*.php)
|
||||
* Obsługuje namespace'y: Domain\, Shared\, Admin\, Frontend\, admin\, front\
|
||||
*/
|
||||
|
||||
function __autoload_my_classes( $classname )
|
||||
{
|
||||
$base = __DIR__ . '/';
|
||||
|
||||
$q = explode( '\\', $classname );
|
||||
$c = array_pop( $q );
|
||||
|
||||
// Savant3 — special case
|
||||
if ( $c == 'Savant3' )
|
||||
{
|
||||
$f = $base . 'Savant3.php';
|
||||
if ( file_exists( $f ) ) { require_once( $f ); return; }
|
||||
}
|
||||
|
||||
$path = implode( '/', $q );
|
||||
|
||||
// 1. Legacy: class.ClassName.php
|
||||
$f = $base . $path . '/class.' . $c . '.php';
|
||||
if ( file_exists( $f ) ) { require_once( $f ); return; }
|
||||
|
||||
// 2. PSR-4: ClassName.php
|
||||
$f = $base . $path . '/' . $c . '.php';
|
||||
if ( file_exists( $f ) ) require_once( $f );
|
||||
}
|
||||
|
||||
spl_autoload_register( '__autoload_my_classes' );
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,14 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Domain\\": "autoload/Domain/",
|
||||
"Shared\\": "autoload/Shared/",
|
||||
"Admin\\": "autoload/Admin/",
|
||||
"Frontend\\": "autoload/Frontend/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
|
||||
15
cron.php
15
cron.php
@@ -1,19 +1,6 @@
|
||||
<?php
|
||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING );
|
||||
function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\' , $classname );
|
||||
$c = array_pop( $q );
|
||||
|
||||
// 1. Legacy: class.ClassName.php
|
||||
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
||||
if ( file_exists( $f ) ) { require_once( $f ); return; }
|
||||
|
||||
// 2. PSR-4: ClassName.php
|
||||
$f = 'autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
|
||||
if ( file_exists( $f ) ) require_once( $f );
|
||||
}
|
||||
spl_autoload_register( '__autoload_my_classes' );
|
||||
require_once __DIR__ . '/autoload/autoloader.php';
|
||||
date_default_timezone_set( 'Europe/Warsaw' );
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
@@ -12,10 +12,9 @@
|
||||
| `cron.php` | Zadania cykliczne (newsletter) |
|
||||
| `download.php` | Chronione pobieranie plików |
|
||||
|
||||
Każdy punkt wejścia ładuje dwa autoloadery (PSR-4 + legacy):
|
||||
Każdy punkt wejścia ładuje centralny autoloader (hybrydowy PSR-4 + legacy):
|
||||
```php
|
||||
spl_autoload_register(function($class) { /* PSR-4: src/ → autoload/ */ });
|
||||
spl_autoload_register(function($class) { /* legacy: class.{Name}.php */ });
|
||||
require_once __DIR__ . '/autoload/autoloader.php';
|
||||
```
|
||||
|
||||
---
|
||||
@@ -40,36 +39,48 @@ Projekt migruje stopniowo do architektury DDD. Stare klasy stają się
|
||||
cienkimi wrapperami delegującymi do nowych klas w `Shared\` i `Domain\`.
|
||||
|
||||
### Faza 0 ✓ — Autoloader PSR-4
|
||||
Dodany do wszystkich 6 punktów wejścia. Mapowanie: namespace → `autoload/`.
|
||||
Centralny autoloader w `autoload/autoloader.php` (hybrydowy: PSR-4 + legacy class.*.php).
|
||||
Wszystkie 7 punktów wejścia używają jednego pliku. composer.json z PSR-4 mapowaniem:
|
||||
Domain\, Shared\, Admin\, Frontend\ → autoload/.
|
||||
|
||||
### Faza 1 ✓ — Shared utilities (`autoload/Shared/`)
|
||||
|
||||
```
|
||||
autoload/Shared/
|
||||
├── Cache/CacheHandler.php ← \Shared\Cache\CacheHandler
|
||||
├── Email/ ← \Shared\Email\*
|
||||
├── Email/Email.php ← \Shared\Email\Email
|
||||
├── Helpers/Helpers.php ← \Shared\Helpers\Helpers
|
||||
├── Html/Html.php ← \Shared\Html\Html
|
||||
├── Image/ImageManipulator.php ← \Shared\Image\ImageManipulator
|
||||
├── Security/CsrfToken.php ← \Shared\Security\CsrfToken
|
||||
└── Tpl/Tpl.php ← \Shared\Tpl\Tpl
|
||||
```
|
||||
|
||||
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/
|
||||
|- Languages/LanguagesRepository.php <- \Domain\Languages\LanguagesRepository OK
|
||||
|- Settings/SettingsRepository.php <- \Domain\Settings\SettingsRepository OK
|
||||
|- User/UserRepository.php <- \Domain\User\UserRepository OK
|
||||
|- Pages/PagesRepository.php <- \Domain\Pages\PagesRepository OK
|
||||
|- Layouts/LayoutsRepository.php <- \Domain\Layouts\LayoutsRepository OK
|
||||
`- Articles/ArticlesRepository.php <- \Domain\Articles\ArticlesRepository OK (w toku)
|
||||
├── 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 ✓
|
||||
├── Layouts/LayoutsRepository.php ← \Domain\Layouts\LayoutsRepository ✓
|
||||
├── 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 ✓
|
||||
```
|
||||
|
||||
Nastepne: `Domain\Banners`, `Domain\Authors`, `Domain\Newsletter`, ...
|
||||
Następne: `Admin\` namespace (Fazy 6–13), `Frontend\` namespace (Fazy 14–16).
|
||||
---
|
||||
|
||||
## Katalogi
|
||||
|
||||
11
download.php
11
download.php
@@ -1,15 +1,6 @@
|
||||
<?php
|
||||
error_reporting( E_ALL & ~E_NOTICE & ~E_WARNING );
|
||||
function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\' , $classname );
|
||||
$c = array_pop( $q );
|
||||
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
||||
|
||||
if ( file_exists( $f ) )
|
||||
require_once( $f );
|
||||
}
|
||||
spl_autoload_register( '__autoload_my_classes' );
|
||||
require_once __DIR__ . '/autoload/autoloader.php';
|
||||
date_default_timezone_set( 'Europe/Warsaw' );
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
15
index.php
15
index.php
@@ -1,19 +1,6 @@
|
||||
<?php
|
||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||
function __autoload_my_classes( $classname )
|
||||
{
|
||||
$q = explode( '\\', $classname );
|
||||
$c = array_pop( $q );
|
||||
|
||||
// 1. Legacy: class.ClassName.php
|
||||
$f = 'autoload/' . implode( '/', $q ) . '/class.' . $c . '.php';
|
||||
if ( file_exists( $f ) ) { require_once( $f ); return; }
|
||||
|
||||
// 2. PSR-4: ClassName.php
|
||||
$f = 'autoload/' . implode( '/', $q ) . '/' . $c . '.php';
|
||||
if ( file_exists( $f ) ) require_once( $f );
|
||||
}
|
||||
spl_autoload_register( '__autoload_my_classes' );
|
||||
require_once __DIR__ . '/autoload/autoloader.php';
|
||||
date_default_timezone_set( 'Europe/Warsaw' );
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
BIN
updates/1.60/ver_1.693.zip
Normal file
BIN
updates/1.60/ver_1.693.zip
Normal file
Binary file not shown.
28
updates/1.60/ver_1.693_manifest.json
Normal file
28
updates/1.60/ver_1.693_manifest.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"changelog": "REF - migracja admin Pages/Layouts/Articles do Domain repositories",
|
||||
"version": "1.693",
|
||||
"files": {
|
||||
"added": [
|
||||
"autoload/Domain/Articles/ArticlesRepository.php",
|
||||
"autoload/Domain/Layouts/LayoutsRepository.php",
|
||||
"autoload/Domain/Pages/PagesRepository.php",
|
||||
"composer.phar"
|
||||
],
|
||||
"deleted": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"autoload/admin/factory/class.Articles.php",
|
||||
"autoload/admin/factory/class.Layouts.php",
|
||||
"autoload/admin/factory/class.Pages.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:3994561d9f3df8ed887f53c903b2a26ae6d17e6b10d98c7cb5cdc59132cef7b5",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-03-04",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
BIN
updates/1.60/ver_1.694.zip
Normal file
BIN
updates/1.60/ver_1.694.zip
Normal file
Binary file not shown.
33
updates/1.60/ver_1.694_manifest.json
Normal file
33
updates/1.60/ver_1.694_manifest.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"changelog": "NEW - centralny autoloader, Shared\\Email, Shared\\Security\\CsrfToken",
|
||||
"version": "1.694",
|
||||
"files": {
|
||||
"added": [
|
||||
".mcp.json",
|
||||
"autoload/Shared/Email/Email.php",
|
||||
"autoload/Shared/Security/CsrfToken.php",
|
||||
"autoload/autoloader.php"
|
||||
],
|
||||
"deleted": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"admin/ajax.php",
|
||||
"admin/index.php",
|
||||
"ajax.php",
|
||||
"api.php",
|
||||
"autoload/Shared/Helpers/Helpers.php",
|
||||
"cron.php",
|
||||
"download.php",
|
||||
"index.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:a21dc4a768bc7c9e71b8a319ff0e6a16bdd894330a5145d0278b220a3ccc4027",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-04-04",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
BIN
updates/1.60/ver_1.695.zip
Normal file
BIN
updates/1.60/ver_1.695.zip
Normal file
Binary file not shown.
33
updates/1.60/ver_1.695_manifest.json
Normal file
33
updates/1.60/ver_1.695_manifest.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"changelog": "NEW - aktualizacja konfiguracji Claude, Serena i CLAUDE.md",
|
||||
"version": "1.695",
|
||||
"files": {
|
||||
"added": [
|
||||
"autoload/Domain/Authors/AuthorsRepository.php",
|
||||
"autoload/Domain/Banners/BannersRepository.php",
|
||||
"autoload/Domain/Newsletter/NewsletterRepository.php",
|
||||
"autoload/Domain/Scontainers/ScontainersRepository.php"
|
||||
],
|
||||
"deleted": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"autoload/admin/factory/class.Authors.php",
|
||||
"autoload/admin/factory/class.Banners.php",
|
||||
"autoload/admin/factory/class.Newsletter.php",
|
||||
"autoload/admin/factory/class.Scontainers.php",
|
||||
"autoload/front/factory/class.Authors.php",
|
||||
"autoload/front/factory/class.Banners.php",
|
||||
"autoload/front/factory/class.Newsletter.php",
|
||||
"autoload/front/factory/class.Scontainers.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:f8b50187c72ced5d00937c74939a4e4300bc6e40c074639d7a9ff8662e4cddd0",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-04-04",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
@@ -11,9 +11,9 @@ $mdb = new medoo( [
|
||||
'charset' => 'utf8'
|
||||
] );
|
||||
|
||||
$current_ver = 1692; // aktualizowane automatycznie przez build-update.ps1
|
||||
$current_ver = 1696; // aktualizowane automatycznie przez build-update.ps1
|
||||
|
||||
// 1. Skan filesystem — lista istniejących ZIPów
|
||||
// 1. Skan filesystem Ä‚ËÂĂ˂¬ĂË€ť lista istniejĂ„ĂË€¦cych ZIPĂłw
|
||||
$versions = [];
|
||||
for ( $i = 1; $i <= $current_ver; $i++ )
|
||||
{
|
||||
@@ -34,7 +34,7 @@ $license = $mdb->get( 'pp_update_licenses', '*', [ 'key' => ( $_GET['key'] ?? ''
|
||||
if ( !$license )
|
||||
die();
|
||||
|
||||
// 3. Sprawdź ważność daty
|
||||
// 3. SprawdĹş waĹĽnoÄąĂË€şĂ„ĂË€ˇ daty
|
||||
if ( $license['valid_to_date'] && $license['valid_to_date'] < date( 'Y-m-d' ) )
|
||||
die();
|
||||
|
||||
@@ -53,11 +53,11 @@ foreach ( $versions as $ver )
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Filtruj wersje wg kanału (beta widzi beta+stable, reszta tylko stable)
|
||||
// 5. Filtruj wersje wg kanaÄąĂË€šu (beta widzi beta+stable, reszta tylko stable)
|
||||
$channels = $license['beta'] ? [ 'beta', 'stable' ] : [ 'stable' ];
|
||||
$allowed = array_flip( $mdb->select( 'pp_update_versions', 'version', [ 'channel' => $channels ] ) ?: [] );
|
||||
|
||||
// 6. Wypisz dostępne wersje
|
||||
// 6. Wypisz dostĂ„ĂË„ËÂpne wersje
|
||||
$valid_to_version = $license['valid_to_version'];
|
||||
foreach ( $versions as $ver )
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user