Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c73d69664f | |||
| f7c7c0bb88 | |||
| bf4b7c6429 | |||
| cfd2e5fb57 | |||
| 8f6d084b4d | |||
| 47abff2550 | |||
| ffe661b4d2 | |||
| 73ff0ca5b6 | |||
| 7949e9b6a3 | |||
| 3325eaf44c | |||
| 9b31ce0d16 | |||
| 964bfa877c | |||
| 36fa3fdeae | |||
| 645037d144 | |||
| b8ab53a6f3 | |||
| dd31c062ad | |||
| 869f25d6db | |||
| b41fa58488 | |||
| 1b4c6fe66a | |||
| 320710fd02 | |||
| 11d720aa25 | |||
| 08bd6d23c9 | |||
| 28de4e88b7 | |||
| 0c1e916ed6 | |||
| 1bebdff3ac | |||
| 5e6c3e46fc | |||
| ff227fa6e0 | |||
| 2e715e803e | |||
| 8e6b29976c |
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
.phpunit.result.cache
Normal file
1
.phpunit.result.cache
Normal file
@@ -0,0 +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.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: {}
|
||||
|
||||
@@ -48,6 +48,15 @@ backups/
|
||||
cache/
|
||||
cron/
|
||||
|
||||
# Moduł zarządzania releaseami (tylko serwer dewelopera)
|
||||
autoload/admin/controls/class.Releases.php
|
||||
autoload/admin/factory/class.Releases.php
|
||||
autoload/admin/view/class.Releases.php
|
||||
admin/templates/releases/
|
||||
|
||||
# Menu dewelopera
|
||||
admin/templates/additional-menu.php
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.serena/
|
||||
|
||||
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';
|
||||
|
||||
11
admin/templates/additional-menu.php
Normal file
11
admin/templates/additional-menu.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
// Menu tylko na serwerze dewelopera — wykluczone z .updateignore
|
||||
?>
|
||||
<div class="title">Developer</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/admin/releases/main_view/">
|
||||
<img src="/admin/css/icons/settings-20-filled.svg">Releases & Licencje
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
312
admin/templates/releases/main-view.php
Normal file
312
admin/templates/releases/main-view.php
Normal file
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
global $gdb;
|
||||
ob_start();
|
||||
?>
|
||||
<style>
|
||||
.releases-tabs-nav { margin-bottom: 0; border-bottom: 1px solid #ddd; }
|
||||
.releases-tabs-nav li { display: inline-block; margin-bottom: -1px; }
|
||||
.releases-tabs-nav li a {
|
||||
display: block; padding: 8px 16px; text-decoration: none; color: #555;
|
||||
border: 1px solid transparent; border-radius: 3px 3px 0 0;
|
||||
background: #f5f5f5; margin-right: 2px; cursor: pointer;
|
||||
}
|
||||
.releases-tabs-nav li.active a {
|
||||
color: #333; background: #fff;
|
||||
border-color: #ddd #ddd #fff;
|
||||
}
|
||||
.releases-tab-pane { display: none; padding: 18px 0 0; }
|
||||
.releases-tab-pane.active { display: block; }
|
||||
.license-form-wrap { display: none; margin-bottom: 20px; }
|
||||
</style>
|
||||
|
||||
<ul class="releases-tabs-nav" id="releases-tabs-nav">
|
||||
<li class="active"><a href="#" data-tab="tab-versions">Wersje</a></li>
|
||||
<li><a href="#" data-tab="tab-licenses">Licencje</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- TAB: Wersje -->
|
||||
<div class="releases-tab-pane active" id="tab-versions">
|
||||
<div style="margin-bottom: 12px;">
|
||||
<form method="post" action="/admin/releases/discover_versions/" style="display:inline"
|
||||
onsubmit="return confirm('Wykryć wersje z dysku i zarejestrować jako stable?')">
|
||||
<button type="submit" class="btn btn-info btn-sm">
|
||||
<i class="fa fa-search"></i> Wykryj wersje z dysku
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<table class="table table-bordered table-striped table-hover table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Wersja</th>
|
||||
<th class="text-center" style="width:100px;">Kanał</th>
|
||||
<th style="width:150px;">Data dodania</th>
|
||||
<th style="width:150px;">Data promocji</th>
|
||||
<th class="text-center" style="width:60px;">ZIP</th>
|
||||
<th class="text-center" style="width:140px;">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($this->versions)): ?>
|
||||
<tr><td colspan="6" class="text-center text-muted">Brak wersji w bazie.</td></tr>
|
||||
<?php else: foreach ($this->versions as $v): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($v['version']) ?></strong></td>
|
||||
<td class="text-center">
|
||||
<?php if ($v['channel'] === 'stable'): ?>
|
||||
<span class="label label-success">stable</span>
|
||||
<?php else: ?>
|
||||
<span class="label label-warning">beta</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($v['created_at'] ?? '') ?></td>
|
||||
<td><?= $v['promoted_at'] ? htmlspecialchars($v['promoted_at']) : '<span class="text-muted">—</span>' ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($v['zip_exists']): ?>
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
<?php else: ?>
|
||||
<span class="text-danger"><i class="fa fa-times"></i></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php if ($v['channel'] === 'beta'): ?>
|
||||
<form method="post" action="/admin/releases/promote/" style="display:inline">
|
||||
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
||||
<button type="submit" class="btn btn-success btn-xs"
|
||||
onclick="return confirm('Promować <?= htmlspecialchars($v['version'], ENT_QUOTES) ?> do stable?')">
|
||||
Promuj →stable
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<form method="post" action="/admin/releases/demote/" style="display:inline">
|
||||
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
||||
<button type="submit" class="btn btn-warning btn-xs"
|
||||
onclick="return confirm('Cofnąć <?= htmlspecialchars($v['version'], ENT_QUOTES) ?> do beta?')">
|
||||
Cofnij →beta
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Licencje -->
|
||||
<div class="releases-tab-pane" id="tab-licenses">
|
||||
|
||||
<div style="margin-bottom: 12px;">
|
||||
<a href="#" class="btn btn-success btn-sm" id="btn-add-license">
|
||||
<i class="fa fa-plus-circle"></i> Dodaj licencję
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Formularz dodawania / edycji -->
|
||||
<div class="license-form-wrap panel panel-default" id="license-form-wrap">
|
||||
<div class="panel-heading">
|
||||
<strong id="license-form-title">Nowa licencja</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form method="post" action="/admin/releases/save_license/" id="license-form">
|
||||
<input type="hidden" name="id" id="lic-id" value="">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label>Domena</label>
|
||||
<input type="text" name="domain" id="lic-domain" class="form-control" placeholder="np. example.com" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label>Klucz licencji</label>
|
||||
<input type="text" name="key" id="lic-key" class="form-control" placeholder="Klucz UUID / losowy ciąg (pusty = domyślny)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group">
|
||||
<label>Ważna do daty</label>
|
||||
<input type="date" name="valid_to_date" id="lic-valid-date" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group">
|
||||
<label>Ważna do wersji</label>
|
||||
<input type="text" name="valid_to_version" id="lic-valid-ver" class="form-control" placeholder="np. 1.700">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group">
|
||||
<label>Dostęp beta</label>
|
||||
<select name="beta" id="lic-beta" class="form-control">
|
||||
<option value="0">Nie</option>
|
||||
<option value="1">Tak</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Notatka</label>
|
||||
<input type="text" name="note" id="lic-note" class="form-control" placeholder="Opcjonalna notatka">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-system btn-sm">
|
||||
<i class="fa fa-save"></i> Zapisz licencję
|
||||
</button>
|
||||
<a href="#" class="btn btn-default btn-sm" id="btn-cancel-license">Anuluj</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabela licencji -->
|
||||
<table class="table table-bordered table-striped table-hover table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domena</th>
|
||||
<th style="width:120px;">Klucz</th>
|
||||
<th style="width:120px;">Do daty</th>
|
||||
<th style="width:100px;">Do wersji</th>
|
||||
<th class="text-center" style="width:70px;">Beta</th>
|
||||
<th>Notatka</th>
|
||||
<th class="text-center" style="width:60px;">Edytuj</th>
|
||||
<th class="text-center" style="width:60px;">Usuń</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($this->licenses)): ?>
|
||||
<tr><td colspan="8" class="text-center text-muted">Brak licencji w bazie.</td></tr>
|
||||
<?php else: foreach ($this->licenses as $lic): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($lic['domain']) ?></td>
|
||||
<td>
|
||||
<code title="<?= htmlspecialchars($lic['key']) ?>">
|
||||
<?= htmlspecialchars(substr($lic['key'], 0, 8)) ?>…
|
||||
</code>
|
||||
</td>
|
||||
<td><?= $lic['valid_to_date'] ? htmlspecialchars($lic['valid_to_date']) : '<span class="text-muted">—</span>' ?></td>
|
||||
<td><?= $lic['valid_to_version'] ? htmlspecialchars($lic['valid_to_version']) : '<span class="text-muted">—</span>' ?></td>
|
||||
<td class="text-center">
|
||||
<form method="post" action="/admin/releases/toggle_beta/" style="display:inline">
|
||||
<input type="hidden" name="id" value="<?= (int)$lic['id'] ?>">
|
||||
<button type="submit"
|
||||
class="label <?= $lic['beta'] ? 'label-info' : 'label-default' ?>"
|
||||
title="Kliknij, aby przełączyć"
|
||||
style="cursor:pointer;border:none;background:none">
|
||||
<?= $lic['beta'] ? 'tak' : 'nie' ?>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($lic['note'] ?? '') ?></td>
|
||||
<td class="text-center">
|
||||
<a href="#"
|
||||
class="btn btn-default btn-xs btn-edit-license"
|
||||
data-id="<?= (int)$lic['id'] ?>"
|
||||
data-domain="<?= htmlspecialchars($lic['domain'], ENT_QUOTES) ?>"
|
||||
data-key="<?= htmlspecialchars($lic['key'], ENT_QUOTES) ?>"
|
||||
data-valid-date="<?= htmlspecialchars($lic['valid_to_date'] ?? '', ENT_QUOTES) ?>"
|
||||
data-valid-ver="<?= htmlspecialchars($lic['valid_to_version'] ?? '', ENT_QUOTES) ?>"
|
||||
data-beta="<?= (int)$lic['beta'] ?>"
|
||||
data-note="<?= htmlspecialchars($lic['note'] ?? '', ENT_QUOTES) ?>">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<form method="post" action="/admin/releases/delete_license/" style="display:inline"
|
||||
onsubmit="return confirm('Usunąć licencję dla <?= htmlspecialchars($lic['domain'], ENT_QUOTES) ?>?')">
|
||||
<input type="hidden" name="id" value="<?= (int)$lic['id'] ?>">
|
||||
<button type="submit" class="btn btn-danger btn-xs">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// Tab switching
|
||||
document.querySelectorAll('#releases-tabs-nav a[data-tab]').forEach(function (link) {
|
||||
link.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
var targetId = this.getAttribute('data-tab');
|
||||
document.querySelectorAll('#releases-tabs-nav li').forEach(function (li) {
|
||||
li.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.releases-tab-pane').forEach(function (pane) {
|
||||
pane.classList.remove('active');
|
||||
});
|
||||
this.parentElement.classList.add('active');
|
||||
document.getElementById(targetId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Show add-license form
|
||||
document.getElementById('btn-add-license').addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
resetLicenseForm();
|
||||
document.getElementById('license-form-title').textContent = 'Nowa licencja';
|
||||
var wrap = document.getElementById('license-form-wrap');
|
||||
if (wrap.style.display === 'none' || wrap.style.display === '') {
|
||||
wrap.style.display = 'block';
|
||||
$(wrap).slideDown(200);
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel
|
||||
document.getElementById('btn-cancel-license').addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
$(document.getElementById('license-form-wrap')).slideUp(200);
|
||||
});
|
||||
|
||||
// Edit buttons
|
||||
document.querySelectorAll('.btn-edit-license').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
var d = this.dataset;
|
||||
document.getElementById('lic-id').value = d.id;
|
||||
document.getElementById('lic-domain').value = d.domain;
|
||||
document.getElementById('lic-key').value = d.key;
|
||||
document.getElementById('lic-valid-date').value = d.validDate;
|
||||
document.getElementById('lic-valid-ver').value = d.validVer;
|
||||
document.getElementById('lic-beta').value = d.beta;
|
||||
document.getElementById('lic-note').value = d.note;
|
||||
document.getElementById('license-form-title').textContent = 'Edytuj licencję: ' + d.domain;
|
||||
var wrap = document.getElementById('license-form-wrap');
|
||||
wrap.style.display = 'block';
|
||||
$(wrap).slideDown(200);
|
||||
wrap.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
});
|
||||
|
||||
function resetLicenseForm() {
|
||||
document.getElementById('lic-id').value = '';
|
||||
document.getElementById('lic-domain').value = '';
|
||||
document.getElementById('lic-key').value = '';
|
||||
document.getElementById('lic-valid-date').value = '';
|
||||
document.getElementById('lic-valid-ver').value = '';
|
||||
document.getElementById('lic-beta').value = '0';
|
||||
document.getElementById('lic-note').value = '';
|
||||
}
|
||||
|
||||
// If URL hash indicates licenses tab, switch to it on load
|
||||
if (window.location.hash === '#licenses') {
|
||||
document.querySelector('[data-tab="tab-licenses"]').click();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
$out = ob_get_clean();
|
||||
|
||||
$grid = new \gridEdit;
|
||||
$grid->id = 'releases-view';
|
||||
$grid->gdb_opt = $gdb;
|
||||
$grid->include_plugins = true;
|
||||
$grid->title = 'Releases & Licencje';
|
||||
$grid->default_buttons = false;
|
||||
$grid->form = false;
|
||||
$grid->external_code = $out;
|
||||
echo $grid->draw();
|
||||
?>
|
||||
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';
|
||||
|
||||
648
autoload/Domain/Articles/ArticlesRepository.php
Normal file
648
autoload/Domain/Articles/ArticlesRepository.php
Normal file
@@ -0,0 +1,648 @@
|
||||
<?php
|
||||
namespace Domain\Articles;
|
||||
|
||||
class ArticlesRepository
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct( $db )
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
public function filesOrderSave( $articleId, $order ): void
|
||||
{
|
||||
$i = 0;
|
||||
$order = explode( ';', $order );
|
||||
|
||||
if ( is_array( $order ) && !empty( $order ) )
|
||||
foreach ( $order as $fileId )
|
||||
$this->db->update( 'pp_articles_files', [ 'o' => (int)$i++ ], [
|
||||
'AND' => [ 'article_id' => $articleId, 'id' => $fileId ]
|
||||
] );
|
||||
}
|
||||
|
||||
public function galleryOrderSave( $articleId, $order ): void
|
||||
{
|
||||
$i = 0;
|
||||
$order = explode( ';', $order );
|
||||
|
||||
if ( is_array( $order ) && !empty( $order ) )
|
||||
foreach ( $order as $imageId )
|
||||
$this->db->update( 'pp_articles_images', [ 'o' => $i++ ], [
|
||||
'AND' => [ 'article_id' => $articleId, 'id' => $imageId ]
|
||||
] );
|
||||
}
|
||||
|
||||
public function additionalParams( $language = 0 )
|
||||
{
|
||||
return $this->db->select( 'pp_articles_additional_params', '*', [ 'AND' => [ 'status' => 1, 'language' => $language ] ] );
|
||||
}
|
||||
|
||||
public function imageAltChange( $imageId, $imageAlt )
|
||||
{
|
||||
$result = $this->db->update( 'pp_articles_images', [ 'alt' => $imageAlt ], [ 'id' => $imageId ] );
|
||||
\S::delete_cache();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function articleUrl( $articleId )
|
||||
{
|
||||
$results = $this->db->query(
|
||||
"SELECT seo_link FROM pp_articles_langs AS pal, pp_langs AS pl WHERE lang_id = pl.id AND article_id = " . (int)$articleId . " AND seo_link != '' ORDER BY o ASC LIMIT 1"
|
||||
)->fetchAll();
|
||||
|
||||
if ( !$results[0]['seo_link'] )
|
||||
{
|
||||
$title = $this->articleTitle( $articleId );
|
||||
return 'a-' . $articleId . '-' . \S::seo( $title );
|
||||
}
|
||||
|
||||
return $results[0]['seo_link'];
|
||||
}
|
||||
|
||||
public function articlePages( $articleId )
|
||||
{
|
||||
$pagesRepo = new \Domain\Pages\PagesRepository( $this->db );
|
||||
$results = $this->db->query( "SELECT page_id FROM pp_articles_pages WHERE article_id = " . (int)$articleId )->fetchAll();
|
||||
|
||||
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
if ( $out == '' )
|
||||
$out .= ' - ';
|
||||
|
||||
$out .= $pagesRepo->pageTitle( $row['page_id'] );
|
||||
|
||||
if ( end( $results ) != $row )
|
||||
$out .= ' / ';
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function articleTitle( $articleId )
|
||||
{
|
||||
$results = $this->db->query(
|
||||
"SELECT title FROM pp_articles_langs AS pal, pp_langs AS pl WHERE lang_id = pl.id AND article_id = " . (int)$articleId . " AND title != '' ORDER BY o ASC LIMIT 1"
|
||||
)->fetchAll();
|
||||
|
||||
return $results[0]['title'];
|
||||
}
|
||||
|
||||
public function deleteFile( $fileId ): bool
|
||||
{
|
||||
$this->db->update( 'pp_articles_files', [ 'to_delete' => 1 ], [ 'id' => (int)$fileId ] );
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteImg( $imageId ): bool
|
||||
{
|
||||
$this->db->update( 'pp_articles_images', [ 'to_delete' => 1 ], [ 'id' => (int)$imageId ] );
|
||||
return true;
|
||||
}
|
||||
|
||||
public function articleDetails( $articleId )
|
||||
{
|
||||
if ( $article = $this->db->get( 'pp_articles', '*', [ 'id' => (int)$articleId ] ) )
|
||||
{
|
||||
$results = $this->db->select( 'pp_articles_langs', '*', [ 'article_id' => (int)$articleId ] );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
$article['languages'][ $row['lang_id'] ] = $row;
|
||||
|
||||
$article['images'] = $this->db->select( 'pp_articles_images', '*', [ 'article_id' => (int)$articleId, 'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ] ] );
|
||||
$article['files'] = $this->db->select( 'pp_articles_files', '*', [ 'article_id' => (int)$articleId, 'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ] ] );
|
||||
$article['pages'] = $this->db->select( 'pp_articles_pages', 'page_id', [ 'article_id' => (int)$articleId ] );
|
||||
$article['tags'] = $this->db->select( 'pp_tags', [ '[><]pp_articles_tags' => [ 'id' => 'tag_id' ] ], 'name', [ 'article_id' => (int)$articleId ] );
|
||||
$article['params'] = $this->db->select( 'pp_articles_additional_values', [ 'param_id', 'value', 'language_id' ], [ 'article_id' => (int)$articleId ] );
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
public function insertMissingHash(): bool
|
||||
{
|
||||
if ( $this->db->count( 'pp_articles', [ 'hash' => null ] ) )
|
||||
{
|
||||
$rows = $this->db->select( 'pp_articles', [ 'id', 'date_add' ], [ 'hash' => null ] );
|
||||
if ( is_array( $rows ) )
|
||||
foreach ( $rows as $row )
|
||||
$this->db->update( 'pp_articles', [ 'hash' => md5( $row['id'] . $row['date_add'] ) ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function articlesByDateAdd( $dateStart, $dateEnd )
|
||||
{
|
||||
$results = $this->db->query(
|
||||
'SELECT id FROM pp_articles WHERE status = 1 AND date_add BETWEEN \'' . $dateStart . '\' AND \'' . $dateEnd . '\' ORDER BY date_add DESC'
|
||||
)->fetchAll();
|
||||
|
||||
if ( is_array( $results ) && !empty( $results ) )
|
||||
foreach ( $results as $row )
|
||||
$articles[] = \front\factory\Articles::article_details( $row['id'], 'pl' );
|
||||
|
||||
return isset( $articles ) ? $articles : null;
|
||||
}
|
||||
|
||||
public function articlesSetArchive( $articleId )
|
||||
{
|
||||
$result = $this->db->update( 'pp_articles', [ 'status' => -1 ], [ 'id' => (int)$articleId ] );
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function fileNameChange( $fileId, $fileName ): bool
|
||||
{
|
||||
$this->db->update( 'pp_articles_files', [ 'name' => $fileName ], [ 'id' => (int)$fileId ] );
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteNonassignedFiles(): void
|
||||
{
|
||||
$results = $this->db->select( 'pp_articles_files', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
if ( file_exists( '../' . $row['src'] ) )
|
||||
unlink( '../' . $row['src'] );
|
||||
|
||||
$this->db->delete( 'pp_articles_files', [ 'article_id' => null ] );
|
||||
}
|
||||
|
||||
public function deleteNonassignedImages(): void
|
||||
{
|
||||
$results = $this->db->select( 'pp_articles_images', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
if ( file_exists( '../' . $row['src'] ) )
|
||||
unlink( '../' . $row['src'] );
|
||||
|
||||
$this->db->delete( 'pp_articles_images', [ 'article_id' => null ] );
|
||||
}
|
||||
|
||||
public function duplicateArticle( $articleId, $userId ): bool
|
||||
{
|
||||
$article = $this->articleDetails( $articleId );
|
||||
|
||||
if ( !$article )
|
||||
return false;
|
||||
|
||||
$this->db->insert( 'pp_articles', [
|
||||
'show_title' => $article['show_title'],
|
||||
'show_date_add' => $article['show_date_add'],
|
||||
'show_date_modify' => $article['show_date_modify'],
|
||||
'date_add' => date( 'Y-m-d H:i:s' ),
|
||||
'date_modify' => date( 'Y-m-d H:i:s' ),
|
||||
'modify_by' => $userId,
|
||||
'layout_id' => $article['layout_id'],
|
||||
'status' => $article['status'],
|
||||
'repeat_entry' => $article['repeat_entry'],
|
||||
'social_icons' => $article['social_icons'],
|
||||
'date_start' => $article['date_start'],
|
||||
'date_end' => $article['event_date'],
|
||||
'priority' => $article['priority'],
|
||||
'password' => $article['password'],
|
||||
'pixieset' => $article['pixieset']
|
||||
] );
|
||||
|
||||
$articleTmpId = $this->db->id();
|
||||
if ( !$articleTmpId )
|
||||
return false;
|
||||
|
||||
foreach ( $article['languages'] as $key => $val )
|
||||
$this->db->insert( 'pp_articles_langs', [
|
||||
'article_id' => $articleTmpId,
|
||||
'lang_id' => $key,
|
||||
'title' => 'Kopia: ' . $val['title'],
|
||||
'entry' => $val['entry'],
|
||||
'text' => $val['text'],
|
||||
'meta_title' => null,
|
||||
'meta_description' => null,
|
||||
'meta_keywords' => null,
|
||||
'seo_link' => null,
|
||||
'copy_from' => $val['copy_from'],
|
||||
'block_direct_access' => $val['block_direct_access']
|
||||
] );
|
||||
|
||||
foreach ( $article['params'] as $param )
|
||||
$this->db->insert( 'pp_articles_additional_values', [
|
||||
'param_id' => $param['param_id'],
|
||||
'value' => $param['value'],
|
||||
'article_id' => $articleTmpId,
|
||||
'language_id' => $param['language_id']
|
||||
] );
|
||||
|
||||
foreach ( $article['pages'] as $page )
|
||||
{
|
||||
$order = $this->maxOrder() + 1;
|
||||
$this->db->insert( 'pp_articles_pages', [
|
||||
'article_id' => $articleTmpId,
|
||||
'page_id' => $page,
|
||||
'o' => (int)$order
|
||||
] );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function articleSave(
|
||||
$articleId, $title, $mainImage, $entry, $text, $tableOfContents, $status, $showTitle, $showTableOfContents, $showDateAdd, $dateAdd, $showDateModify, $dateModify, $seoLink, $metaTitle, $metaDescription,
|
||||
$metaKeywords, $layoutId, $pages, $noindex, $repeatEntry, $copyFrom, $socialIcons, $eventDate, $tags, $blockDirectAccess, $priority, $password, $pixieset, $idAuthor, $params, $userId
|
||||
)
|
||||
{
|
||||
$eventDate = explode( ' - ', $eventDate );
|
||||
|
||||
if ( !$articleId )
|
||||
{
|
||||
$this->db->insert( 'pp_articles', [
|
||||
'show_title' => $showTitle == 'on' ? 1 : 0,
|
||||
'show_table_of_contents' => $showTableOfContents == 'on' ? 1 : 0,
|
||||
'show_date_add' => $showDateAdd == 'on' ? 1 : 0,
|
||||
'show_date_modify' => $showDateModify == 'on' ? 1 : 0,
|
||||
'date_add' => date( 'Y-m-d H:i:s' ),
|
||||
'date_modify' => date( 'Y-m-d H:i:s' ),
|
||||
'modify_by' => $userId,
|
||||
'layout_id' => $layoutId ? (int)$layoutId : null,
|
||||
'status' => $status == 'on' ? 1 : 0,
|
||||
'repeat_entry' => $repeatEntry == 'on' ? 1 : 0,
|
||||
'social_icons' => $socialIcons == 'on' ? 1 : 0,
|
||||
'date_start' => $eventDate[0] ? $eventDate[0] : null,
|
||||
'date_end' => $eventDate[1] ? $eventDate[1] : null,
|
||||
'priority' => $priority == 'on' ? 1 : 0,
|
||||
'password' => $password ? $password : null,
|
||||
'pixieset' => $pixieset,
|
||||
'id_author' => $idAuthor ? $idAuthor : null
|
||||
] );
|
||||
|
||||
$id = $this->db->id();
|
||||
if ( !$id )
|
||||
return false;
|
||||
|
||||
$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_articles_langs', [
|
||||
'article_id' => (int)$id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title[ $i ] != '' ? $title[ $i ] : null,
|
||||
'main_image' => $mainImage[$i] != '' ? $mainImage[$i] : null,
|
||||
'entry' => $entry[ $i ] != '' ? $entry[ $i ] : null,
|
||||
'text' => $text[ $i ] != '' ? $text[ $i ] : null,
|
||||
'table_of_contents' => $tableOfContents[$i] != '' ? $tableOfContents[$i] : null,
|
||||
'meta_title' => $metaTitle[ $i ] != '' ? $metaTitle[ $i ] : null,
|
||||
'meta_description' => $metaDescription[ $i ] != '' ? $metaDescription[ $i ] : null,
|
||||
'meta_keywords' => $metaKeywords[ $i ] != '' ? $metaKeywords[ $i ] : null,
|
||||
'seo_link' => \S::seo( $seoLink[ $i ] ) != '' ? \S::seo( $seoLink[ $i ] ) : null,
|
||||
'noindex' => $noindex[ $i ],
|
||||
'copy_from' => $copyFrom[ $i ] != '' ? $copyFrom[ $i ] : null,
|
||||
'block_direct_access' => $blockDirectAccess[ $i ]
|
||||
] );
|
||||
$i++;
|
||||
}
|
||||
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
|
||||
{
|
||||
$this->db->insert( 'pp_articles_langs', [
|
||||
'article_id' => (int)$id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title != '' ? $title : null,
|
||||
'main_image' => $mainImage != '' ? $mainImage : null,
|
||||
'entry' => $entry != '' ? $entry : null,
|
||||
'text' => $text != '' ? $text : null,
|
||||
'table_of_contents' => $tableOfContents != '' ? $tableOfContents : null,
|
||||
'meta_title' => $metaTitle != '' ? $metaTitle : null,
|
||||
'meta_description' => $metaDescription != '' ? $metaDescription : null,
|
||||
'meta_keywords' => $metaKeywords != '' ? $metaKeywords : null,
|
||||
'seo_link' => \S::seo( $seoLink ) != '' ? \S::seo( $seoLink ) : null,
|
||||
'noindex' => $noindex,
|
||||
'copy_from' => $copyFrom != '' ? $copyFrom : null,
|
||||
'block_direct_access' => $blockDirectAccess
|
||||
] );
|
||||
}
|
||||
|
||||
$results = $this->db->select( 'pp_articles_additional_params', '*', [ 'AND' => [ 'status' => 1, 'language' => 0 ] ] );
|
||||
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$this->db->insert( 'pp_articles_additional_values', [
|
||||
'param_id' => $row['id'],
|
||||
'value' => $params[ 'ap_' . $row['name'] ],
|
||||
'article_id' => (int)$id,
|
||||
'language_id' => null
|
||||
] );
|
||||
}
|
||||
|
||||
if ( is_array( $pages ) ) foreach ( $pages as $page )
|
||||
{
|
||||
$order = $this->maxOrder() + 1;
|
||||
$this->db->insert( 'pp_articles_pages', [
|
||||
'article_id' => (int)$id,
|
||||
'page_id' => (int)$page,
|
||||
'o' => (int)$order
|
||||
] );
|
||||
}
|
||||
else if ( $pages )
|
||||
{
|
||||
$order = $this->maxOrder() + 1;
|
||||
$this->db->insert( 'pp_articles_pages', [
|
||||
'article_id' => (int)$id,
|
||||
'page_id' => (int)$pages,
|
||||
'o' => (int)$order
|
||||
] );
|
||||
}
|
||||
|
||||
$results = $this->db->select( 'pp_articles_files', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$dir = '/upload/article_files/article_' . $id;
|
||||
$new_file_name = str_replace( '/upload/article_files/tmp', $dir, $row['src'] );
|
||||
|
||||
if ( file_exists( '..' . $row['src'] ) )
|
||||
{
|
||||
if ( !is_dir( '../' . $dir ) and $created !== true )
|
||||
{
|
||||
if ( mkdir( '../' . $dir, 0755, true ) )
|
||||
$created = true;
|
||||
}
|
||||
rename( '..' . $row['src'], '..' . $new_file_name );
|
||||
}
|
||||
|
||||
$this->db->update( 'pp_articles_files', [ 'src' => $new_file_name, 'article_id' => $id ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
|
||||
$created = false;
|
||||
|
||||
$results = $this->db->select( 'pp_articles_images', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$dir = '/upload/article_images/article_' . $id;
|
||||
$new_file_name = str_replace( '/upload/article_images/tmp', $dir, $row['src'] );
|
||||
|
||||
if ( file_exists( '../' . $new_file_name ) )
|
||||
{
|
||||
$ext = strrpos( $new_file_name, '.' );
|
||||
$fileName_a = substr( $new_file_name, 0, $ext );
|
||||
$fileName_b = substr( $new_file_name, $ext );
|
||||
$count = 1;
|
||||
|
||||
while ( file_exists( '../' . $fileName_a . '_' . $count . $fileName_b ) )
|
||||
$count++;
|
||||
|
||||
$new_file_name = $fileName_a . '_' . $count . $fileName_b;
|
||||
}
|
||||
|
||||
if ( file_exists( '..' . $row['src'] ) )
|
||||
{
|
||||
if ( !is_dir( '../' . $dir ) and $created !== true )
|
||||
{
|
||||
if ( mkdir( '../' . $dir, 0755, true ) )
|
||||
$created = true;
|
||||
}
|
||||
rename( '..' . $row['src'], '..' . $new_file_name );
|
||||
}
|
||||
|
||||
$this->db->update( 'pp_articles_images', [ 'src' => $new_file_name, 'article_id' => (int)$id ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
|
||||
$tags = explode( ',', $tags );
|
||||
if ( is_array( $tags ) ) foreach ( $tags as $tag )
|
||||
{
|
||||
if ( trim( $tag ) != '' )
|
||||
{
|
||||
$tag_id = $this->db->get( 'pp_tags', 'id', [ 'name' => $tag ] );
|
||||
if ( !$tag_id )
|
||||
{
|
||||
$this->db->insert( 'pp_tags', [ 'name' => $tag ] );
|
||||
$tag_id = $this->db->id();
|
||||
}
|
||||
|
||||
$this->db->insert( 'pp_articles_tags', [ 'article_id' => (int)$id, 'tag_id' => (int)$tag_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
|
||||
return $id;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->db->update( 'pp_articles', [
|
||||
'show_title' => $showTitle == 'on' ? 1 : 0,
|
||||
'show_table_of_contents' => $showTableOfContents == 'on' ? 1 : 0,
|
||||
'show_date_add' => $showDateAdd == 'on' ? 1 : 0,
|
||||
'date_add' => $dateAdd,
|
||||
'show_date_modify' => $showDateModify == 'on' ? 1 : 0,
|
||||
'date_modify' => $dateModify ? $dateModify : date( 'Y-m-d H:i:s' ),
|
||||
'modify_by' => $userId,
|
||||
'layout_id' => $layoutId ? (int)$layoutId : null,
|
||||
'status' => $status == 'on' ? 1 : 0,
|
||||
'repeat_entry' => $repeatEntry == 'on' ? 1 : 0,
|
||||
'social_icons' => $socialIcons == 'on' ? 1 : 0,
|
||||
'date_start' => $eventDate[0] ? $eventDate[0] : null,
|
||||
'date_end' => $eventDate[1] ? $eventDate[1] : null,
|
||||
'priority' => $priority == 'on' ? 1 : 0,
|
||||
'password' => $password ? $password : null,
|
||||
'pixieset' => $pixieset,
|
||||
'id_author' => $idAuthor ? $idAuthor : null
|
||||
], [
|
||||
'id' => (int)$articleId
|
||||
] );
|
||||
|
||||
if ( $dateAdd )
|
||||
$this->db->update( 'pp_articles', [ 'date_add' => $dateAdd ], [ 'id' => (int)$articleId ] );
|
||||
|
||||
$i = 0;
|
||||
$this->db->delete( 'pp_articles_langs', [ 'article_id' => (int)$articleId ] );
|
||||
|
||||
$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_articles_langs', [
|
||||
'article_id' => (int)$articleId,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title[ $i ] != '' ? $title[ $i ] : null,
|
||||
'main_image' => $mainImage[$i] != '' ? $mainImage[$i] : null,
|
||||
'entry' => $entry[ $i ] != '' ? $entry[ $i ] : null,
|
||||
'text' => $text[ $i ] != '' ? $text[ $i ] : null,
|
||||
'table_of_contents' => $tableOfContents[$i] != '' ? $tableOfContents[$i] : null,
|
||||
'meta_title' => $metaTitle[ $i ] != '' ? $metaTitle[ $i ] : null,
|
||||
'meta_description' => $metaDescription[ $i ] != '' ? $metaDescription[ $i ] : null,
|
||||
'meta_keywords' => $metaKeywords[ $i ] != '' ? $metaKeywords[ $i ] : null,
|
||||
'seo_link' => \S::seo( $seoLink[ $i ] ) != '' ? \S::seo( $seoLink[ $i ] ) : null,
|
||||
'noindex' => $noindex[ $i ],
|
||||
'copy_from' => $copyFrom[ $i ] != '' ? $copyFrom[ $i ] : null,
|
||||
'block_direct_access' => $blockDirectAccess[ $i ]
|
||||
] );
|
||||
$i++;
|
||||
}
|
||||
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
|
||||
{
|
||||
$this->db->insert( 'pp_articles_langs', [
|
||||
'article_id' => (int)$articleId,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title != '' ? $title : null,
|
||||
'main_image' => $mainImage != '' ? $mainImage : null,
|
||||
'entry' => $entry != '' ? $entry : null,
|
||||
'text' => $text != '' ? $text : null,
|
||||
'table_of_contents' => $tableOfContents != '' ? $tableOfContents : null,
|
||||
'meta_title' => $metaTitle != '' ? $metaTitle : null,
|
||||
'meta_description' => $metaDescription != '' ? $metaDescription : null,
|
||||
'meta_keywords' => $metaKeywords != '' ? $metaKeywords : null,
|
||||
'seo_link' => \S::seo( $seoLink ) != '' ? \S::seo( $seoLink ) : null,
|
||||
'noindex' => $noindex,
|
||||
'copy_from' => $copyFrom != '' ? $copyFrom : null,
|
||||
'block_direct_access' => $blockDirectAccess
|
||||
] );
|
||||
}
|
||||
|
||||
$this->db->delete( 'pp_articles_additional_values', [ 'article_id' => (int)$articleId ] );
|
||||
|
||||
$results = $this->db->select( 'pp_articles_additional_params', '*', [ 'AND' => [ 'status' => 1, 'language' => 0 ] ] );
|
||||
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$this->db->insert( 'pp_articles_additional_values', [
|
||||
'param_id' => $row['id'],
|
||||
'value' => $params[ 'ap_' . $row['name'] ],
|
||||
'article_id' => (int)$articleId,
|
||||
'language_id' => null
|
||||
] );
|
||||
}
|
||||
|
||||
$results = $this->db->select( 'pp_articles_additional_params', '*', [ 'AND' => [ 'status' => 1, 'language' => 1 ] ] );
|
||||
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$results2 = $this->db->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||
if ( is_array( $results2 ) ) foreach ( $results2 as $row2 )
|
||||
{
|
||||
$this->db->insert( 'pp_articles_additional_values', [
|
||||
'param_id' => $row['id'],
|
||||
'value' => $params[ 'ap_' . $row['name'] . '_' . $row2['id'] ],
|
||||
'article_id' => (int)$articleId,
|
||||
'language_id' => $row2['id']
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
$not_in = [ 0 ];
|
||||
if ( is_array( $pages ) ) foreach ( $pages as $page )
|
||||
$not_in[] = $page;
|
||||
else if ( $pages )
|
||||
$not_in[] = $pages;
|
||||
|
||||
$this->db->delete( 'pp_articles_pages', [ 'AND' => [ 'article_id' => (int)$articleId, 'page_id[!]' => $not_in ] ] );
|
||||
|
||||
$pages_tmp = $this->db->select( 'pp_articles_pages', 'page_id', [ 'article_id' => (int)$articleId ] );
|
||||
if ( !is_array( $pages ) )
|
||||
$pages = [ $pages ];
|
||||
|
||||
$pages = array_diff( $pages, $pages_tmp );
|
||||
if ( is_array( $pages ) ) foreach ( $pages as $page )
|
||||
{
|
||||
$order = $this->maxOrder() + 1;
|
||||
$this->db->insert( 'pp_articles_pages', [
|
||||
'article_id' => (int)$articleId,
|
||||
'page_id' => (int)$page,
|
||||
'o' => (int)$order
|
||||
] );
|
||||
}
|
||||
|
||||
$results = $this->db->select( 'pp_articles_files', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$dir = '/upload/article_files/article_' . $articleId;
|
||||
$new_file_name = str_replace( '/upload/article_files/tmp', $dir, $row['src'] );
|
||||
|
||||
if ( file_exists( '..' . $row['src'] ) )
|
||||
{
|
||||
if ( !is_dir( '../' . $dir ) and $created !== true )
|
||||
{
|
||||
if ( mkdir( '../' . $dir, 0755, true ) )
|
||||
$created = true;
|
||||
}
|
||||
rename( '..' . $row['src'], '..' . $new_file_name );
|
||||
}
|
||||
|
||||
$this->db->update( 'pp_articles_files', [ 'src' => $new_file_name, 'article_id' => (int)$articleId ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
|
||||
$created = false;
|
||||
|
||||
$results = $this->db->select( 'pp_articles_images', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$dir = '/upload/article_images/article_' . $articleId;
|
||||
$new_file_name = str_replace( '/upload/article_images/tmp', $dir, $row['src'] );
|
||||
|
||||
if ( file_exists( '../' . $new_file_name ) )
|
||||
{
|
||||
$ext = strrpos( $new_file_name, '.' );
|
||||
$fileName_a = substr( $new_file_name, 0, $ext );
|
||||
$fileName_b = substr( $new_file_name, $ext );
|
||||
$count = 1;
|
||||
|
||||
while ( file_exists( '../' . $fileName_a . '_' . $count . $fileName_b ) )
|
||||
$count++;
|
||||
|
||||
$new_file_name = $fileName_a . '_' . $count . $fileName_b;
|
||||
}
|
||||
|
||||
if ( file_exists( '..' . $row['src'] ) )
|
||||
{
|
||||
if ( !is_dir( '../' . $dir ) and $created !== true )
|
||||
{
|
||||
if ( mkdir( '../' . $dir, 0755, true ) )
|
||||
$created = true;
|
||||
}
|
||||
rename( '..' . $row['src'], '..' . $new_file_name );
|
||||
}
|
||||
|
||||
$this->db->update( 'pp_articles_images', [ 'src' => $new_file_name, 'article_id' => (int)$articleId ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
|
||||
$results = $this->db->select( 'pp_articles_images', '*', [ 'AND' => [ 'article_id' => (int)$articleId, 'to_delete' => 1 ] ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
if ( file_exists( '../' . $row['src'] ) )
|
||||
unlink( '../' . $row['src'] );
|
||||
|
||||
$this->db->delete( 'pp_articles_images', [ 'AND' => [ 'article_id' => (int)$articleId, 'to_delete' => 1 ] ] );
|
||||
|
||||
$results = $this->db->select( 'pp_articles_files', '*', [ 'AND' => [ 'article_id' => (int)$articleId, 'to_delete' => 1 ] ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
if ( file_exists( '../' . $row['src'] ) )
|
||||
unlink( '../' . $row['src'] );
|
||||
|
||||
$this->db->delete( 'pp_articles_files', [ 'AND' => [ 'article_id' => (int)$articleId, 'to_delete' => 1 ] ] );
|
||||
|
||||
$this->db->delete( 'pp_articles_tags', [ 'article_id' => (int)$articleId ] );
|
||||
|
||||
$tags = explode( ',', $tags );
|
||||
if ( is_array( $tags ) ) foreach ( $tags as $tag )
|
||||
{
|
||||
if ( trim( $tag ) != '' )
|
||||
{
|
||||
$tag_id = $this->db->get( 'pp_tags', 'id', [ 'name' => $tag ] );
|
||||
if ( !$tag_id )
|
||||
{
|
||||
$this->db->insert( 'pp_tags', [ 'name' => $tag ] );
|
||||
$tag_id = $this->db->id();
|
||||
}
|
||||
|
||||
$this->db->insert( 'pp_articles_tags', [ 'article_id' => (int)$articleId, 'tag_id' => (int)$tag_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
return $articleId;
|
||||
}
|
||||
}
|
||||
|
||||
public function maxOrder()
|
||||
{
|
||||
return $this->db->max( 'pp_articles_pages', 'o' );
|
||||
}
|
||||
}
|
||||
?>
|
||||
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 ] );
|
||||
}
|
||||
}
|
||||
123
autoload/Domain/Layouts/LayoutsRepository.php
Normal file
123
autoload/Domain/Layouts/LayoutsRepository.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
namespace Domain\Layouts;
|
||||
|
||||
class LayoutsRepository
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct( $db )
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
public function layoutDelete( $layoutId )
|
||||
{
|
||||
if ( $this->db->count( 'pp_layouts' ) > 1 )
|
||||
return $this->db->delete( 'pp_layouts', [ 'id' => (int)$layoutId ] );
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function layoutDetails( $layoutId )
|
||||
{
|
||||
$layout = $this->db->get( 'pp_layouts', '*', [ 'id' => (int)$layoutId ] );
|
||||
$layout['pages'] = $this->db->select( 'pp_layouts_pages', 'page_id', [ 'layout_id' => (int)$layoutId ] );
|
||||
|
||||
return $layout;
|
||||
}
|
||||
|
||||
public function layoutSave( $layoutId, $name, $status, $pages, $html, $css, $js, $mHtml, $mCss, $mJs )
|
||||
{
|
||||
if ( !$layoutId )
|
||||
return $this->createLayout( $name, $status, $pages, $html, $css, $js, $mHtml, $mCss, $mJs );
|
||||
|
||||
return $this->updateLayout( $layoutId, $name, $status, $pages, $html, $css, $js, $mHtml, $mCss, $mJs );
|
||||
}
|
||||
|
||||
public function menusList()
|
||||
{
|
||||
$pagesRepo = new \Domain\Pages\PagesRepository( $this->db );
|
||||
$results = $this->db->select( 'pp_menus', 'id', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$menu = $pagesRepo->menuDetails( $row );
|
||||
$menu['pages'] = $pagesRepo->menuPages( $row );
|
||||
$menus[] = $menu;
|
||||
}
|
||||
|
||||
return isset( $menus ) ? $menus : null;
|
||||
}
|
||||
|
||||
public function layoutsList()
|
||||
{
|
||||
return $this->db->select( 'pp_layouts', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
|
||||
}
|
||||
|
||||
private function createLayout( $name, $status, $pages, $html, $css, $js, $mHtml, $mCss, $mJs )
|
||||
{
|
||||
if ( $status == 'on' )
|
||||
$this->db->update( 'pp_layouts', [ 'status' => 0 ] );
|
||||
|
||||
$this->db->insert( 'pp_layouts', [
|
||||
'name' => $name,
|
||||
'html' => $html,
|
||||
'css' => $css,
|
||||
'js' => $js,
|
||||
'm_html' => $mHtml,
|
||||
'm_css' => $mCss,
|
||||
'm_js' => $mJs,
|
||||
'status' => $status == 'on' ? 1 : 0
|
||||
] );
|
||||
|
||||
$id = $this->db->id();
|
||||
if ( !$id )
|
||||
return false;
|
||||
|
||||
$this->replaceLayoutPages( (int)$id, $pages );
|
||||
\S::delete_cache();
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function updateLayout( $layoutId, $name, $status, $pages, $html, $css, $js, $mHtml, $mCss, $mJs )
|
||||
{
|
||||
if ( $status == 'on' )
|
||||
$this->db->update( 'pp_layouts', [ 'status' => 0 ] );
|
||||
|
||||
$this->db->update( 'pp_layouts', [
|
||||
'name' => $name,
|
||||
'html' => $html,
|
||||
'css' => $css,
|
||||
'js' => $js,
|
||||
'm_html' => $mHtml,
|
||||
'm_css' => $mCss,
|
||||
'm_js' => $mJs,
|
||||
'status' => $status == 'on' ? 1 : 0
|
||||
], [
|
||||
'id' => $layoutId
|
||||
] );
|
||||
|
||||
$this->db->delete( 'pp_layouts_pages', [ 'layout_id' => (int)$layoutId ] );
|
||||
$this->replaceLayoutPages( (int)$layoutId, $pages );
|
||||
|
||||
\S::delete_cache();
|
||||
return $layoutId;
|
||||
}
|
||||
|
||||
private function replaceLayoutPages( int $layoutId, $pages ): void
|
||||
{
|
||||
if ( is_array( $pages ) )
|
||||
foreach ( $pages as $page )
|
||||
{
|
||||
$this->db->delete( 'pp_layouts_pages', [ 'page_id' => (int)$page ] );
|
||||
$this->db->insert( 'pp_layouts_pages', [ 'layout_id' => $layoutId, 'page_id' => (int)$page ] );
|
||||
}
|
||||
else if ( $pages )
|
||||
{
|
||||
$this->db->delete( 'pp_layouts_pages', [ 'page_id' => (int)$pages ] );
|
||||
$this->db->insert( 'pp_layouts_pages', [ 'layout_id' => $layoutId, 'page_id' => (int)$pages ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
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;
|
||||
}
|
||||
}
|
||||
451
autoload/Domain/Pages/PagesRepository.php
Normal file
451
autoload/Domain/Pages/PagesRepository.php
Normal file
@@ -0,0 +1,451 @@
|
||||
<?php
|
||||
namespace Domain\Pages;
|
||||
|
||||
class PagesRepository
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct( $db )
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
public function saveArticlesOrder( $pageId, $articles ): bool
|
||||
{
|
||||
if ( is_array( $articles ) )
|
||||
{
|
||||
$this->db->update( 'pp_articles_pages', [ 'o' => 0 ], [ 'page_id' => (int) $pageId ] );
|
||||
$x = 0;
|
||||
|
||||
for ( $i = 0; $i < count( $articles ); $i++ )
|
||||
{
|
||||
if ( $articles[$i]['item_id'] )
|
||||
{
|
||||
$x++;
|
||||
$this->db->update( 'pp_articles_pages', [ 'o' => $x ], [
|
||||
'AND' => [ 'page_id' => (int) $pageId, 'article_id' => $articles[$i]['item_id'] ]
|
||||
] );
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function pageArticles( $pageId )
|
||||
{
|
||||
$results = $this->db->query(
|
||||
'SELECT article_id, o, status FROM pp_articles_pages AS ap INNER JOIN pp_articles AS a ON a.id = ap.article_id WHERE page_id = ' . (int) $pageId . ' AND status != -1 ORDER BY o ASC'
|
||||
)->fetchAll();
|
||||
|
||||
$articles = [];
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$row['title'] = \admin\factory\Articles::article_title( $row['article_id'] );
|
||||
$articles[] = $row;
|
||||
}
|
||||
|
||||
return $articles;
|
||||
}
|
||||
|
||||
public function menusList()
|
||||
{
|
||||
return $this->db->select( 'pp_menus', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
|
||||
}
|
||||
|
||||
public function savePagesOrder( $menuId, $pages ): bool
|
||||
{
|
||||
if ( is_array( $pages ) )
|
||||
{
|
||||
$this->db->update( 'pp_pages', [ 'o' => 0 ], [ 'menu_id' => (int) $menuId ] );
|
||||
$x = 0;
|
||||
|
||||
for ( $i = 0; $i < count( $pages ); $i++ )
|
||||
{
|
||||
if ( $pages[$i]['item_id'] )
|
||||
{
|
||||
$parentId = $pages[$i]['parent_id'] ? $pages[$i]['parent_id'] : 0;
|
||||
|
||||
if ( $pages[$i]['item_id'] && $pages[$i]['depth'] > 1 )
|
||||
{
|
||||
if ( $pages[$i]['depth'] == 2 )
|
||||
$parentId = null;
|
||||
|
||||
$x++;
|
||||
$this->db->update( 'pp_pages', [ 'o' => $x, 'parent_id' => $parentId ], [ 'id' => (int) $pages[$i]['item_id'] ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\S::delete_cache();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function pageDelete( $pageId ): bool
|
||||
{
|
||||
if ( $this->db->count( 'pp_pages', [ 'parent_id' => (int) $pageId ] ) )
|
||||
return false;
|
||||
|
||||
if ( $this->db->delete( 'pp_pages', [ 'id' => (int) $pageId ] ) )
|
||||
{
|
||||
\S::delete_cache();
|
||||
\S::htacces();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function maxOrder(): int
|
||||
{
|
||||
return (int) $this->db->max( 'pp_pages', 'o' );
|
||||
}
|
||||
|
||||
public function updateSubpagesMenuId( int $parentId, int $menuId ): void
|
||||
{
|
||||
$this->updateSubpagesMenuIdRecursive( $parentId, $menuId );
|
||||
}
|
||||
|
||||
public function generateSeoLink( $title, $pageId, $articleId, $lang, $pid )
|
||||
{
|
||||
$seoLink = \S::seo( $title );
|
||||
$seoLinkCheck = false;
|
||||
$i = 0;
|
||||
|
||||
while ( !$seoLinkCheck )
|
||||
{
|
||||
if ( $this->db->count( 'pp_pages_langs', [ 'AND' => [ 'seo_link' => $seoLink, 'page_id[!]' => (int) $pageId ] ] ) )
|
||||
$seoLink = $seoLink . '-' . ( ++$i );
|
||||
else
|
||||
$seoLinkCheck = true;
|
||||
}
|
||||
|
||||
$seoLinkCheck = false;
|
||||
|
||||
while ( !$seoLinkCheck )
|
||||
{
|
||||
if ( $this->db->count( 'pp_articles_langs', [ 'AND' => [ 'seo_link' => $seoLink, 'article_id[!]' => (int) $articleId ] ] ) )
|
||||
$seoLink = $seoLink . '-' . ( ++$i );
|
||||
else
|
||||
$seoLinkCheck = true;
|
||||
}
|
||||
|
||||
return $seoLink;
|
||||
}
|
||||
|
||||
public function googleUrlPreview( $pageId, $title, $lang, $pid, $id, $seoLink, $languageLink = '' )
|
||||
{
|
||||
$prefix = $languageLink;
|
||||
$status = true;
|
||||
$idPage = $pageId;
|
||||
$seo = '';
|
||||
|
||||
do
|
||||
{
|
||||
if ( $pageId )
|
||||
{
|
||||
$parent = $this->pageDetails( $pageId );
|
||||
$parentId = $parent['parent_id'];
|
||||
}
|
||||
else
|
||||
$parentId = $pid;
|
||||
|
||||
if ( $parentId )
|
||||
{
|
||||
$results = $this->db->query(
|
||||
"SELECT title, seo_link, page_id FROM pp_pages_langs AS ppl, pp_langs AS pl WHERE lang_id = pl.id AND page_id = " . (int) $parentId . " AND ppl.lang_id = '" . $lang . "' "
|
||||
)->fetchAll();
|
||||
if ( $results[0]['seo_link'] )
|
||||
$seo = $results[0]['seo_link'] . '/' . $seo;
|
||||
else
|
||||
$seo = 's-' . $results[0]['page_id'] . '-' . \S::seo( $results[0]['title'] ) . '/' . $seo;
|
||||
$pageId = $results[0]['page_id'];
|
||||
}
|
||||
else
|
||||
$status = false;
|
||||
}
|
||||
while ( $status );
|
||||
|
||||
if ( $id )
|
||||
{
|
||||
if ( !$seoLink )
|
||||
$seo = $seo . 's-' . $id . '-' . \S::seo( $title );
|
||||
else
|
||||
$seo = $seo . $seoLink;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( !$seoLink )
|
||||
$seo = $seo . 's-' . $idPage . '-' . \S::seo( $title );
|
||||
else
|
||||
$seo = $seo . $seoLink;
|
||||
}
|
||||
|
||||
if ( $prefix )
|
||||
$seo = $prefix . $seo;
|
||||
|
||||
return $seo;
|
||||
}
|
||||
|
||||
public function menuDelete( $menuId )
|
||||
{
|
||||
if ( $this->db->count( 'pp_pages', [ 'menu_id' => (int) $menuId ] ) )
|
||||
return false;
|
||||
|
||||
return $this->db->delete( 'pp_menus', [ 'id' => (int) $menuId ] );
|
||||
}
|
||||
|
||||
public function menuDetails( $menuId )
|
||||
{
|
||||
return $this->db->get( 'pp_menus', '*', [ 'id' => (int) $menuId ] );
|
||||
}
|
||||
|
||||
public function menuSave( $menuId, $name, $status )
|
||||
{
|
||||
$status == 'on' ? $status = 1 : $status = 0;
|
||||
|
||||
if ( !$menuId )
|
||||
{
|
||||
return $this->db->insert( 'pp_menus', [ 'name' => $name, 'status' => $status ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->db->update( 'pp_menus', [ 'name' => $name, 'status' => $status ], [ 'id' => (int) $menuId ] );
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function menuLists()
|
||||
{
|
||||
return $this->db->select( 'pp_menus', '*', [ 'ORDER' => [ 'id' => 'ASC' ] ] );
|
||||
}
|
||||
|
||||
public function pageDetails( $pageId )
|
||||
{
|
||||
$page = $this->db->get( 'pp_pages', '*', [ 'id' => (int) $pageId ] );
|
||||
|
||||
$results = $this->db->select( 'pp_pages_langs', '*', [ 'page_id' => (int) $pageId ] );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
$page['languages'][$row['lang_id']] = $row;
|
||||
|
||||
$page['layout_id'] = $this->db->get( 'pp_layouts_pages', 'layout_id', [ 'page_id' => (int) $pageId ] );
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
public function pageUrl( $pageId )
|
||||
{
|
||||
$results = $this->db->query(
|
||||
"SELECT seo_link, title lang_id FROM pp_pages_langs AS ppl, pp_langs AS pl WHERE lang_id = pl.id AND page_id = " . (int) $pageId . " AND seo_link != '' ORDER BY o ASC LIMIT 1"
|
||||
)->fetchAll();
|
||||
|
||||
if ( !$results[0]['seo_link'] )
|
||||
{
|
||||
$title = $this->pageTitle( $article_id );
|
||||
return 's-' . $pageId . '-' . \S::seo( $title );
|
||||
}
|
||||
else
|
||||
return $results[0]['seo_link'];
|
||||
}
|
||||
|
||||
public function pageTitle( $pageId )
|
||||
{
|
||||
$result = $this->db->select( 'pp_pages_langs', [ '[><]pp_langs' => [ 'lang_id' => 'id' ] ], 'title', [
|
||||
'AND' => [ 'page_id' => (int) $pageId, 'title[!]' => '' ], 'ORDER' => [ 'o' => 'ASC' ], 'LIMIT' => 1
|
||||
] );
|
||||
return $result[0];
|
||||
}
|
||||
|
||||
public function pageLanguages( $pageId )
|
||||
{
|
||||
return $this->db->select( 'pp_pages_langs', '*', [ 'AND' => [ 'page_id' => (int) $pageId, 'title[!]' => null ] ] );
|
||||
}
|
||||
|
||||
public function menuPages( $menuId, $parentId = null )
|
||||
{
|
||||
$results = $this->db->select( 'pp_pages', [ 'id', 'menu_id', 'status', 'parent_id', 'start' ], [
|
||||
'AND' => [ 'menu_id' => $menuId, 'parent_id' => $parentId ], 'ORDER' => [ 'o' => 'ASC' ]
|
||||
] );
|
||||
|
||||
if ( !is_array( $results ) || !count( $results ) )
|
||||
return null;
|
||||
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$row['title'] = $this->pageTitle( $row['id'] );
|
||||
$row['languages'] = $this->pageLanguages( $row['id'] );
|
||||
$row['subpages'] = $this->menuPages( $menuId, $row['id'] );
|
||||
|
||||
$pages[] = $row;
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
public function pageSave(
|
||||
$pageId, $title, $seoLink, $metaTitle, $metaDescription, $metaKeywords, $menuId, $parentId, $pageType, $sortType, $layoutId, $articlesLimit, $showTitle, $status, $link, $noindex, $start,
|
||||
$siteTitle, $blockDirectAccess, $cache, $canonical
|
||||
)
|
||||
{
|
||||
$parentId = $parentId ? $parentId : null;
|
||||
|
||||
if ( !$pageId )
|
||||
return $this->createPage(
|
||||
$title, $seoLink, $metaTitle, $metaDescription, $metaKeywords, $menuId, $parentId, $pageType, $sortType, $layoutId, $articlesLimit, $showTitle, $status, $link, $noindex, $start,
|
||||
$siteTitle, $blockDirectAccess, $cache, $canonical
|
||||
);
|
||||
|
||||
return $this->updatePage(
|
||||
$pageId, $title, $seoLink, $metaTitle, $metaDescription, $metaKeywords, $menuId, $parentId, $pageType, $sortType, $layoutId, $articlesLimit, $showTitle, $status, $link, $noindex, $start,
|
||||
$siteTitle, $blockDirectAccess, $cache, $canonical
|
||||
);
|
||||
}
|
||||
|
||||
private function createPage(
|
||||
$title, $seoLink, $metaTitle, $metaDescription, $metaKeywords, $menuId, $parentId, $pageType, $sortType, $layoutId, $articlesLimit, $showTitle, $status, $link, $noindex, $start, $siteTitle,
|
||||
$blockDirectAccess, $cache, $canonical
|
||||
)
|
||||
{
|
||||
$order = $this->maxOrder() + 1;
|
||||
|
||||
$this->db->insert( 'pp_pages', [
|
||||
'menu_id' => (int) $menuId,
|
||||
'page_type' => $pageType,
|
||||
'sort_type' => $sortType,
|
||||
'articles_limit' => $articlesLimit,
|
||||
'show_title' => $showTitle == 'on' ? 1 : 0,
|
||||
'status' => $status == 'on' ? 1 : 0,
|
||||
'o' => (int) $order,
|
||||
'parent_id' => $parentId,
|
||||
'start' => $start == 'on' ? 1 : 0,
|
||||
'cache' => $cache == 'on' ? 1 : 0,
|
||||
] );
|
||||
|
||||
$id = $this->db->id();
|
||||
if ( !$id )
|
||||
return false;
|
||||
|
||||
if ( $start )
|
||||
$this->db->update( 'pp_pages', [ 'start' => 0 ], [ 'id[!]' => (int) $id ] );
|
||||
|
||||
if ( $layoutId )
|
||||
$this->db->insert( 'pp_layouts_pages', [ 'page_id' => (int) $id, 'layout_id' => (int) $layoutId ] );
|
||||
|
||||
$languages = $this->activeLanguages();
|
||||
$this->savePageLanguages( (int) $id, $languages, $title, $metaDescription, $metaKeywords, $metaTitle, $seoLink, $noindex, $siteTitle, $link, $blockDirectAccess, $canonical );
|
||||
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function updatePage(
|
||||
$pageId, $title, $seoLink, $metaTitle, $metaDescription, $metaKeywords, $menuId, $parentId, $pageType, $sortType, $layoutId, $articlesLimit, $showTitle, $status, $link, $noindex, $start,
|
||||
$siteTitle, $blockDirectAccess, $cache, $canonical
|
||||
)
|
||||
{
|
||||
$this->db->update( 'pp_pages', [
|
||||
'menu_id' => (int) $menuId,
|
||||
'page_type' => $pageType,
|
||||
'sort_type' => $sortType,
|
||||
'articles_limit' => $articlesLimit,
|
||||
'show_title' => $showTitle == 'on' ? 1 : 0,
|
||||
'status' => $status == 'on' ? 1 : 0,
|
||||
'parent_id' => $parentId,
|
||||
'start' => $start == 'on' ? 1 : 0,
|
||||
'cache' => $cache == 'on' ? 1 : 0,
|
||||
], [
|
||||
'id' => (int) $pageId
|
||||
] );
|
||||
|
||||
if ( $layoutId )
|
||||
{
|
||||
$this->db->delete( 'pp_layouts_pages', [ 'page_id' => (int) $pageId ] );
|
||||
$this->db->insert( 'pp_layouts_pages', [ 'layout_id' => (int) $layoutId, 'page_id' => (int) $pageId ] );
|
||||
}
|
||||
|
||||
if ( $start )
|
||||
$this->db->update( 'pp_pages', [ 'start' => 0 ], [ 'id[!]' => (int) $pageId ] );
|
||||
|
||||
$this->db->delete( 'pp_pages_langs', [ 'page_id' => (int) $pageId ] );
|
||||
|
||||
$languages = $this->activeLanguages();
|
||||
$this->savePageLanguages( (int) $pageId, $languages, $title, $metaDescription, $metaKeywords, $metaTitle, $seoLink, $noindex, $siteTitle, $link, $blockDirectAccess, $canonical );
|
||||
|
||||
$this->updateSubpagesMenuIdRecursive( (int) $pageId, (int) $menuId );
|
||||
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
|
||||
return $pageId;
|
||||
}
|
||||
|
||||
private function activeLanguages(): array
|
||||
{
|
||||
return $this->db->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ) ?: [];
|
||||
}
|
||||
|
||||
private function savePageLanguages(
|
||||
int $pageId, array $languages, $title, $metaDescription, $metaKeywords, $metaTitle, $seoLink, $noindex, $siteTitle, $link, $blockDirectAccess, $canonical
|
||||
): void
|
||||
{
|
||||
$isMulti = count( $languages ) > 1;
|
||||
|
||||
foreach ( $languages as $i => $row )
|
||||
{
|
||||
$titleValue = $this->languageValue( $title, $i, $isMulti );
|
||||
$metaDescriptionValue = $this->languageValue( $metaDescription, $i, $isMulti );
|
||||
$metaKeywordsValue = $this->languageValue( $metaKeywords, $i, $isMulti );
|
||||
$metaTitleValue = $this->languageValue( $metaTitle, $i, $isMulti );
|
||||
$seoLinkValue = $this->languageValue( $seoLink, $i, $isMulti );
|
||||
$noindexValue = $this->languageValue( $noindex, $i, $isMulti );
|
||||
$siteTitleValue = $this->languageValue( $siteTitle, $i, $isMulti );
|
||||
$linkValue = $this->languageValue( $link, $i, $isMulti );
|
||||
$blockDirectAccessValue = $this->languageValue( $blockDirectAccess, $i, $isMulti );
|
||||
$canonicalValue = $this->languageValue( $canonical, $i, $isMulti );
|
||||
|
||||
$seo = \S::seo( $seoLinkValue );
|
||||
|
||||
$this->db->insert( 'pp_pages_langs', [
|
||||
'page_id' => $pageId,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $this->nullIfEmpty( $titleValue ),
|
||||
'meta_description' => $this->nullIfEmpty( $metaDescriptionValue ),
|
||||
'meta_keywords' => $this->nullIfEmpty( $metaKeywordsValue ),
|
||||
'meta_title' => $this->nullIfEmpty( $metaTitleValue ),
|
||||
'seo_link' => $seo != '' ? $seo : null,
|
||||
'noindex' => $noindexValue,
|
||||
'site_title' => $this->nullIfEmpty( $siteTitleValue ),
|
||||
'link' => $this->nullIfEmpty( $linkValue ),
|
||||
'block_direct_access' => $blockDirectAccessValue,
|
||||
'canonical' => $this->nullIfEmpty( $canonicalValue )
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
private function languageValue( $value, int $index, bool $isMulti )
|
||||
{
|
||||
if ( $isMulti )
|
||||
return is_array( $value ) ? ( $value[$index] ?? null ) : null;
|
||||
|
||||
return is_array( $value ) ? ( $value[0] ?? null ) : $value;
|
||||
}
|
||||
|
||||
private function nullIfEmpty( $value )
|
||||
{
|
||||
return $value != '' ? $value : null;
|
||||
}
|
||||
|
||||
private function updateSubpagesMenuIdRecursive( int $parentId, int $menuId ): void
|
||||
{
|
||||
$this->db->update( 'pp_pages', [ 'menu_id' => $menuId ], [ 'parent_id' => $parentId ] );
|
||||
|
||||
$results = $this->db->select( 'pp_pages', [ 'id' ], [ 'parent_id' => $parentId ] );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
$this->updateSubpagesMenuIdRecursive( (int) $row['id'], $menuId );
|
||||
}
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,13 @@ class SettingsRepository
|
||||
{
|
||||
if ( !$settings = \Shared\Cache\CacheHandler::fetch( 'settings_details' ) )
|
||||
{
|
||||
$results = $this->db->select( 'pp_settings', '*' );
|
||||
$settings = [];
|
||||
$results = $this->db->select( 'pp_settings', '*' );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
$settings[ $row['param'] ] = $row['value'];
|
||||
|
||||
\Shared\Cache\CacheHandler::store( 'settings_details', $settings ?? [] );
|
||||
\Shared\Cache\CacheHandler::store( 'settings_details', $settings );
|
||||
}
|
||||
|
||||
return $settings ?? [];
|
||||
|
||||
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] );
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,11 @@ class Tpl
|
||||
$this->vars[ $name ] = $value;
|
||||
}
|
||||
|
||||
public function __isset( $name )
|
||||
{
|
||||
return isset( $this->vars[ $name ] );
|
||||
}
|
||||
|
||||
public function __get( $name )
|
||||
{
|
||||
return $this->vars[ $name ];
|
||||
|
||||
62
autoload/admin/controls/class.Releases.php
Normal file
62
autoload/admin/controls/class.Releases.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
namespace admin\controls;
|
||||
|
||||
class Releases
|
||||
{
|
||||
public static function main_view(): string
|
||||
{
|
||||
return \admin\view\Releases::main_view();
|
||||
}
|
||||
|
||||
public static function promote(): void
|
||||
{
|
||||
$version = trim(\S::get('version'));
|
||||
if ($version)
|
||||
\admin\factory\Releases::promote($version);
|
||||
header('Location: /admin/releases/main_view/');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function demote(): void
|
||||
{
|
||||
$version = trim(\S::get('version'));
|
||||
if ($version)
|
||||
\admin\factory\Releases::demote($version);
|
||||
header('Location: /admin/releases/main_view/');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function discover_versions(): void
|
||||
{
|
||||
$added = \admin\factory\Releases::discover_versions();
|
||||
\S::set_message("Wykryto i dodano {$added} nowych wersji jako stable.");
|
||||
header('Location: /admin/releases/main_view/');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function save_license(): void
|
||||
{
|
||||
\admin\factory\Releases::save_license($_POST);
|
||||
\S::set_message('Licencja została zapisana.');
|
||||
header('Location: /admin/releases/main_view/#licenses');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function delete_license(): void
|
||||
{
|
||||
$id = (int)\S::get('id');
|
||||
if ($id)
|
||||
\admin\factory\Releases::delete_license($id);
|
||||
header('Location: /admin/releases/main_view/#licenses');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function toggle_beta(): void
|
||||
{
|
||||
$id = (int)\S::get('id');
|
||||
if ($id)
|
||||
\admin\factory\Releases::toggle_beta($id);
|
||||
header('Location: /admin/releases/main_view/#licenses');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -1,714 +1,117 @@
|
||||
<?php
|
||||
namespace admin\factory;
|
||||
|
||||
class Articles
|
||||
{
|
||||
public static function duplicate_article( $article_id )
|
||||
private static function repo(): \Domain\Articles\ArticlesRepository
|
||||
{
|
||||
global $mdb, $user;
|
||||
|
||||
$article = \admin\factory\Articles::article_details( $article_id );
|
||||
|
||||
if ( $article )
|
||||
{
|
||||
$mdb -> insert( 'pp_articles', [
|
||||
'show_title' => $article['show_title'],
|
||||
'show_date_add' => $article['show_date_add'],
|
||||
'show_date_modify' => $article['show_date_modify'],
|
||||
'date_add' => date( 'Y-m-d H:i:s' ),
|
||||
'date_modify' => date( 'Y-m-d H:i:s' ),
|
||||
'modify_by' => $user['id'],
|
||||
'layout_id' => $article['layout_id'],
|
||||
'status' => $article['status'],
|
||||
'repeat_entry' => $article['repeat_entry'],
|
||||
'social_icons' => $article['social_icons'],
|
||||
'date_start' => $article['date_start'],
|
||||
'date_end' => $article['event_date'],
|
||||
'priority' => $article['priority'],
|
||||
'password' => $article['password'],
|
||||
'pixieset' => $article['pixieset']
|
||||
] );
|
||||
|
||||
$article_tmp_id = $mdb -> id();
|
||||
|
||||
if ( $article_tmp_id )
|
||||
{
|
||||
foreach ( $article['languages'] as $key => $val )
|
||||
{
|
||||
$mdb -> insert( 'pp_articles_langs', [
|
||||
'article_id' => $article_tmp_id,
|
||||
'lang_id' => $key,
|
||||
'title' => 'Kopia: ' . $val['title'],
|
||||
'entry' => $val['entry'],
|
||||
'text' => $val['text'],
|
||||
'meta_title' => null,
|
||||
'meta_description' => null,
|
||||
'meta_keywords' => null,
|
||||
'seo_link' => null,
|
||||
'copy_from' => $val['copy_from'],
|
||||
'block_direct_access' => $val['block_direct_access']
|
||||
] );
|
||||
}
|
||||
|
||||
foreach ( $article['params'] as $param )
|
||||
{
|
||||
$mdb -> insert( 'pp_articles_additional_values', [
|
||||
'param_id' => $param['param_id'],
|
||||
'value' => $param['value'],
|
||||
'article_id' => $article_tmp_id,
|
||||
'language_id' => $param['language_id']
|
||||
] );
|
||||
}
|
||||
|
||||
foreach ( $article['pages'] as $page )
|
||||
{
|
||||
$order = self::max_order() + 1;
|
||||
$mdb -> insert( 'pp_articles_pages', [
|
||||
'article_id' => $article_tmp_id,
|
||||
'page_id' => $page,
|
||||
'o' => (int)$order
|
||||
] );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
global $mdb;
|
||||
return new \Domain\Articles\ArticlesRepository( $mdb );
|
||||
}
|
||||
|
||||
public static function insert_missing_hash() {
|
||||
global $mdb;
|
||||
public static function duplicate_article( $article_id )
|
||||
{
|
||||
global $user;
|
||||
return self::repo()->duplicateArticle( $article_id, (int)$user['id'] );
|
||||
}
|
||||
|
||||
if ( $mdb -> count( 'pp_articles', [ 'hash' => null ] ) ) {
|
||||
$rows = $mdb -> select( 'pp_articles', [ 'id', 'date_add' ], [ 'hash' => null ] );
|
||||
if ( is_array( $rows ) ) foreach ( $rows as $row ) {
|
||||
$mdb -> update( 'pp_articles', [ 'hash' => md5( $row['id'] . $row['date_add'] ) ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
}
|
||||
return true;
|
||||
public static function insert_missing_hash()
|
||||
{
|
||||
return self::repo()->insertMissingHash();
|
||||
}
|
||||
|
||||
static public function files_order_save( $article_id, $order )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$order = explode( ';', $order );
|
||||
if ( is_array( $order ) and !empty( $order ) ) foreach ( $order as $file_id )
|
||||
{
|
||||
$mdb -> update( 'pp_articles_files', [
|
||||
'o' => (int)$i++
|
||||
], [
|
||||
'AND' => [
|
||||
'article_id' => $article_id,
|
||||
'id' => $file_id
|
||||
]
|
||||
] );
|
||||
}
|
||||
self::repo()->filesOrderSave( $article_id, $order );
|
||||
}
|
||||
|
||||
public static function gallery_order_save( $article_id, $order )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$order = explode( ';', $order );
|
||||
if ( is_array( $order ) and !empty( $order ) ) foreach ( $order as $image_id )
|
||||
{
|
||||
$mdb -> update( 'pp_articles_images', [
|
||||
'o' => $i++
|
||||
], [
|
||||
'AND' => [
|
||||
'article_id' => $article_id,
|
||||
'id' => $image_id
|
||||
]
|
||||
] );
|
||||
}
|
||||
self::repo()->galleryOrderSave( $article_id, $order );
|
||||
}
|
||||
|
||||
public static function additional_params( $language = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> select( 'pp_articles_additional_params', '*', [ 'AND' => [ 'status' => 1, 'language' => $language ] ] );
|
||||
return self::repo()->additionalParams( $language );
|
||||
}
|
||||
|
||||
public static function image_alt_change( $image_id, $image_alt )
|
||||
{
|
||||
global $mdb;
|
||||
$result = $mdb -> update( 'pp_articles_images', [
|
||||
'alt' => $image_alt
|
||||
], [
|
||||
'id' => $image_id
|
||||
] );
|
||||
\S::delete_cache();
|
||||
return $result;
|
||||
return self::repo()->imageAltChange( $image_id, $image_alt );
|
||||
}
|
||||
|
||||
public static function articles_by_date_add( $date_start, $date_end )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$results = $mdb -> query( 'SELECT '
|
||||
. 'id '
|
||||
. 'FROM '
|
||||
. 'pp_articles '
|
||||
. 'WHERE '
|
||||
. 'status = 1 '
|
||||
. 'AND '
|
||||
. 'date_add BETWEEN \'' . $date_start . '\' AND \'' . $date_end . '\' '
|
||||
. 'ORDER BY '
|
||||
. 'date_add DESC' ) -> fetchAll();
|
||||
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
|
||||
$articles[] = \front\factory\Articles::article_details( $row['id'], 'pl' );
|
||||
|
||||
return $articles;
|
||||
return self::repo()->articlesByDateAdd( $date_start, $date_end );
|
||||
}
|
||||
|
||||
public static function article_url( $article_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$results = $mdb -> query( "SELECT seo_link FROM pp_articles_langs AS pal, pp_langs AS pl WHERE lang_id = pl.id AND article_id = " . (int)$article_id . " AND seo_link != '' ORDER BY o ASC LIMIT 1" ) -> fetchAll();
|
||||
if ( !$results[0]['seo_link'] )
|
||||
{
|
||||
$title = self::article_title( $article_id );
|
||||
return 'a-' . $article_id . '-' . \S::seo( $title );
|
||||
}
|
||||
else
|
||||
return $results[0]['seo_link'];
|
||||
return self::repo()->articleUrl( $article_id );
|
||||
}
|
||||
|
||||
public static function article_pages( $article_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$results = $mdb -> query( "SELECT page_id FROM pp_articles_pages WHERE article_id = " . (int)$article_id ) -> fetchAll();
|
||||
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
if ( $out == '' )
|
||||
$out .= ' - ';
|
||||
|
||||
$out .= \admin\factory\Pages::page_title( $row['page_id'] );
|
||||
|
||||
if ( end( $results ) != $row )
|
||||
$out .= ' / ';
|
||||
}
|
||||
|
||||
return $out;
|
||||
return self::repo()->articlePages( $article_id );
|
||||
}
|
||||
|
||||
public static function article_title( $article_id )
|
||||
{
|
||||
global $mdb;
|
||||
$results = $mdb -> query( "SELECT title FROM pp_articles_langs AS pal, pp_langs AS pl WHERE lang_id = pl.id AND article_id = " . (int)$article_id . " AND title != '' ORDER BY o ASC LIMIT 1" ) -> fetchAll();
|
||||
return $results[0]['title'];
|
||||
return self::repo()->articleTitle( $article_id );
|
||||
}
|
||||
|
||||
public static function articles_set_archive( $article_id )
|
||||
{
|
||||
global $mdb;
|
||||
$result = $mdb -> update( 'pp_articles', [ 'status' => -1 ], [ 'id' => (int)$article_id ] );
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
return $result;
|
||||
return self::repo()->articlesSetArchive( $article_id );
|
||||
}
|
||||
|
||||
public static function file_name_change( $file_id, $file_name )
|
||||
{
|
||||
global $mdb;
|
||||
$mdb -> update( 'pp_articles_files', [ 'name' => $file_name ], [ 'id' => (int)$file_id ] );
|
||||
return true;
|
||||
return self::repo()->fileNameChange( $file_id, $file_name );
|
||||
}
|
||||
|
||||
public static function delete_file( $file_id )
|
||||
{
|
||||
global $mdb;
|
||||
$mdb -> update( 'pp_articles_files', [ 'to_delete' => 1 ], [ 'id' => (int)$file_id ] );
|
||||
return true;
|
||||
return self::repo()->deleteFile( $file_id );
|
||||
}
|
||||
|
||||
public static function delete_img( $image_id )
|
||||
{
|
||||
global $mdb;
|
||||
$mdb -> update( 'pp_articles_images', [ 'to_delete' => 1 ], [ 'id' => (int)$image_id ] );
|
||||
return true;
|
||||
return self::repo()->deleteImg( $image_id );
|
||||
}
|
||||
|
||||
public static function article_details( $article_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
if ( $article = $mdb -> get( 'pp_articles', '*', [ 'id' => (int)$article_id ] ) )
|
||||
{
|
||||
$results = $mdb -> select( 'pp_articles_langs', '*', [ 'article_id' => (int)$article_id ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
$article['languages'][ $row['lang_id'] ] = $row;
|
||||
|
||||
$article['images'] = $mdb -> select( 'pp_articles_images', '*', [ 'article_id' => (int)$article_id, 'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ] ] );
|
||||
$article['files'] = $mdb -> select( 'pp_articles_files', '*', [ 'article_id' => (int)$article_id, 'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ] ] );
|
||||
$article['pages'] = $mdb -> select( 'pp_articles_pages', 'page_id', [ 'article_id' => (int)$article_id ] );
|
||||
$article['tags'] = $mdb -> select( 'pp_tags', [ '[><]pp_articles_tags' => [ 'id' => 'tag_id' ] ], 'name', [ 'article_id' => (int)$article_id ] );
|
||||
$article['params'] = $mdb -> select( 'pp_articles_additional_values', [ 'param_id', 'value', 'language_id' ], [ 'article_id' => (int)$article_id ] );
|
||||
}
|
||||
|
||||
return $article;
|
||||
return self::repo()->articleDetails( $article_id );
|
||||
}
|
||||
|
||||
public static function max_order()
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> max( 'pp_articles_pages', 'o' );
|
||||
return self::repo()->maxOrder();
|
||||
}
|
||||
|
||||
public static function article_save(
|
||||
$article_id, $title, $main_image, $entry, $text, $table_of_contents, $status, $show_title, $show_table_of_contents, $show_date_add, $date_add, $show_date_modify, $date_modify, $seo_link, $meta_title, $meta_description,
|
||||
$meta_keywords, $layout_id, $pages, $noindex, $repeat_entry, $copy_from, $social_icons, $event_date, $tags, $block_direct_access, $priority,
|
||||
$password, $pixieset, $id_author, $params )
|
||||
$article_id, $title, $main_image, $entry, $text, $table_of_contents, $status, $show_title, $show_table_of_contents, $show_date_add, $date_add, $show_date_modify, $date_modify, $seo_link, $meta_title, $meta_description,
|
||||
$meta_keywords, $layout_id, $pages, $noindex, $repeat_entry, $copy_from, $social_icons, $event_date, $tags, $block_direct_access, $priority,
|
||||
$password, $pixieset, $id_author, $params
|
||||
)
|
||||
{
|
||||
|
||||
global $mdb, $user;
|
||||
|
||||
$event_date = explode( ' - ', $event_date );
|
||||
|
||||
if ( !$article_id )
|
||||
{
|
||||
$mdb -> insert( 'pp_articles', [
|
||||
'show_title' => $show_title == 'on' ? 1 : 0,
|
||||
'show_table_of_contents' => $show_table_of_contents == 'on' ? 1 : 0,
|
||||
'show_date_add' => $show_date_add == 'on' ? 1 : 0,
|
||||
'show_date_modify' => $show_date_modify == 'on' ? 1 : 0,
|
||||
'date_add' => date( 'Y-m-d H:i:s' ),
|
||||
'date_modify' => date( 'Y-m-d H:i:s' ),
|
||||
'modify_by' => $user['id'],
|
||||
'layout_id' => $layout_id ? (int)$layout_id : null,
|
||||
'status' => $status == 'on' ? 1 : 0,
|
||||
'repeat_entry' => $repeat_entry == 'on' ? 1 : 0,
|
||||
'social_icons' => $social_icons == 'on' ? 1 : 0,
|
||||
'date_start' => $event_date[0] ? $event_date[0] : null,
|
||||
'date_end' => $event_date[1] ? $event_date[1] : null,
|
||||
'priority' => $priority == 'on' ? 1 : 0,
|
||||
'password' => $password ? $password : null,
|
||||
'pixieset' => $pixieset,
|
||||
'id_author' => $id_author ? $id_author : null
|
||||
] );
|
||||
|
||||
$id = $mdb -> id();
|
||||
|
||||
if ( $id )
|
||||
{
|
||||
$i = 0;
|
||||
|
||||
/* tłumaczenia */
|
||||
$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_articles_langs', [
|
||||
'article_id' => (int)$id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title[ $i ] != '' ? $title[ $i ] : null,
|
||||
'main_image' => $main_image[$i] != '' ? $main_image[$i] : null,
|
||||
'entry' => $entry[ $i ] != '' ? $entry[ $i ] : null,
|
||||
'text' => $text[ $i ] != '' ? $text[ $i ] : null,
|
||||
'table_of_contents' => $table_of_contents[$i] != '' ? $table_of_contents[$i] : null,
|
||||
'meta_title' => $meta_title[ $i ] != '' ? $meta_title[ $i ] : null,
|
||||
'meta_description' => $meta_description[ $i ] != '' ? $meta_description[ $i ] : null,
|
||||
'meta_keywords' => $meta_keywords[ $i ] != '' ? $meta_keywords[ $i ] : null,
|
||||
'seo_link' => \S::seo( $seo_link[ $i ] ) != '' ? \S::seo( $seo_link[ $i ] ) : null,
|
||||
'noindex' => $noindex[ $i ],
|
||||
'copy_from' => $copy_from[ $i ] != '' ? $copy_from[ $i ] : null,
|
||||
'block_direct_access' => $block_direct_access[ $i ]
|
||||
] );
|
||||
$i++;
|
||||
}
|
||||
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
|
||||
{
|
||||
$mdb -> insert( 'pp_articles_langs', [
|
||||
'article_id' => (int)$id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title != '' ? $title : null,
|
||||
'main_image' => $main_image != '' ? $main_image : null,
|
||||
'entry' => $entry != '' ? $entry : null,
|
||||
'text' => $text != '' ? $text : null,
|
||||
'table_of_contents' => $table_of_contents != '' ? $table_of_contents : null,
|
||||
'meta_title' => $meta_title != '' ? $meta_title : null,
|
||||
'meta_description' => $meta_description != '' ? $meta_description : null,
|
||||
'meta_keywords' => $meta_keywords != '' ? $meta_keywords : null,
|
||||
'seo_link' => \S::seo( $seo_link ) != '' ? \S::seo( $seo_link ) : null,
|
||||
'noindex' => $noindex,
|
||||
'copy_from' => $copy_from != '' ? $copy_from : null,
|
||||
'block_direct_access' => $block_direct_access
|
||||
] );
|
||||
}
|
||||
|
||||
/* parametry bez wersji językowych */
|
||||
$results = $mdb -> select( 'pp_articles_additional_params', '*', [ 'AND' => [ 'status' => 1, 'language' => 0 ] ] );
|
||||
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$mdb -> insert( 'pp_articles_additional_values', [
|
||||
'param_id' => $row['id'],
|
||||
'value' => $params[ 'ap_' . $row['name'] ],
|
||||
'article_id' => (int)$id,
|
||||
'language_id' => null
|
||||
] );
|
||||
}
|
||||
|
||||
/* strony */
|
||||
if ( is_array( $pages ) ) foreach ( $pages as $page )
|
||||
{
|
||||
$order = self::max_order() + 1;
|
||||
|
||||
$mdb -> insert( 'pp_articles_pages', [
|
||||
'article_id' => (int)$id,
|
||||
'page_id' => (int)$page,
|
||||
'o' => (int)$order
|
||||
] );
|
||||
}
|
||||
else if ( $pages )
|
||||
{
|
||||
$order = self::max_order() + 1;
|
||||
|
||||
$mdb -> insert( 'pp_articles_pages', [
|
||||
'article_id' => (int)$id,
|
||||
'page_id' => (int)$pages,
|
||||
'o' => (int)$order
|
||||
] );
|
||||
}
|
||||
|
||||
/* pliki */
|
||||
$results = $mdb -> select( 'pp_articles_files', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$dir = '/upload/article_files/article_' . $id;
|
||||
|
||||
$new_file_name = str_replace( '/upload/article_files/tmp', $dir, $row['src'] );
|
||||
|
||||
if ( file_exists( '..' . $row['src'] ) )
|
||||
{
|
||||
if ( !is_dir( '../' . $dir ) and $created !== true )
|
||||
{
|
||||
if ( mkdir( '../' . $dir, 0755, true ) )
|
||||
$created = true;
|
||||
}
|
||||
rename( '..' . $row['src'], '..' . $new_file_name );
|
||||
}
|
||||
|
||||
$mdb -> update( 'pp_articles_files', [ 'src' => $new_file_name, 'article_id' => $id ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
|
||||
$created = false;
|
||||
|
||||
/* zdjęcia */
|
||||
$results = $mdb -> select( 'pp_articles_images', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$dir = '/upload/article_images/article_' . $id;
|
||||
|
||||
$new_file_name = str_replace( '/upload/article_images/tmp', $dir, $row['src'] );
|
||||
|
||||
if ( file_exists( '../' . $new_file_name ) )
|
||||
{
|
||||
$ext = strrpos( $new_file_name, '.' );
|
||||
$fileName_a = substr( $new_file_name, 0, $ext );
|
||||
$fileName_b = substr( $new_file_name, $ext );
|
||||
|
||||
$count = 1;
|
||||
|
||||
while ( file_exists( '../' . $fileName_a . '_' . $count . $fileName_b ) )
|
||||
$count++;
|
||||
|
||||
$new_file_name = $fileName_a . '_' . $count . $fileName_b;
|
||||
}
|
||||
|
||||
if ( file_exists( '..' . $row['src'] ) )
|
||||
{
|
||||
if ( !is_dir( '../' . $dir ) and $created !== true )
|
||||
{
|
||||
if ( mkdir( '../' . $dir, 0755, true ) )
|
||||
$created = true;
|
||||
}
|
||||
rename( '..' . $row['src'], '..' . $new_file_name );
|
||||
}
|
||||
|
||||
$mdb -> update( 'pp_articles_images', [ 'src' => $new_file_name, 'article_id' => (int)$id ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
|
||||
/* tagi */
|
||||
$tags = explode( ',', $tags );
|
||||
if ( is_array( $tags ) ) foreach ( $tags as $tag )
|
||||
{
|
||||
if ( trim( $tag ) != '' )
|
||||
{
|
||||
$tag_id = $mdb -> get( 'pp_tags', 'id', [ 'name' => $tag ] );
|
||||
if ( !$tag_id )
|
||||
{
|
||||
$mdb -> insert( 'pp_tags', [ 'name' => $tag ] );
|
||||
$tag_id = $mdb -> id();
|
||||
}
|
||||
|
||||
$mdb -> insert( 'pp_articles_tags', [ 'article_id' => (int)$id, 'tag_id' => (int)$tag_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb -> update( 'pp_articles', [
|
||||
'show_title' => $show_title == 'on' ? 1 : 0,
|
||||
'show_table_of_contents' => $show_table_of_contents == 'on' ? 1 : 0,
|
||||
'show_date_add' => $show_date_add == 'on' ? 1 : 0,
|
||||
'date_add' => $date_add,
|
||||
'show_date_modify' => $show_date_modify == 'on' ? 1 : 0,
|
||||
'date_modify' => $date_modify ? $date_modify : date( 'Y-m-d H:i:s' ),
|
||||
'modify_by' => $user['id'],
|
||||
'layout_id' => $layout_id ? (int)$layout_id : null,
|
||||
'status' => $status == 'on' ? 1 : 0,
|
||||
'repeat_entry' => $repeat_entry == 'on' ? 1 : 0,
|
||||
'social_icons' => $social_icons == 'on' ? 1 : 0,
|
||||
'date_start' => $event_date[0] ? $event_date[0] : null,
|
||||
'date_end' => $event_date[1] ? $event_date[1] : null,
|
||||
'priority' => $priority == 'on' ? 1 : 0,
|
||||
'password' => $password ? $password : null,
|
||||
'pixieset' => $pixieset,
|
||||
'id_author' => $id_author ? $id_author : null
|
||||
], [
|
||||
'id' => (int)$article_id
|
||||
] );
|
||||
|
||||
if ( $date_add )
|
||||
$mdb -> update( 'pp_articles', [ 'date_add' => $date_add ], [ 'id' => (int)$article_id ] );
|
||||
|
||||
$i = 0;
|
||||
|
||||
/* tłumaczenia */
|
||||
$mdb -> delete( 'pp_articles_langs', [ 'article_id' => (int)$article_id ] );
|
||||
|
||||
$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_articles_langs', [
|
||||
'article_id' => (int)$article_id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title[ $i ] != '' ? $title[ $i ] : null,
|
||||
'main_image' => $main_image[$i] != '' ? $main_image[$i] : null,
|
||||
'entry' => $entry[ $i ] != '' ? $entry[ $i ] : null,
|
||||
'text' => $text[ $i ] != '' ? $text[ $i ] : null,
|
||||
'table_of_contents' => $table_of_contents[$i] != '' ? $table_of_contents[$i] : null,
|
||||
'meta_title' => $meta_title[ $i ] != '' ? $meta_title[ $i ] : null,
|
||||
'meta_description' => $meta_description[ $i ] != '' ? $meta_description[ $i ] : null,
|
||||
'meta_keywords' => $meta_keywords[ $i ] != '' ? $meta_keywords[ $i ] : null,
|
||||
'seo_link' => \S::seo( $seo_link[ $i ] ) != '' ? \S::seo( $seo_link[ $i ] ) : null,
|
||||
'noindex' => $noindex[ $i ],
|
||||
'copy_from' => $copy_from[ $i ] != '' ? $copy_from[ $i ] : null,
|
||||
'block_direct_access' => $block_direct_access[ $i ]
|
||||
] );
|
||||
$i++;
|
||||
}
|
||||
else if ( is_array( $results ) and count( $results ) == 1 ) foreach ( $results as $row )
|
||||
{
|
||||
$mdb -> insert( 'pp_articles_langs', [
|
||||
'article_id' => (int)$article_id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title != '' ? $title : null,
|
||||
'main_image' => $main_image != '' ? $main_image : null,
|
||||
'entry' => $entry != '' ? $entry : null,
|
||||
'text' => $text != '' ? $text : null,
|
||||
'table_of_contents' => $table_of_contents != '' ? $table_of_contents : null,
|
||||
'meta_title' => $meta_title != '' ? $meta_title : null,
|
||||
'meta_description' => $meta_description != '' ? $meta_description : null,
|
||||
'meta_keywords' => $meta_keywords != '' ? $meta_keywords : null,
|
||||
'seo_link' => \S::seo( $seo_link ) != '' ? \S::seo( $seo_link ) : null,
|
||||
'noindex' => $noindex,
|
||||
'copy_from' => $copy_from != '' ? $copy_from : null,
|
||||
'block_direct_access' => $block_direct_access
|
||||
] );
|
||||
}
|
||||
|
||||
/* dodatkowe parametry */
|
||||
$mdb -> delete( 'pp_articles_additional_values', [ 'article_id' => (int)$article_id ] );
|
||||
|
||||
/* parametry bez wersji językowych */
|
||||
$results = $mdb -> select( 'pp_articles_additional_params', '*', [ 'AND' => [ 'status' => 1, 'language' => 0 ] ] );
|
||||
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$mdb -> insert( 'pp_articles_additional_values', [
|
||||
'param_id' => $row['id'],
|
||||
'value' => $params[ 'ap_' . $row['name'] ],
|
||||
'article_id' => (int)$article_id,
|
||||
'language_id' => null
|
||||
] );
|
||||
}
|
||||
|
||||
/* parametry z wersjami językowymi */
|
||||
$results = $mdb -> select( 'pp_articles_additional_params', '*', [ 'AND' => [ 'status' => 1, 'language' => 1 ] ] );
|
||||
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$results2 = $mdb -> select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||
if ( is_array( $results2 ) ) foreach ( $results2 as $row2 )
|
||||
{
|
||||
$mdb -> insert( 'pp_articles_additional_values', [
|
||||
'param_id' => $row['id'],
|
||||
'value' => $params[ 'ap_' . $row['name'] . '_' . $row2['id'] ],
|
||||
'article_id' => (int)$article_id,
|
||||
'language_id' => $row2['id']
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
/* strony */
|
||||
$not_in = [ 0 ];
|
||||
|
||||
if ( is_array( $pages ) ) foreach ( $pages as $page )
|
||||
$not_in[] = $page;
|
||||
else if ( $pages )
|
||||
$not_in[] = $pages;
|
||||
|
||||
$mdb -> delete( 'pp_articles_pages', [ 'AND' => [ 'article_id' => (int)$article_id, 'page_id[!]' => $not_in ] ] );
|
||||
|
||||
$pages_tmp = $mdb -> select( 'pp_articles_pages', 'page_id', [ 'article_id' => (int)$article_id ] );
|
||||
|
||||
if ( !is_array( $pages ) )
|
||||
$pages = [ $pages ];
|
||||
|
||||
$pages = array_diff( $pages, $pages_tmp );
|
||||
|
||||
if ( is_array( $pages ) ) foreach ( $pages as $page )
|
||||
{
|
||||
$order = self::max_order() + 1;
|
||||
|
||||
$mdb -> insert( 'pp_articles_pages', [
|
||||
'article_id' => (int)$article_id,
|
||||
'page_id' => (int)$page,
|
||||
'o' => (int)$order
|
||||
] );
|
||||
}
|
||||
|
||||
/* pliki */
|
||||
$results = $mdb -> select( 'pp_articles_files', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$dir = '/upload/article_files/article_' . $article_id;
|
||||
|
||||
$new_file_name = str_replace( '/upload/article_files/tmp', $dir, $row['src'] );
|
||||
|
||||
if ( file_exists( '..' . $row['src'] ) )
|
||||
{
|
||||
if ( !is_dir( '../' . $dir ) and $created !== true )
|
||||
{
|
||||
if ( mkdir( '../' . $dir, 0755, true ) )
|
||||
$created = true;
|
||||
}
|
||||
rename( '..' . $row['src'], '..' . $new_file_name );
|
||||
}
|
||||
|
||||
$mdb -> update( 'pp_articles_files', [ 'src' => $new_file_name, 'article_id' => (int)$article_id ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
|
||||
$created = false;
|
||||
|
||||
/* zdjęcia */
|
||||
$results = $mdb -> select( 'pp_articles_images', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$dir = '/upload/article_images/article_' . $article_id;
|
||||
|
||||
$new_file_name = str_replace( '/upload/article_images/tmp', $dir, $row['src'] );
|
||||
|
||||
if ( file_exists( '../' . $new_file_name ) )
|
||||
{
|
||||
$ext = strrpos( $new_file_name, '.' );
|
||||
$fileName_a = substr( $new_file_name, 0, $ext );
|
||||
$fileName_b = substr( $new_file_name, $ext );
|
||||
|
||||
$count = 1;
|
||||
|
||||
while ( file_exists( '../' . $fileName_a . '_' . $count . $fileName_b ) )
|
||||
$count++;
|
||||
|
||||
$new_file_name = $fileName_a . '_' . $count . $fileName_b;
|
||||
}
|
||||
|
||||
if ( file_exists( '..' . $row['src'] ) )
|
||||
{
|
||||
if ( !is_dir( '../' . $dir ) and $created !== true )
|
||||
{
|
||||
if ( mkdir( '../' . $dir, 0755, true ) )
|
||||
$created = true;
|
||||
}
|
||||
rename( '..' . $row['src'], '..' . $new_file_name );
|
||||
}
|
||||
|
||||
$mdb -> update( 'pp_articles_images', [ 'src' => $new_file_name, 'article_id' => (int)$article_id ], [ 'id' => $row['id'] ] );
|
||||
}
|
||||
|
||||
$results = $mdb -> select( 'pp_articles_images', '*', [ 'AND' => [ 'article_id' => (int)$article_id, 'to_delete' => 1 ] ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
if ( file_exists( '../' . $row['src'] ) )
|
||||
unlink( '../' . $row['src'] );
|
||||
}
|
||||
|
||||
$mdb -> delete( 'pp_articles_images', [ 'AND' => [ 'article_id' => (int)$article_id, 'to_delete' => 1 ] ] );
|
||||
|
||||
$results = $mdb -> select( 'pp_articles_files', '*', [ 'AND' => [ 'article_id' => (int)$article_id, 'to_delete' => 1 ] ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
if ( file_exists( '../' . $row['src'] ) )
|
||||
unlink( '../' . $row['src'] );
|
||||
}
|
||||
|
||||
$mdb -> delete( 'pp_articles_files', [ 'AND' => [ 'article_id' => (int)$article_id, 'to_delete' => 1 ] ] );
|
||||
|
||||
/* tagi */
|
||||
$mdb -> delete( 'pp_articles_tags', [ 'article_id' => (int)$article_id ] );
|
||||
|
||||
$tags = explode( ',', $tags );
|
||||
if ( is_array( $tags ) ) foreach ( $tags as $tag )
|
||||
{
|
||||
if ( trim( $tag ) != '' )
|
||||
{
|
||||
$tag_id = $mdb -> get( 'pp_tags', 'id', [ 'name' => $tag ] );
|
||||
if ( !$tag_id )
|
||||
{
|
||||
$mdb -> insert( 'pp_tags', [ 'name' => $tag ] );
|
||||
$tag_id = $mdb -> id();
|
||||
}
|
||||
|
||||
$mdb -> insert( 'pp_articles_tags', [ 'article_id' => (int)$article_id, 'tag_id' => (int)$tag_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
return $article_id;
|
||||
}
|
||||
global $user;
|
||||
return self::repo()->articleSave(
|
||||
$article_id, $title, $main_image, $entry, $text, $table_of_contents, $status, $show_title, $show_table_of_contents, $show_date_add, $date_add, $show_date_modify, $date_modify, $seo_link, $meta_title, $meta_description,
|
||||
$meta_keywords, $layout_id, $pages, $noindex, $repeat_entry, $copy_from, $social_icons, $event_date, $tags, $block_direct_access, $priority,
|
||||
$password, $pixieset, $id_author, $params, (int)$user['id']
|
||||
);
|
||||
}
|
||||
|
||||
public static function delete_nonassigned_files()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$results = $mdb -> select( 'pp_articles_files', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
if ( file_exists( '../' . $row['src'] ) )
|
||||
unlink( '../' . $row['src'] );
|
||||
}
|
||||
|
||||
$mdb -> delete( 'pp_articles_files', [ 'article_id' => null ] );
|
||||
self::repo()->deleteNonassignedFiles();
|
||||
}
|
||||
|
||||
public static function delete_nonassigned_images()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$results = $mdb -> select( 'pp_articles_images', '*', [ 'article_id' => null ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
if ( file_exists( '../' . $row['src'] ) )
|
||||
unlink( '../' . $row['src'] );
|
||||
}
|
||||
|
||||
$mdb -> delete( 'pp_articles_images', [ 'article_id' => null ] );
|
||||
self::repo()->deleteNonassignedImages();
|
||||
}
|
||||
}
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
}
|
||||
@@ -3,139 +3,36 @@ namespace admin\factory;
|
||||
|
||||
class Layouts
|
||||
{
|
||||
public static function layout_delete( $layout_id )
|
||||
private static function repo(): \Domain\Layouts\LayoutsRepository
|
||||
{
|
||||
global $mdb;
|
||||
if ( $mdb -> count( 'pp_layouts' ) > 1 )
|
||||
return $mdb -> delete( 'pp_layouts', [ 'id' => (int)$layout_id ] );
|
||||
return false;
|
||||
return new \Domain\Layouts\LayoutsRepository( $mdb );
|
||||
}
|
||||
|
||||
public static function layout_delete( $layout_id )
|
||||
{
|
||||
return self::repo()->layoutDelete( $layout_id );
|
||||
}
|
||||
|
||||
public static function layout_details( $layout_id )
|
||||
{
|
||||
global $mdb;
|
||||
$layout = $mdb -> get( 'pp_layouts', '*', [ 'id' => (int)$layout_id ] );
|
||||
|
||||
$layout['pages'] = $mdb -> select( 'pp_layouts_pages', 'page_id', [ 'layout_id' => (int)$layout_id ] );
|
||||
|
||||
return $layout;
|
||||
|
||||
return self::repo()->layoutDetails( $layout_id );
|
||||
}
|
||||
|
||||
public static function layout_save( $layout_id, $name, $status, $pages, $html, $css, $js, $m_html, $m_css, $m_js )
|
||||
{
|
||||
global $mdb;
|
||||
if ( !$layout_id )
|
||||
{
|
||||
if ( $status == 'on' )
|
||||
$mdb -> update( 'pp_layouts', [ 'status' => 0 ] );
|
||||
|
||||
$mdb -> insert( 'pp_layouts', [
|
||||
'name' => $name,
|
||||
'html' => $html,
|
||||
'css' => $css,
|
||||
'js' => $js,
|
||||
'm_html' => $m_html,
|
||||
'm_css' => $m_css,
|
||||
'm_js' => $m_js,
|
||||
'status' => $status == 'on' ? 1 : 0
|
||||
] );
|
||||
|
||||
$id = $mdb -> id();
|
||||
|
||||
if ( $id )
|
||||
{
|
||||
if ( is_array( $pages ) ) foreach ( $pages as $page )
|
||||
{
|
||||
$mdb -> delete( 'pp_layouts_pages', [ 'page_id' => (int)$page ] );
|
||||
|
||||
$mdb -> insert( 'pp_layouts_pages', [
|
||||
'layout_id' => (int)$id,
|
||||
'page_id' => (int)$page
|
||||
] );
|
||||
}
|
||||
else if ( $pages )
|
||||
{
|
||||
$mdb -> delete( 'pp_layouts_pages', [ 'page_id' => (int)$pages ] );
|
||||
|
||||
$mdb -> insert( 'pp_layouts_pages', [
|
||||
'layout_id' => (int)$id,
|
||||
'page_id' => (int)$pages
|
||||
] );
|
||||
}
|
||||
|
||||
\S::delete_cache();
|
||||
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( $status == 'on' )
|
||||
$mdb -> update( 'pp_layouts', [ 'status' => 0 ] );
|
||||
|
||||
$mdb -> update( 'pp_layouts', [
|
||||
'name' => $name,
|
||||
'html' => $html,
|
||||
'css' => $css,
|
||||
'js' => $js,
|
||||
'm_html' => $m_html,
|
||||
'm_css' => $m_css,
|
||||
'm_js' => $m_js,
|
||||
'status' => $status == 'on' ? 1 : 0
|
||||
], [
|
||||
'id' => $layout_id
|
||||
] );
|
||||
|
||||
$mdb -> delete( 'pp_layouts_pages', [ 'layout_id' => (int)$layout_id ] );
|
||||
|
||||
if ( is_array( $pages ) ) foreach ( $pages as $page )
|
||||
{
|
||||
$mdb -> delete( 'pp_layouts_pages', [ 'page_id' => (int)$page ] );
|
||||
|
||||
$mdb -> insert( 'pp_layouts_pages', [
|
||||
'layout_id' => (int)$layout_id,
|
||||
'page_id' => (int)$page
|
||||
] );
|
||||
}
|
||||
else if ( $pages )
|
||||
{
|
||||
$mdb -> delete( 'pp_layouts_pages', [ 'page_id' => (int)$pages ] );
|
||||
|
||||
$mdb -> insert( 'pp_layouts_pages', [
|
||||
'layout_id' => (int)$layout_id,
|
||||
'page_id' => (int)$pages
|
||||
] );
|
||||
}
|
||||
|
||||
\S::delete_cache();
|
||||
|
||||
return $layout_id;
|
||||
}
|
||||
return false;
|
||||
|
||||
{
|
||||
return self::repo()->layoutSave( $layout_id, $name, $status, $pages, $html, $css, $js, $m_html, $m_css, $m_js );
|
||||
}
|
||||
|
||||
public static function menus_list()
|
||||
{
|
||||
global $mdb;
|
||||
$results = $mdb -> select( 'pp_menus', 'id', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
{
|
||||
$menu = \admin\factory\Pages::menu_details( $row );
|
||||
$menu['pages'] = \admin\factory\Pages::menu_pages( $row );
|
||||
|
||||
$menus[] = $menu;
|
||||
}
|
||||
return $menus;
|
||||
|
||||
return self::repo()->menusList();
|
||||
}
|
||||
|
||||
public static function layouts_list()
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> select( 'pp_layouts', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
|
||||
}
|
||||
return self::repo()->layoutsList();
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +1,53 @@
|
||||
<?
|
||||
|
||||
<?php
|
||||
namespace admin\factory;
|
||||
|
||||
class Pages
|
||||
{
|
||||
|
||||
public static $_page_types = [ 0 => 'pełne artykuły', 1 => 'wprowadzenia', 2 => 'miniaturki', 3 => 'link', 4 => 'kontakt' ];
|
||||
public static $_sort_types = [
|
||||
0 => 'data dodania - najstarsze na początku',
|
||||
1 => 'data dodania - najnowsze na początku',
|
||||
2 => 'data modyfikacji - rosnąco',
|
||||
3 => 'data mofyfikacji - malejąco',
|
||||
4 => 'ręczne',
|
||||
5 => 'alfabetycznie - A - Z',
|
||||
6 => 'alfabetycznie - Z - A'
|
||||
0 => 'data dodania - najstarsze na początku',
|
||||
1 => 'data dodania - najnowsze na początku',
|
||||
2 => 'data modyfikacji - rosnąco',
|
||||
3 => 'data mofyfikacji - malejąco',
|
||||
4 => 'ręczne',
|
||||
5 => 'alfabetycznie - A - Z',
|
||||
6 => 'alfabetycznie - Z - A'
|
||||
];
|
||||
|
||||
private static function repo(): \Domain\Pages\PagesRepository
|
||||
{
|
||||
global $mdb;
|
||||
return new \Domain\Pages\PagesRepository( $mdb );
|
||||
}
|
||||
|
||||
public static function save_articles_order( $page_id, $articles )
|
||||
{
|
||||
global $mdb;
|
||||
if ( is_array( $articles ) )
|
||||
{
|
||||
$mdb -> update( 'pp_articles_pages', [ 'o' => 0 ],
|
||||
[ 'page_id' => (int) $page_id ] );
|
||||
|
||||
for ( $i = 0; $i < count( $articles ); $i++ )
|
||||
{
|
||||
if ( $articles[$i]['item_id'] )
|
||||
{
|
||||
$x++;
|
||||
$mdb -> update( 'pp_articles_pages', [ 'o' => $x ],
|
||||
[ 'AND' => [ 'page_id' => (int) $page_id, 'article_id' => $articles[$i]['item_id'] ] ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return self::repo()->saveArticlesOrder( $page_id, $articles );
|
||||
}
|
||||
|
||||
public static function page_articles( $page_id )
|
||||
{
|
||||
global $mdb;
|
||||
$results = $mdb -> query( 'SELECT '
|
||||
. 'article_id, o, status '
|
||||
. 'FROM '
|
||||
. 'pp_articles_pages AS ap '
|
||||
. 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
|
||||
. 'WHERE '
|
||||
. 'page_id = ' . (int) $page_id . ' AND status != -1 '
|
||||
. 'ORDER BY '
|
||||
. 'o ASC' ) -> fetchAll();
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$row['title'] = \admin\factory\Articles::article_title( $row['article_id'] );
|
||||
$articles[] = $row;
|
||||
}
|
||||
return $articles;
|
||||
return self::repo()->pageArticles( $page_id );
|
||||
}
|
||||
|
||||
public static function menus_list()
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> select( 'pp_menus', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
|
||||
return self::repo()->menusList();
|
||||
}
|
||||
|
||||
public static function save_pages_order( $menu_id, $pages )
|
||||
{
|
||||
global $mdb;
|
||||
if ( is_array( $pages ) )
|
||||
{
|
||||
$mdb -> update( 'pp_pages', [ 'o' => 0 ], [ 'menu_id' => (int) $menu_id ] );
|
||||
|
||||
for ( $i = 0; $i < count( $pages ); $i++ )
|
||||
{
|
||||
if ( $pages[$i]['item_id'] )
|
||||
{
|
||||
$pages[$i]['parent_id'] ? $parent_id = $pages[$i]['parent_id'] : $parent_id = 0;
|
||||
|
||||
if ( $pages[$i]['item_id'] && $pages[$i]['depth'] > 1 )
|
||||
{
|
||||
if ( $pages[$i]['depth'] == 2 )
|
||||
$parent_id = null;
|
||||
|
||||
$x++;
|
||||
|
||||
$mdb -> update( 'pp_pages', [ 'o' => $x, 'parent_id' => $parent_id ],
|
||||
[ 'id' => (int) $pages[$i]['item_id'] ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\S::delete_cache();
|
||||
|
||||
return true;
|
||||
return self::repo()->savePagesOrder( $menu_id, $pages );
|
||||
}
|
||||
|
||||
public static function page_delete( $page_id )
|
||||
{
|
||||
global $mdb;
|
||||
if ( $mdb -> count( 'pp_pages', [ 'parent_id' => (int) $page_id ] ) )
|
||||
return false;
|
||||
|
||||
if ( $mdb -> delete( 'pp_pages', [ 'id' => (int) $page_id ] ) )
|
||||
{
|
||||
\S::delete_cache();
|
||||
\S::htacces();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return self::repo()->pageDelete( $page_id );
|
||||
}
|
||||
|
||||
public static function max_order()
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> max( 'pp_pages', 'o' );
|
||||
return self::repo()->maxOrder();
|
||||
}
|
||||
|
||||
public static function page_save(
|
||||
@@ -122,388 +55,70 @@ class Pages
|
||||
$site_title, $block_direct_access, $cache, $canonical
|
||||
)
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
if ( !$parent_id )
|
||||
$parent_id = null;
|
||||
|
||||
if ( !$page_id )
|
||||
{
|
||||
$order = self::max_order() + 1;
|
||||
|
||||
$mdb -> insert( 'pp_pages', [
|
||||
'menu_id' => (int) $menu_id,
|
||||
'page_type' => $page_type,
|
||||
'sort_type' => $sort_type,
|
||||
'articles_limit' => $articles_limit,
|
||||
'show_title' => $show_title == 'on' ? 1 : 0,
|
||||
'status' => $status == 'on' ? 1 : 0,
|
||||
'o' => (int) $order,
|
||||
'parent_id' => $parent_id,
|
||||
'start' => $start == 'on' ? 1 : 0,
|
||||
'cache' => $cache == 'on' ? 1 : 0
|
||||
] );
|
||||
|
||||
$id = $mdb -> id();
|
||||
|
||||
if ( $id )
|
||||
{
|
||||
if ( $start )
|
||||
$mdb -> update( 'pp_pages', [ 'start' => 0 ], [ 'id[!]' => (int)$id ] );
|
||||
|
||||
if ( $layout_id )
|
||||
$mdb -> insert( 'pp_layouts_pages', [ 'page_id' => (int) $id, 'layout_id' => (int)$layout_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_pages_langs', [
|
||||
'page_id' => (int) $id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title[$i] != '' ? $title[$i] : null,
|
||||
'meta_description' => $meta_description[$i] != '' ? $meta_description[$i] : null,
|
||||
'meta_keywords' => $meta_keywords[$i] != '' ? $meta_keywords[$i] : null,
|
||||
'meta_title' => $meta_title[$i] != '' ? $meta_title[$i] : null,
|
||||
'seo_link' => \S::seo( $seo_link[$i] ) != '' ? \S::seo( $seo_link[$i] ) : null,
|
||||
'noindex' => $noindex[$i],
|
||||
'site_title' => $site_title[$i] != '' ? $site_title[$i] : null,
|
||||
'link' => $link[$i] != '' ? $link[$i] : null,
|
||||
'block_direct_access' => $block_direct_access[$i],
|
||||
'canonical' => $canonical[$i] != '' ? $canonical[$i] : null
|
||||
] );
|
||||
$i++;
|
||||
}
|
||||
else if ( is_array( $results ) and count( $results ) == 1 )
|
||||
{
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$mdb -> insert( 'pp_pages_langs', [
|
||||
'page_id' => (int) $id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title != '' ? $title : null,
|
||||
'meta_description' => $meta_description != '' ? $meta_description : null,
|
||||
'meta_keywords' => $meta_keywords != '' ? $meta_keywords : null,
|
||||
'meta_title' => $meta_title != '' ? $meta_title : null,
|
||||
'seo_link' => \S::seo( $seo_link ) != '' ? \S::seo( $seo_link ) : null,
|
||||
'noindex' => $noindex,
|
||||
'site_title' => $site_title != '' ? $site_title : null,
|
||||
'link' => $link != '' ? $link : null,
|
||||
'block_direct_access' => $block_direct_access,
|
||||
'canonical' => $canonical != '' ? $canonical : null
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb -> update( 'pp_pages',
|
||||
[
|
||||
'menu_id' => (int) $menu_id,
|
||||
'page_type' => $page_type,
|
||||
'sort_type' => $sort_type,
|
||||
'articles_limit' => $articles_limit,
|
||||
'show_title' => $show_title == 'on' ? 1 : 0,
|
||||
'status' => $status == 'on' ? 1 : 0,
|
||||
'parent_id' => $parent_id,
|
||||
'start' => $start == 'on' ? 1 : 0,
|
||||
'cache' => $cache == 'on' ? 1 : 0
|
||||
], [
|
||||
'id' => (int) $page_id
|
||||
] );
|
||||
|
||||
if ( $layout_id )
|
||||
{
|
||||
$mdb -> delete( 'pp_layouts_pages', [ 'page_id' => (int) $page_id ] );
|
||||
$mdb -> insert( 'pp_layouts_pages',
|
||||
[ 'layout_id' => (int) $layout_id, 'page_id' => (int) $page_id ] );
|
||||
}
|
||||
|
||||
if ( $start )
|
||||
$mdb -> update( 'pp_pages', [ 'start' => 0 ],
|
||||
[ 'id[!]' => (int) $page_id ] );
|
||||
|
||||
$i = 0;
|
||||
|
||||
$mdb -> delete( 'pp_pages_langs', [ 'page_id' => (int) $page_id ] );
|
||||
|
||||
$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_pages_langs',
|
||||
[
|
||||
'page_id' => (int) $page_id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title[$i] != '' ? $title[$i] : null,
|
||||
'meta_description' => $meta_description[$i] != '' ? $meta_description[$i] : null,
|
||||
'meta_keywords' => $meta_keywords[$i] != '' ? $meta_keywords[$i] : null,
|
||||
'meta_title' => $meta_title[$i] != '' ? $meta_title[$i] : null,
|
||||
'seo_link' => \S::seo( $seo_link[$i] ) != '' ? \S::seo( $seo_link[$i] ) : null,
|
||||
'noindex' => $noindex[$i],
|
||||
'site_title' => $site_title[$i] != '' ? $site_title[$i] : null,
|
||||
'link' => $link[$i] != '' ? $link[$i] : null,
|
||||
'block_direct_access' => $block_direct_access[$i],
|
||||
'canonical' => $canonical[$i] != '' ? $canonical[$i] : null
|
||||
] );
|
||||
|
||||
$i++;
|
||||
}
|
||||
else if ( is_array( $results ) and count( $results ) == 1 )
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$mdb -> insert( 'pp_pages_langs',
|
||||
[
|
||||
'page_id' => (int) $page_id,
|
||||
'lang_id' => $row['id'],
|
||||
'title' => $title != '' ? $title : null,
|
||||
'meta_description' => $meta_description != '' ? $meta_description : null,
|
||||
'meta_keywords' => $meta_keywords != '' ? $meta_keywords : null,
|
||||
'meta_title' => $meta_title != '' ? $meta_title : null,
|
||||
'seo_link' => \S::seo( $seo_link ) != '' ? \S::seo( $seo_link ) : null,
|
||||
'noindex' => $noindex,
|
||||
'site_title' => $site_title != '' ? $site_title : null,
|
||||
'link' => $link != '' ? $link : null,
|
||||
'block_direct_access' => $block_direct_access,
|
||||
'canonical' => $canonical != '' ? $canonical : null
|
||||
] );
|
||||
}
|
||||
|
||||
self::update_supages_menu_id( $page_id, $menu_id );
|
||||
|
||||
\S::htacces();
|
||||
\S::delete_cache();
|
||||
|
||||
return $page_id;
|
||||
}
|
||||
return false;
|
||||
return self::repo()->pageSave(
|
||||
$page_id, $title, $seo_link, $meta_title, $meta_description, $meta_keywords, $menu_id, $parent_id, $page_type, $sort_type, $layout_id, $articles_limit, $show_title, $status, $link, $noindex, $start,
|
||||
$site_title, $block_direct_access, $cache, $canonical
|
||||
);
|
||||
}
|
||||
|
||||
public static function update_supages_menu_id( $parent_id, $menu_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$mdb -> update( 'pp_pages', [ 'menu_id' => (int) $menu_id ],
|
||||
[ 'parent_id' => $parent_id ] );
|
||||
|
||||
$results = $mdb -> select( 'pp_pages', [ 'id' ], [ 'parent_id' => $parent_id ] );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
self::update_supages_menu_id( $row['id'], $menu_id );
|
||||
self::repo()->updateSubpagesMenuId( (int) $parent_id, (int) $menu_id );
|
||||
}
|
||||
|
||||
public static function generate_seo_link( $title, $page_id, $article_id,
|
||||
$lang, $pid )
|
||||
public static function generate_seo_link( $title, $page_id, $article_id, $lang, $pid )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$seo_link = \S::seo( $title );
|
||||
|
||||
|
||||
while ( !$seo_link_check )
|
||||
{
|
||||
if ( $mdb -> count( 'pp_pages_langs',
|
||||
[ 'AND' => [ 'seo_link' => $seo_link, 'page_id[!]' => (int) $page_id ] ] ) )
|
||||
$seo_link = $seo_link . '-' . ( ++$i );
|
||||
else
|
||||
$seo_link_check = true;
|
||||
}
|
||||
|
||||
$seo_link_check = false;
|
||||
|
||||
while ( !$seo_link_check )
|
||||
{
|
||||
if ( $mdb -> count( 'pp_articles_langs',
|
||||
[ 'AND' => [ 'seo_link' => $seo_link, 'article_id[!]' => (int) $article_id ] ] ) )
|
||||
$seo_link = $seo_link . '-' . ( ++$i );
|
||||
else
|
||||
$seo_link_check = true;
|
||||
}
|
||||
return $seo_link;
|
||||
return self::repo()->generateSeoLink( $title, $page_id, $article_id, $lang, $pid );
|
||||
}
|
||||
|
||||
public static function google_url_preview( $page_id, $title, $lang, $pid, $id, $seo_link, $language_link = '' )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$prefix = $language_link;
|
||||
$status = true;
|
||||
$id_page = $page_id;
|
||||
|
||||
do
|
||||
{
|
||||
if ( $page_id )
|
||||
{
|
||||
$parent = \admin\factory\Pages::page_details( $page_id );
|
||||
$parent_id = $parent['parent_id'];
|
||||
}
|
||||
else
|
||||
$parent_id = $pid;
|
||||
|
||||
if ( $parent_id )
|
||||
{
|
||||
$results = $mdb -> query( "SELECT title, seo_link, page_id FROM pp_pages_langs AS ppl, pp_langs AS pl WHERE lang_id = pl.id AND page_id = " . (int) $parent_id . " AND ppl.lang_id = '" . $lang . "' " ) -> fetchAll();
|
||||
if ( $results[0]['seo_link'] )
|
||||
$seo = $results[0]['seo_link'] . '/' . $seo;
|
||||
else
|
||||
$seo = 's-' . $results[0]['page_id'] . '-' . \S::seo( $results[0]['title'] ) . '/' . $seo;
|
||||
$page_id = $results[0]['page_id'];
|
||||
}
|
||||
else
|
||||
$status = false;
|
||||
}
|
||||
while ( $status );
|
||||
|
||||
if ( $id )
|
||||
{
|
||||
if ( !$seo_link )
|
||||
$seo = $seo . 's-' . $id . '-' . \S::seo( $title );
|
||||
else
|
||||
$seo = $seo . $seo_link;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( !$seo_link )
|
||||
$seo = $seo . 's-' . $id_page . '-' . \S::seo( $title );
|
||||
else
|
||||
$seo = $seo . $seo_link;
|
||||
}
|
||||
|
||||
if ( $prefix )
|
||||
$seo = $prefix . $seo;
|
||||
|
||||
return $seo;
|
||||
return self::repo()->googleUrlPreview( $page_id, $title, $lang, $pid, $id, $seo_link, $language_link );
|
||||
}
|
||||
|
||||
public static function menu_delete( $menu_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
if ( $mdb -> count( 'pp_pages', [ 'menu_id' => (int) $menu_id ] ) )
|
||||
return false;
|
||||
|
||||
return $mdb -> delete( 'pp_menus', [ 'id' => (int) $menu_id ] );
|
||||
return self::repo()->menuDelete( $menu_id );
|
||||
}
|
||||
|
||||
public static function menu_details( $menu_id )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> get( 'pp_menus', '*', [ 'id' => (int) $menu_id ] );
|
||||
return self::repo()->menuDetails( $menu_id );
|
||||
}
|
||||
|
||||
public static function menu_save( $menu_id, $name, $status )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$status == 'on' ? $status = 1 : $status = 0;
|
||||
|
||||
if ( !$menu_id )
|
||||
{
|
||||
return $mdb -> insert( 'pp_menus',
|
||||
[
|
||||
'name' => $name,
|
||||
'status' => $status
|
||||
] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb -> update( 'pp_menus',
|
||||
[
|
||||
'name' => $name,
|
||||
'status' => $status
|
||||
], [
|
||||
'id' => (int) $menu_id
|
||||
] );
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return self::repo()->menuSave( $menu_id, $name, $status );
|
||||
}
|
||||
|
||||
public static function menu_lists()
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> select( 'pp_menus', '*', [ 'ORDER' => [ 'id' => 'ASC' ] ] );
|
||||
return self::repo()->menuLists();
|
||||
}
|
||||
|
||||
public static function page_details( $page_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$page = $mdb -> get( 'pp_pages', '*', [ 'id' => (int) $page_id ] );
|
||||
|
||||
$results = $mdb -> select( 'pp_pages_langs', '*',
|
||||
[ 'page_id' => (int) $page_id ] );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
$page['languages'][$row['lang_id']] = $row;
|
||||
|
||||
$page['layout_id'] = $mdb -> get( 'pp_layouts_pages', 'layout_id',
|
||||
[ 'page_id' => (int) $page_id ] );
|
||||
|
||||
return $page;
|
||||
return self::repo()->pageDetails( $page_id );
|
||||
}
|
||||
|
||||
public static function page_url( $page_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$results = $mdb -> query( "SELECT seo_link, title lang_id FROM pp_pages_langs AS ppl, pp_langs AS pl WHERE lang_id = pl.id AND page_id = " . (int) $page_id . " AND seo_link != '' ORDER BY o ASC LIMIT 1" ) -> fetchAll();
|
||||
|
||||
if ( !$results[0]['seo_link'] )
|
||||
{
|
||||
$title = self::page_title( $article_id );
|
||||
return 's-' . $page_id . '-' . \S::seo( $title );
|
||||
}
|
||||
else
|
||||
return $results[0]['seo_link'];
|
||||
return self::repo()->pageUrl( $page_id );
|
||||
}
|
||||
|
||||
public static function page_title( $page_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$result = $mdb -> select( 'pp_pages_langs',
|
||||
[ '[><]pp_langs' => [ 'lang_id' => 'id' ] ], 'title',
|
||||
[ 'AND' => [ 'page_id' => (int) $page_id, 'title[!]' => '' ], 'ORDER' => [ 'o' => 'ASC' ], 'LIMIT' => 1 ] );
|
||||
return $result[0];
|
||||
return self::repo()->pageTitle( $page_id );
|
||||
}
|
||||
|
||||
public static function page_languages( $page_id )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> select( 'pp_pages_langs', '*',
|
||||
[ 'AND' => [ 'page_id' => (int) $page_id, 'title[!]' => null ] ] );
|
||||
return self::repo()->pageLanguages( $page_id );
|
||||
}
|
||||
|
||||
public static function menu_pages( $menu_id, $parent_id = null )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$results = $mdb -> select( 'pp_pages',
|
||||
[ 'id', 'menu_id', 'status', 'parent_id', 'start' ],
|
||||
[ 'AND' => [ 'menu_id' => $menu_id, 'parent_id' => $parent_id ], 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||
if ( is_array( $results ) )
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$row['title'] = self::page_title( $row['id'] );
|
||||
$row['languages'] = self::page_languages( $row['id'] );
|
||||
$row['subpages'] = self::menu_pages( $menu_id, $row['id'] );
|
||||
|
||||
$pages[] = $row;
|
||||
}
|
||||
|
||||
return $pages;
|
||||
return self::repo()->menuPages( $menu_id, $parent_id );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
?>
|
||||
|
||||
68
autoload/admin/factory/class.Releases.php
Normal file
68
autoload/admin/factory/class.Releases.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
namespace admin\factory;
|
||||
|
||||
class Releases
|
||||
{
|
||||
public static function get_versions(): array
|
||||
{
|
||||
global $mdb;
|
||||
$repo = new \Domain\Releases\ReleasesRepository($mdb);
|
||||
return $repo->getVersions();
|
||||
}
|
||||
|
||||
public static function promote(string $version): void
|
||||
{
|
||||
global $mdb;
|
||||
$repo = new \Domain\Releases\ReleasesRepository($mdb);
|
||||
$repo->promote($version);
|
||||
}
|
||||
|
||||
public static function demote(string $version): void
|
||||
{
|
||||
global $mdb;
|
||||
$repo = new \Domain\Releases\ReleasesRepository($mdb);
|
||||
$repo->demote($version);
|
||||
}
|
||||
|
||||
public static function discover_versions(): int
|
||||
{
|
||||
global $mdb;
|
||||
$repo = new \Domain\Releases\ReleasesRepository($mdb);
|
||||
return $repo->discoverVersions();
|
||||
}
|
||||
|
||||
public static function get_licenses(): array
|
||||
{
|
||||
global $mdb;
|
||||
$repo = new \Domain\Releases\ReleasesRepository($mdb);
|
||||
return $repo->getLicenses();
|
||||
}
|
||||
|
||||
public static function get_license(int $id): array
|
||||
{
|
||||
global $mdb;
|
||||
$repo = new \Domain\Releases\ReleasesRepository($mdb);
|
||||
return $repo->getLicense($id);
|
||||
}
|
||||
|
||||
public static function save_license(array $data): void
|
||||
{
|
||||
global $mdb;
|
||||
$repo = new \Domain\Releases\ReleasesRepository($mdb);
|
||||
$repo->saveLicense($data);
|
||||
}
|
||||
|
||||
public static function delete_license(int $id): void
|
||||
{
|
||||
global $mdb;
|
||||
$repo = new \Domain\Releases\ReleasesRepository($mdb);
|
||||
$repo->deleteLicense($id);
|
||||
}
|
||||
|
||||
public static function toggle_beta(int $id): void
|
||||
{
|
||||
global $mdb;
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
||||
13
autoload/admin/view/class.Releases.php
Normal file
13
autoload/admin/view/class.Releases.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace admin\view;
|
||||
|
||||
class Releases
|
||||
{
|
||||
public static function main_view(): string
|
||||
{
|
||||
$tpl = new \Tpl;
|
||||
$tpl->versions = \admin\factory\Releases::get_versions();
|
||||
$tpl->licenses = \admin\factory\Releases::get_licenses();
|
||||
return $tpl->render('releases/main-view');
|
||||
}
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
composer.json
Normal file
18
composer.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
1688
composer.lock
generated
Normal file
1688
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
composer.phar
Normal file
BIN
composer.phar
Normal file
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
$database['host'] = 'localhost';
|
||||
$database['host_remote'] = 'host117523.hostido.net.pl';
|
||||
$database['user'] = 'host117523_cmspro';
|
||||
$database['password'] = '3sJADeqKHLqHddfavDeR';
|
||||
$database['name'] = 'host117523_cmspro';
|
||||
|
||||
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,34 +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 ✓
|
||||
├── Settings/SettingsRepository.php ← \Domain\Settings\SettingsRepository ✓
|
||||
└── User/UserRepository.php ← \Domain\User\UserRepository ✓
|
||||
├── 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 ✓
|
||||
```
|
||||
|
||||
Następne: `Domain\Pages`, `Domain\Layouts`, `Domain\Articles`, ...
|
||||
|
||||
Następne: `Admin\` namespace (Fazy 6–13), `Frontend\` namespace (Fazy 14–16).
|
||||
---
|
||||
|
||||
## Katalogi
|
||||
@@ -118,3 +131,4 @@ Główne tabele: `pp_users`, `pp_articles`, `pp_articles_langs`, `pp_pages`,
|
||||
- Tabela: `pp_languages`
|
||||
- Składnia w treści: `[LANG:klucz]`
|
||||
- Cache tłumaczeń: `$_SESSION['lang-{lang_id}']`
|
||||
|
||||
|
||||
120
docs/TESTING.md
120
docs/TESTING.md
@@ -1,106 +1,54 @@
|
||||
# Testowanie shopPRO
|
||||
# Testowanie cmsPRO
|
||||
|
||||
## Szybki start
|
||||
|
||||
```bash
|
||||
# Pelny suite (PowerShell — rekomendowane)
|
||||
./test.ps1
|
||||
# Instalacja PHPUnit (jednorazowo)
|
||||
composer install
|
||||
|
||||
# Uruchomienie testów
|
||||
./vendor/bin/phpunit
|
||||
|
||||
# Konkretny plik
|
||||
./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php
|
||||
./vendor/bin/phpunit tests/Unit/Domain/Settings/SettingsRepositoryTest.php
|
||||
|
||||
# Konkretny test
|
||||
./test.ps1 --filter testGetQuantityReturnsCorrectValue
|
||||
|
||||
# Alternatywne
|
||||
composer test # standard
|
||||
./test.bat # testdox (czytelna lista)
|
||||
./test-simple.bat # kropki
|
||||
./test-debug.bat # debug
|
||||
./test.sh # Git Bash
|
||||
./vendor/bin/phpunit --filter testAllSettingsReturnsMappedArray
|
||||
```
|
||||
|
||||
## Aktualny stan
|
||||
|
||||
```text
|
||||
OK (805 tests, 2253 assertions)
|
||||
Testy jednostkowe dla Domain\ (Faza 2 DDD)
|
||||
```
|
||||
|
||||
Zweryfikowano: 2026-02-24 (ver. 0.318)
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
- **PHPUnit 9.6** via `phpunit.phar`
|
||||
- **PHPUnit 10** via `composer`
|
||||
- **Bootstrap:** `tests/bootstrap.php`
|
||||
- **Config:** `phpunit.xml`
|
||||
|
||||
## Struktura testow
|
||||
## Struktura testów
|
||||
|
||||
```
|
||||
tests/
|
||||
|-- bootstrap.php
|
||||
|-- stubs/
|
||||
| |-- CacheHandler.php (inline w bootstrap)
|
||||
| |-- Helpers.php (Shared\Helpers\Helpers stub)
|
||||
| `-- ShopProduct.php (shop\Product stub)
|
||||
|-- Unit/
|
||||
| |-- Domain/
|
||||
| | |-- Article/ArticleRepositoryTest.php
|
||||
| | |-- Attribute/AttributeRepositoryTest.php
|
||||
| | |-- Banner/BannerRepositoryTest.php
|
||||
| | |-- Basket/BasketCalculatorTest.php
|
||||
| | |-- Cache/CacheRepositoryTest.php
|
||||
| | |-- Category/CategoryRepositoryTest.php
|
||||
| | |-- Coupon/CouponRepositoryTest.php
|
||||
| | |-- CronJob/CronJobTypeTest.php
|
||||
| | |-- CronJob/CronJobRepositoryTest.php
|
||||
| | |-- CronJob/CronJobProcessorTest.php
|
||||
| | |-- Dictionaries/DictionariesRepositoryTest.php
|
||||
| | |-- Integrations/IntegrationsRepositoryTest.php
|
||||
| | |-- Languages/LanguagesRepositoryTest.php
|
||||
| | |-- Layouts/LayoutsRepositoryTest.php
|
||||
| | |-- Newsletter/NewsletterRepositoryTest.php
|
||||
| | |-- Pages/PagesRepositoryTest.php
|
||||
| | |-- PaymentMethod/PaymentMethodRepositoryTest.php
|
||||
| | |-- Producer/ProducerRepositoryTest.php
|
||||
| | |-- Product/ProductRepositoryTest.php
|
||||
| | |-- ProductSet/ProductSetRepositoryTest.php
|
||||
| | |-- Promotion/PromotionRepositoryTest.php
|
||||
| | |-- Settings/SettingsRepositoryTest.php
|
||||
| | |-- ShopStatus/ShopStatusRepositoryTest.php
|
||||
| | |-- Transport/TransportRepositoryTest.php
|
||||
| | |-- Update/UpdateRepositoryTest.php
|
||||
| | `-- User/UserRepositoryTest.php
|
||||
| `-- admin/
|
||||
| `-- Controllers/
|
||||
| |-- ArticlesControllerTest.php
|
||||
| |-- DictionariesControllerTest.php
|
||||
| |-- IntegrationsControllerTest.php
|
||||
| |-- ProductArchiveControllerTest.php
|
||||
| |-- SettingsControllerTest.php
|
||||
| |-- ShopAttributeControllerTest.php
|
||||
| |-- ShopCategoryControllerTest.php
|
||||
| |-- ShopCouponControllerTest.php
|
||||
| |-- ShopPaymentMethodControllerTest.php
|
||||
| |-- ShopProducerControllerTest.php
|
||||
| |-- ShopProductControllerTest.php
|
||||
| |-- ShopProductSetsControllerTest.php
|
||||
| |-- ShopPromotionControllerTest.php
|
||||
| |-- ShopStatusesControllerTest.php
|
||||
| |-- ShopTransportControllerTest.php
|
||||
| `-- UsersControllerTest.php
|
||||
| `-- api/
|
||||
| |-- ApiRouterTest.php
|
||||
| `-- Controllers/
|
||||
| |-- OrdersApiControllerTest.php
|
||||
| |-- ProductsApiControllerTest.php
|
||||
| `-- DictionariesApiControllerTest.php
|
||||
`-- Integration/ (puste — zarezerwowane)
|
||||
├── bootstrap.php ← autoloader + stuby (CacheHandler, S)
|
||||
└── Unit/
|
||||
└── Domain/
|
||||
├── Languages/LanguagesRepositoryTest.php
|
||||
├── Settings/SettingsRepositoryTest.php
|
||||
└── User/UserRepositoryTest.php
|
||||
```
|
||||
|
||||
## Dodawanie nowych testow
|
||||
## Stuby (bootstrap.php)
|
||||
|
||||
1. Plik w `tests/Unit/Domain/<Module>/<Class>Test.php`, `tests/Unit/admin/Controllers/<Class>Test.php` lub `tests/Unit/api/Controllers/<Class>Test.php`.
|
||||
- `\Shared\Cache\CacheHandler` — in-memory stub z `fetch()`/`store()`/`delete()`/`reset()`
|
||||
- `\S` — stub z `delete_cache()`, `htacces()`, `get_domain()`, `send_email()`
|
||||
- `medoo` — mockowany przez PHPUnit (`$this->createMock(\medoo::class)`)
|
||||
|
||||
## Dodawanie nowych testów
|
||||
|
||||
1. Plik w `tests/Unit/Domain/<Modul>/<Klasa>Test.php`.
|
||||
2. Rozszerz `PHPUnit\Framework\TestCase`.
|
||||
3. Nazwy metod zaczynaj od `test`.
|
||||
4. Wzorzec AAA: Arrange, Act, Assert.
|
||||
@@ -108,19 +56,11 @@ tests/
|
||||
## Mockowanie Medoo
|
||||
|
||||
```php
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->method('get')->willReturn(42);
|
||||
$db = $this->createMock(\medoo::class);
|
||||
$db->method('get')->willReturn(['id' => 1]);
|
||||
|
||||
$repo = new ProductRepository($mockDb);
|
||||
$value = $repo->getQuantity(123);
|
||||
$repo = new SettingsRepository($db);
|
||||
$value = $repo->visitCounter();
|
||||
|
||||
$this->assertEquals(42, $value);
|
||||
$this->assertSame('1', $value);
|
||||
```
|
||||
|
||||
## Bootstrap — stuby
|
||||
|
||||
`tests/bootstrap.php` rejestruje autoloader i definiuje stuby:
|
||||
- `Redis`, `RedisConnection` — klasy Redis (aby nie wymagac rozszerzenia)
|
||||
- `Shared\Cache\CacheHandler` — inline stub z `get()`/`set()`/`exists()`/`delete()`/`deletePattern()`
|
||||
- `Shared\Helpers\Helpers` — z `tests/stubs/Helpers.php`
|
||||
- `shop\Product` — z `tests/stubs/ShopProduct.php`
|
||||
|
||||
113
docs/plans/2026-02-28-update-channels-design.md
Normal file
113
docs/plans/2026-02-28-update-channels-design.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Design: Dwukanałowy system aktualizacji + zarządzanie licencjami
|
||||
|
||||
**Data:** 2026-02-28
|
||||
**Status:** Zatwierdzony
|
||||
|
||||
## Kontekst
|
||||
|
||||
cmsPRO jest używany przez wielu klientów. Celem jest wprowadzenie dwustopniowej dystrybucji aktualizacji:
|
||||
- Kanał **beta** — tylko wybrane testowe instalacje (1–2 strony dewelopera)
|
||||
- Kanał **stable** — wszyscy pozostali klienci
|
||||
|
||||
Zarządzanie odbywa się z panelu admina na `cmspro.project-dc.pl` (ten sam serwer i DB co serwer aktualizacji).
|
||||
|
||||
**Kluczowe ograniczenie:** nowy moduł admina oraz nowe tabele DB nigdy nie trafiają do paczek aktualizacyjnych wysyłanych klientom.
|
||||
|
||||
---
|
||||
|
||||
## Sekcja 1: Baza danych
|
||||
|
||||
Dwie nowe tabele tworzone **ręcznie tylko na serwerze dewelopera**. Nie mogą pojawić się w żadnym SQL migracyjnym dla klientów.
|
||||
|
||||
```sql
|
||||
CREATE TABLE pp_update_licenses (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`key` VARCHAR(64) NOT NULL UNIQUE,
|
||||
domain VARCHAR(255) NOT NULL,
|
||||
valid_to_date DATE NULL,
|
||||
valid_to_version VARCHAR(10) NULL,
|
||||
beta TINYINT(1) NOT NULL DEFAULT 0,
|
||||
note TEXT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE pp_update_versions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
version VARCHAR(10) NOT NULL UNIQUE,
|
||||
channel ENUM('beta','stable') NOT NULL DEFAULT 'beta',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
promoted_at DATETIME NULL
|
||||
);
|
||||
```
|
||||
|
||||
Dane z `updates/versions.php` (tablica `$license`) migrujemy jednorazowo do `pp_update_licenses`.
|
||||
|
||||
---
|
||||
|
||||
## Sekcja 2: Plik `updates/versions.php`
|
||||
|
||||
Plik zostaje w tym samym miejscu, zmienia się tylko logika (hardkodowane tablice zastępowane odczytem z DB).
|
||||
|
||||
### Nowy przepływ:
|
||||
|
||||
1. `require_once '../config.php'` → dostęp do `$mdb` (Medoo)
|
||||
2. Skan filesystem — lista istniejących ZIPów (jak dotychczas)
|
||||
3. **Auto-discovery:** każdy ZIP nieobecny w `pp_update_versions` → `INSERT IGNORE` z `channel='beta'`. Wrzucenie nowego ZIPa na serwer automatycznie rejestruje go jako beta — bez zmian w `build-update.ps1`.
|
||||
4. Walidacja klucza z `pp_update_licenses` (zamiast `$license[...]`)
|
||||
5. Filtrowanie wersji według kanału:
|
||||
- klient z `beta=1` → wersje gdzie `channel IN ('beta','stable')`
|
||||
- klient z `beta=0` → tylko `channel='stable'`
|
||||
6. Dalsze filtrowanie po `valid_to_date` i `valid_to_version` (bez zmian)
|
||||
7. Wypisanie listy wersji (jak dotychczas)
|
||||
|
||||
---
|
||||
|
||||
## Sekcja 3: Nowy moduł admina `Releases`
|
||||
|
||||
Nowa nazwa `Releases` (odróżnienie od istniejącego modułu `Update` używanego przez klientów do aktualizacji własnej instalacji).
|
||||
|
||||
### Zakładka "Wersje"
|
||||
|
||||
- Tabela z `pp_update_versions` + status istnienia ZIPa na dysku
|
||||
- Kolumny: wersja, kanał, data dodania, data promocji
|
||||
- Akcje: **Promuj do stable** / **Cofnij do beta**
|
||||
|
||||
### Zakładka "Licencje"
|
||||
|
||||
- Tabela z `pp_update_licenses`
|
||||
- Kolumny: domena, klucz (skrócony hash), ważna do daty, ważna do wersji, beta (toggle), notatka
|
||||
- Pełny CRUD: dodaj / edytuj / usuń
|
||||
- Szybki toggle flagi `beta` bezpośrednio z listy
|
||||
|
||||
### Pliki modułu
|
||||
|
||||
Wszystkie wykluczone z paczek ZIP dla klientów:
|
||||
|
||||
```
|
||||
autoload/admin/controls/class.Releases.php
|
||||
autoload/admin/factory/class.Releases.php
|
||||
autoload/admin/view/class.Releases.php
|
||||
admin/templates/releases/main-view.php
|
||||
admin/templates/releases/licenses-view.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sekcja 4: Wykluczenie z aktualizacji klientów
|
||||
|
||||
### Pliki PHP
|
||||
W `build-update.ps1` — dodanie powyższych ścieżek do listy wykluczeń.
|
||||
|
||||
### Tabele DB
|
||||
`pp_update_licenses` i `pp_update_versions` **nigdy** nie pojawiają się w:
|
||||
- plikach `_sql.txt` dołączanych do paczek
|
||||
- sekcji `sql` w plikach `_manifest.json`
|
||||
|
||||
Tabele tworzone jednorazowo ręcznie na serwerze dewelopera.
|
||||
|
||||
---
|
||||
|
||||
## Migracja danych
|
||||
|
||||
Jednorazowy skrypt PHP (uruchamiany ręcznie na serwerze) przenosi dane z `$license[...]` z `versions.php` do `pp_update_licenses`. Po migracji tablica `$license` w `versions.php` jest usuwana.
|
||||
766
docs/plans/2026-02-28-update-channels.md
Normal file
766
docs/plans/2026-02-28-update-channels.md
Normal file
@@ -0,0 +1,766 @@
|
||||
# Releases Module Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Wdrożyć dwukanałowy system aktualizacji (beta/stable) z zarządzaniem licencjami w panelu admina.
|
||||
|
||||
**Architecture:** Dwie nowe tabele MySQL (`pp_update_licenses`, `pp_update_versions`) dostępne tylko na serwerze dewelopera. `updates/versions.php` czyta z DB przez Medoo. Nowy moduł `Releases` w panelu admina zarządza wersjami i licencjami. Całość wykluczona z paczek klientów przez `.updateignore`.
|
||||
|
||||
**Tech Stack:** PHP 8.x, Medoo 1.x (`$mdb` global), jQuery UI (dialog), Bootstrap 3 (klasy CSS), `\Tpl` (admin templates z `admin/templates/`), `\S::get()` (POST→GET params).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Dodaj wykluczenia do `.updateignore`
|
||||
|
||||
**Files:**
|
||||
- Modify: `.updateignore`
|
||||
|
||||
**Step 1: Dopisz nowe wykluczenia**
|
||||
|
||||
Otwórz `.updateignore` i dopisz na końcu:
|
||||
|
||||
```
|
||||
# Moduł zarządzania releaseami (tylko serwer dewelopera)
|
||||
autoload/admin/controls/class.Releases.php
|
||||
autoload/admin/factory/class.Releases.php
|
||||
autoload/admin/view/class.Releases.php
|
||||
admin/templates/releases/
|
||||
|
||||
# Menu dewelopera
|
||||
templates/additional-menu.php
|
||||
```
|
||||
|
||||
**Step 2: Zweryfikuj manualnie**
|
||||
|
||||
Uruchom build w trybie dry-run i sprawdź, że powyższe pliki NIE pojawiają się na liście:
|
||||
```powershell
|
||||
./build-update.ps1 -ToTag v9.999 -DryRun
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .updateignore
|
||||
git commit -m "chore: wyklucz modul Releases i menu dewelopera z paczek klientow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Utwórz tabele DB (jednorazowo na serwerze)
|
||||
|
||||
**Files:**
|
||||
- Create: `_db_releases_setup.sql` (uruchom raz w phpMyAdmin, nie commituj)
|
||||
|
||||
**Step 1: Utwórz plik SQL**
|
||||
|
||||
```sql
|
||||
CREATE TABLE pp_update_licenses (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`key` VARCHAR(64) NOT NULL UNIQUE,
|
||||
domain VARCHAR(255) NOT NULL,
|
||||
valid_to_date DATE NULL,
|
||||
valid_to_version VARCHAR(10) NULL,
|
||||
beta TINYINT(1) NOT NULL DEFAULT 0,
|
||||
note TEXT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE pp_update_versions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
version VARCHAR(10) NOT NULL UNIQUE,
|
||||
channel ENUM('beta','stable') NOT NULL DEFAULT 'beta',
|
||||
created_at DATETIME NULL,
|
||||
promoted_at DATETIME NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
**Step 2: Uruchom w phpMyAdmin na serwerze `cmspro.project-dc.pl`**
|
||||
|
||||
Wklej SQL do phpMyAdmin → Execute. Obie tabele muszą być widoczne.
|
||||
|
||||
**Step 3: Nie commituj** — ten plik jest tylko pomocniczy, możesz go usunąć po wykonaniu.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Utwórz `admin\factory\Releases`
|
||||
|
||||
**Files:**
|
||||
- Create: `autoload/admin/factory/class.Releases.php`
|
||||
|
||||
**Step 1: Napisz klasę**
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace admin\factory;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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]
|
||||
);
|
||||
}
|
||||
|
||||
public static function demote(string $version): void
|
||||
{
|
||||
global $mdb;
|
||||
$mdb->update('pp_update_versions',
|
||||
['channel' => 'beta', 'promoted_at' => null],
|
||||
['version' => $version]
|
||||
);
|
||||
}
|
||||
|
||||
public static function get_licenses(): array
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb->select('pp_update_licenses', '*', ['ORDER' => ['domain' => 'ASC']]) ?: [];
|
||||
}
|
||||
|
||||
public static function get_license(int $id): array
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb->get('pp_update_licenses', '*', ['id' => $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);
|
||||
}
|
||||
|
||||
public static function delete_license(int $id): void
|
||||
{
|
||||
global $mdb;
|
||||
$mdb->delete('pp_update_licenses', ['id' => $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';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Weryfikacja składni**
|
||||
|
||||
```bash
|
||||
php -l autoload/admin/factory/class.Releases.php
|
||||
```
|
||||
Oczekiwane: `No syntax errors detected`
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Utwórz `admin\controls\Releases`
|
||||
|
||||
**Files:**
|
||||
- Create: `autoload/admin/controls/class.Releases.php`
|
||||
|
||||
**Step 1: Napisz klasę**
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace admin\controls;
|
||||
|
||||
class Releases
|
||||
{
|
||||
public static function main_view(): string
|
||||
{
|
||||
return \admin\view\Releases::main_view();
|
||||
}
|
||||
|
||||
public static function promote(): void
|
||||
{
|
||||
$version = trim(\S::get('version'));
|
||||
if ($version)
|
||||
\admin\factory\Releases::promote($version);
|
||||
header('Location: /admin/releases/main_view/');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function demote(): void
|
||||
{
|
||||
$version = trim(\S::get('version'));
|
||||
if ($version)
|
||||
\admin\factory\Releases::demote($version);
|
||||
header('Location: /admin/releases/main_view/');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function save_license(): void
|
||||
{
|
||||
\admin\factory\Releases::save_license($_POST);
|
||||
\S::set_message('Licencja została zapisana.');
|
||||
header('Location: /admin/releases/main_view/');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function delete_license(): void
|
||||
{
|
||||
$id = (int)\S::get('id');
|
||||
if ($id)
|
||||
\admin\factory\Releases::delete_license($id);
|
||||
header('Location: /admin/releases/main_view/');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function toggle_beta(): void
|
||||
{
|
||||
$id = (int)\S::get('id');
|
||||
if ($id)
|
||||
\admin\factory\Releases::toggle_beta($id);
|
||||
header('Location: /admin/releases/main_view/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Weryfikacja składni**
|
||||
|
||||
```bash
|
||||
php -l autoload/admin/controls/class.Releases.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Utwórz `admin\view\Releases`
|
||||
|
||||
**Files:**
|
||||
- Create: `autoload/admin/view/class.Releases.php`
|
||||
|
||||
**Step 1: Napisz klasę**
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace admin\view;
|
||||
|
||||
class Releases
|
||||
{
|
||||
public static function main_view(): string
|
||||
{
|
||||
$tpl = new \Tpl;
|
||||
$tpl->versions = \admin\factory\Releases::get_versions();
|
||||
$tpl->licenses = \admin\factory\Releases::get_licenses();
|
||||
return $tpl->render('releases/main-view');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Weryfikacja składni**
|
||||
|
||||
```bash
|
||||
php -l autoload/admin/view/class.Releases.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Utwórz szablon `admin/templates/releases/main-view.php`
|
||||
|
||||
**Files:**
|
||||
- Create: `admin/templates/releases/main-view.php`
|
||||
|
||||
**Step 1: Utwórz katalog i plik**
|
||||
|
||||
```php
|
||||
<?php global $user; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
|
||||
<!-- TABS NAV -->
|
||||
<ul class="nav nav-tabs releases-tabs" style="margin-bottom:20px">
|
||||
<li class="active"><a href="#" data-tab="versions">Wersje</a></li>
|
||||
<li><a href="#" data-tab="licenses">Licencje</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- TAB: WERSJE -->
|
||||
<div id="tab-versions">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h3 class="panel-title">Wersje</h3></div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Wersja</th><th>Kanał</th><th>Dodana</th>
|
||||
<th>Promocja do stable</th><th>ZIP</th><th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<? foreach ($this->versions as $v): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($v['version']) ?></strong></td>
|
||||
<td>
|
||||
<? if ($v['channel'] == 'stable'): ?>
|
||||
<span class="label label-success">stable</span>
|
||||
<? else: ?>
|
||||
<span class="label label-warning">beta</span>
|
||||
<? endif; ?>
|
||||
</td>
|
||||
<td><?= $v['created_at'] ? substr($v['created_at'], 0, 10) : '-' ?></td>
|
||||
<td><?= $v['promoted_at'] ? substr($v['promoted_at'], 0, 10) : '-' ?></td>
|
||||
<td><?= $v['zip_exists'] ? '<span class="text-success">✓</span>' : '<span class="text-danger">✗</span>' ?></td>
|
||||
<td>
|
||||
<? if ($v['channel'] == 'beta'): ?>
|
||||
<a href="/admin/releases/promote/?version=<?= urlencode($v['version']) ?>"
|
||||
class="btn btn-xs btn-success"
|
||||
onclick="return confirm('Promować <?= $v['version'] ?> do stable?')">
|
||||
Promuj →stable
|
||||
</a>
|
||||
<? else: ?>
|
||||
<a href="/admin/releases/demote/?version=<?= urlencode($v['version']) ?>"
|
||||
class="btn btn-xs btn-default"
|
||||
onclick="return confirm('Cofnąć <?= $v['version'] ?> do beta?')">
|
||||
Cofnij →beta
|
||||
</a>
|
||||
<? endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<? endforeach; ?>
|
||||
<? if (!$this->versions): ?>
|
||||
<tr><td colspan="6" class="text-center text-muted">Brak wersji w bazie. Wersje będą rejestrowane automatycznie przy pierwszym odpytaniu versions.php.</td></tr>
|
||||
<? endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /tab-versions -->
|
||||
|
||||
<!-- TAB: LICENCJE -->
|
||||
<div id="tab-licenses" style="display:none">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<h3 class="panel-title">Licencje</h3>
|
||||
<button class="btn btn-sm btn-system" id="btn-add-license">+ Dodaj licencję</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<!-- Formularz dodawania/edycji (ukryty) -->
|
||||
<div id="license-form-box" style="display:none;border:1px solid #ddd;padding:15px;margin-bottom:20px;background:#f9f9f9">
|
||||
<h4 id="license-form-title">Nowa licencja</h4>
|
||||
<form method="post" action="/admin/releases/save_license/">
|
||||
<input type="hidden" name="id" id="f-id" value="">
|
||||
<div class="row">
|
||||
<div class="form-group col-lg-3">
|
||||
<label>Klucz (hash)</label>
|
||||
<input type="text" name="key" id="f-key" class="form-control" placeholder="md5/hash lub pusty dla domyślnego">
|
||||
</div>
|
||||
<div class="form-group col-lg-3">
|
||||
<label>Domena</label>
|
||||
<input type="text" name="domain" id="f-domain" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group col-lg-2">
|
||||
<label>Ważna do daty</label>
|
||||
<input type="date" name="valid_to_date" id="f-valid-date" class="form-control">
|
||||
</div>
|
||||
<div class="form-group col-lg-2">
|
||||
<label>Ważna do wersji</label>
|
||||
<input type="text" name="valid_to_version" id="f-valid-ver" class="form-control" placeholder="np. 1.618">
|
||||
</div>
|
||||
<div class="form-group col-lg-1">
|
||||
<label>Beta</label><br>
|
||||
<input type="checkbox" name="beta" id="f-beta" value="1">
|
||||
</div>
|
||||
<div class="form-group col-lg-2">
|
||||
<label>Notatka</label>
|
||||
<input type="text" name="note" id="f-note" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-system btn-sm">Zapisz</button>
|
||||
<button type="button" class="btn btn-default btn-sm" id="btn-cancel-license">Anuluj</button>
|
||||
</form>
|
||||
</div><!-- /license-form-box -->
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domena</th><th>Klucz</th><th>Do daty</th>
|
||||
<th>Do wersji</th><th>Beta</th><th>Notatka</th><th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<? foreach ($this->licenses as $lic): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($lic['domain']) ?></td>
|
||||
<td><code title="<?= htmlspecialchars($lic['key']) ?>">
|
||||
<?= $lic['key'] === '' ? '<em>(domyślny)</em>' : htmlspecialchars(substr($lic['key'], 0, 8)) . '…' ?>
|
||||
</code></td>
|
||||
<td><?= $lic['valid_to_date'] ?: '<em>∞</em>' ?></td>
|
||||
<td><?= $lic['valid_to_version'] ?: '<em>∞</em>' ?></td>
|
||||
<td>
|
||||
<a href="/admin/releases/toggle_beta/?id=<?= $lic['id'] ?>"
|
||||
class="label <?= $lic['beta'] ? 'label-info' : 'label-default' ?>"
|
||||
title="Kliknij aby przełączyć">
|
||||
<?= $lic['beta'] ? 'beta' : 'stable' ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($lic['note'] ?? '') ?></td>
|
||||
<td>
|
||||
<button class="btn btn-xs btn-default btn-edit-license"
|
||||
data-id="<?= $lic['id'] ?>"
|
||||
data-key="<?= htmlspecialchars($lic['key']) ?>"
|
||||
data-domain="<?= htmlspecialchars($lic['domain']) ?>"
|
||||
data-valid-date="<?= $lic['valid_to_date'] ?? '' ?>"
|
||||
data-valid-ver="<?= $lic['valid_to_version'] ?? '' ?>"
|
||||
data-beta="<?= $lic['beta'] ?>"
|
||||
data-note="<?= htmlspecialchars($lic['note'] ?? '') ?>">Edytuj</button>
|
||||
<a href="/admin/releases/delete_license/?id=<?= $lic['id'] ?>"
|
||||
class="btn btn-xs btn-danger"
|
||||
onclick="return confirm('Usunąć licencję <?= htmlspecialchars($lic['domain']) ?>?')">Usuń</a>
|
||||
</td>
|
||||
</tr>
|
||||
<? endforeach; ?>
|
||||
<? if (!$this->licenses): ?>
|
||||
<tr><td colspan="7" class="text-center text-muted">Brak licencji. Dodaj pierwszą lub uruchom skrypt migracji.</td></tr>
|
||||
<? endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /tab-licenses -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Przełączanie tabów
|
||||
$('.releases-tabs a').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('.releases-tabs li').removeClass('active');
|
||||
$(this).parent().addClass('active');
|
||||
$('[id^="tab-"]').hide();
|
||||
$('#tab-' + $(this).data('tab')).show();
|
||||
});
|
||||
|
||||
// Formularz: Dodaj nową licencję
|
||||
$('#btn-add-license').on('click', function() {
|
||||
$('#license-form-title').text('Nowa licencja');
|
||||
$('#f-id').val('');
|
||||
$('#f-key, #f-domain, #f-valid-date, #f-valid-ver, #f-note').val('');
|
||||
$('#f-beta').prop('checked', false);
|
||||
$('#license-form-box').slideDown();
|
||||
});
|
||||
|
||||
// Formularz: Edytuj istniejącą licencję
|
||||
$('.btn-edit-license').on('click', function() {
|
||||
var d = $(this).data();
|
||||
$('#license-form-title').text('Edytuj licencję: ' + d.domain);
|
||||
$('#f-id').val(d.id);
|
||||
$('#f-key').val(d.key);
|
||||
$('#f-domain').val(d.domain);
|
||||
$('#f-valid-date').val(d.validDate);
|
||||
$('#f-valid-ver').val(d.validVer);
|
||||
$('#f-beta').prop('checked', d.beta == 1);
|
||||
$('#f-note').val(d.note);
|
||||
$('#license-form-box').slideDown();
|
||||
$('html, body').animate({ scrollTop: $('#license-form-box').offset().top - 20 }, 300);
|
||||
});
|
||||
|
||||
// Anuluj formularz
|
||||
$('#btn-cancel-license').on('click', function() {
|
||||
$('#license-form-box').slideUp();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
**Step 2: Zweryfikuj, że szablon jest poprawny**
|
||||
|
||||
Otwórz w przeglądarce: `https://cmspro.project-dc.pl/admin/releases/main_view/`
|
||||
|
||||
Oczekiwane: strona ładuje się bez błędów PHP, widoczne dwa taby, tabela wersji pusta lub z danymi.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Dodaj pozycję menu dla dewelopera
|
||||
|
||||
**Files:**
|
||||
- Create: `templates/additional-menu.php` (ten plik nie trafia do klientów — dodany do `.updateignore` w Task 1)
|
||||
|
||||
**Step 1: Utwórz plik**
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Menu tylko na serwerze dewelopera — wykluczone z .updateignore
|
||||
?>
|
||||
<div class="title">Developer</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/admin/releases/main_view/">
|
||||
<img src="/admin/css/icons/settings-20-filled.svg">Releases & Licencje
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Step 2: Sprawdź w przeglądarce**
|
||||
|
||||
Po odświeżeniu panelu admina powinna pojawić się sekcja "Developer" z linkiem "Releases & Licencje" w menu bocznym.
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Migracja danych licencji do DB
|
||||
|
||||
Licencje z hardkodowanej tablicy `$license` w `updates/versions.php` muszą trafić do `pp_update_licenses` **przed** przejściem na nową wersję `versions.php`.
|
||||
|
||||
**Files:**
|
||||
- Create: `_migrate_licenses.php` (tymczasowy, uruchom raz przez przeglądarkę, potem usuń)
|
||||
|
||||
**Step 1: Utwórz skrypt migracji**
|
||||
|
||||
```php
|
||||
<?php
|
||||
// UWAGA: Uruchom JEDEN raz przez przeglądarkę, potem natychmiast usuń!
|
||||
// URL: https://cmspro.project-dc.pl/_migrate_licenses.php
|
||||
|
||||
require_once 'config.php';
|
||||
require_once 'libraries/medoo/medoo.php';
|
||||
|
||||
$mdb = new medoo([
|
||||
'database_type' => 'mysql',
|
||||
'database_name' => $database['name'],
|
||||
'server' => $database['host'],
|
||||
'username' => $database['user'],
|
||||
'password' => $database['password'],
|
||||
'charset' => 'utf8'
|
||||
]);
|
||||
|
||||
// Wyodrębnij $license z pliku versions.php bez uruchamiania die()
|
||||
$source = file_get_contents('updates/versions.php');
|
||||
|
||||
// Wyciągnij wszystkie wpisy $license['key']['domain'] itd.
|
||||
preg_match_all('/\$license\[\'([^\']*)\'\]\[\'domain\'\]\s*=\s*\'([^\']*)\';/', $source, $m_domain);
|
||||
preg_match_all('/\$license\[\'([^\']*)\'\]\[\'valid_to_date\'\]\s*=\s*\'([^\']*)\';/', $source, $m_date);
|
||||
preg_match_all('/\$license\[\'([^\']*)\'\]\[\'valid_to_version\'\]\s*=\s*\'([^\']*)\';/', $source, $m_ver);
|
||||
|
||||
// Zbuduj mapę po kluczu
|
||||
$dates = array_combine($m_date[1], $m_date[2]);
|
||||
$vers = array_combine($m_ver[1], $m_ver[2]);
|
||||
|
||||
$count = 0;
|
||||
foreach ($m_domain[1] as $i => $key) {
|
||||
$domain = $m_domain[2][$i];
|
||||
$row = [
|
||||
'key' => $key,
|
||||
'domain' => $domain,
|
||||
'valid_to_date' => ($dates[$key] ?? '') ?: null,
|
||||
'valid_to_version' => ($vers[$key] ?? '') ?: null,
|
||||
'beta' => 0,
|
||||
];
|
||||
// Pomiń jeśli już istnieje
|
||||
if ($mdb->has('pp_update_licenses', ['key' => $key])) {
|
||||
echo "SKIP (już istnieje): $domain ($key)<br>";
|
||||
continue;
|
||||
}
|
||||
$mdb->insert('pp_update_licenses', $row);
|
||||
echo "OK: $domain ($key)<br>";
|
||||
$count++;
|
||||
}
|
||||
echo "<hr><strong>Zmigrowano $count licencji.</strong>";
|
||||
echo "<br><strong style='color:red'>USUŃ ten plik z serwera!</strong>";
|
||||
```
|
||||
|
||||
**Step 2: Wgraj na serwer i uruchom**
|
||||
|
||||
```
|
||||
https://cmspro.project-dc.pl/_migrate_licenses.php
|
||||
```
|
||||
|
||||
Oczekiwane: lista "OK: domena (klucz)" dla każdej licencji, na końcu podsumowanie.
|
||||
|
||||
**Step 3: Usuń skrypt z serwera**
|
||||
|
||||
Skasuj `_migrate_licenses.php` — nie commituj go do repozytorium.
|
||||
|
||||
**Step 4: Ustaw flagę beta dla swoich testowych stron**
|
||||
|
||||
W panelu admina `/admin/releases/main_view/` → zakładka Licencje → przy swoich testowych domenach kliknij "stable" aby przełączyć na "beta".
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Przebuduj `updates/versions.php`
|
||||
|
||||
**Files:**
|
||||
- Modify: `updates/versions.php`
|
||||
|
||||
> **WAŻNE:** Wykonaj ten krok dopiero po Task 8 (migracja danych do DB).
|
||||
|
||||
**Step 1: Zastąp całą zawartość pliku**
|
||||
|
||||
```php
|
||||
<?
|
||||
require_once '../config.php';
|
||||
require_once '../libraries/medoo/medoo.php';
|
||||
|
||||
$mdb = new medoo( [
|
||||
'database_type' => 'mysql',
|
||||
'database_name' => $database['name'],
|
||||
'server' => $database['host'],
|
||||
'username' => $database['user'],
|
||||
'password' => $database['password'],
|
||||
'charset' => 'utf8'
|
||||
] );
|
||||
|
||||
$current_ver = 1691; // aktualizowane automatycznie przez build-update.ps1
|
||||
|
||||
// 1. Skan filesystem — lista istniejących ZIPów
|
||||
$versions = [];
|
||||
for ( $i = 1; $i <= $current_ver; $i++ )
|
||||
{
|
||||
$dir = substr( number_format( $i / 1000, 3 ), 0, strlen( number_format( $i / 1000, 3 ) ) - 2 ) . '0';
|
||||
$version_old = number_format( $i / 1000, 2 );
|
||||
$version_new = number_format( $i / 1000, 3 );
|
||||
|
||||
if ( file_exists( '../updates/' . $dir . '/ver_' . $version_old . '.zip' ) )
|
||||
$versions[] = $version_old;
|
||||
|
||||
if ( file_exists( '../updates/' . $dir . '/ver_' . $version_new . '.zip' ) )
|
||||
$versions[] = $version_new;
|
||||
}
|
||||
$versions = array_unique( $versions );
|
||||
|
||||
// 2. Walidacja klucza licencji
|
||||
$license = $mdb->get( 'pp_update_licenses', '*', [ 'key' => ( $_GET['key'] ?? '' ) ] );
|
||||
if ( !$license )
|
||||
die();
|
||||
|
||||
// 3. Sprawdź ważność daty
|
||||
if ( $license['valid_to_date'] && $license['valid_to_date'] < date( 'Y-m-d' ) )
|
||||
die();
|
||||
|
||||
// 4. Auto-discovery: rejestruj nowe ZIPy jako beta
|
||||
$known = array_flip( $mdb->select( 'pp_update_versions', 'version', [] ) ?: [] );
|
||||
foreach ( $versions as $ver )
|
||||
{
|
||||
if ( !isset( $known[$ver] ) )
|
||||
{
|
||||
@$mdb->insert( 'pp_update_versions', [
|
||||
'version' => $ver,
|
||||
'channel' => 'beta',
|
||||
'created_at' => date( 'Y-m-d H:i:s' )
|
||||
] );
|
||||
$known[$ver] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
$valid_to_version = $license['valid_to_version'];
|
||||
foreach ( $versions as $ver )
|
||||
{
|
||||
if ( !isset( $allowed[$ver] ) )
|
||||
continue;
|
||||
|
||||
if ( $valid_to_version && $ver > $valid_to_version )
|
||||
continue;
|
||||
|
||||
echo $ver . PHP_EOL;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Weryfikacja składni**
|
||||
|
||||
```bash
|
||||
php -l updates/versions.php
|
||||
```
|
||||
|
||||
**Step 3: Test ręczny**
|
||||
|
||||
Otwórz w przeglądarce (podaj klucz jednej z migrowanych licencji):
|
||||
```
|
||||
https://cmspro.project-dc.pl/updates/versions.php?key=TWOJ_KLUCZ
|
||||
```
|
||||
|
||||
Oczekiwane:
|
||||
- Dla klucza z `beta=0`: zwraca TYLKO wersje `channel='stable'` (pusta lista jeśli żadna jeszcze niepromowana)
|
||||
- Dla klucza z `beta=1`: zwraca wersje `channel='beta'` i `'stable'`
|
||||
- Dla nieprawidłowego klucza: brak odpowiedzi (die())
|
||||
|
||||
**Step 4: Sprawdź panel aktualizacji u klienta**
|
||||
|
||||
Na swojej testowej stronie (beta=1) otwórz `/admin/update/main_view/` — powinien widzieć dostępne wersje.
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Commit końcowy
|
||||
|
||||
**Step 1: Sprawdź status**
|
||||
|
||||
```bash
|
||||
git status
|
||||
git diff updates/versions.php
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add \
|
||||
.updateignore \
|
||||
autoload/admin/factory/class.Releases.php \
|
||||
autoload/admin/controls/class.Releases.php \
|
||||
autoload/admin/view/class.Releases.php \
|
||||
admin/templates/releases/main-view.php \
|
||||
templates/additional-menu.php \
|
||||
updates/versions.php
|
||||
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: dwukanalowy system aktualizacji (beta/stable) + zarzadzanie licencjami
|
||||
|
||||
- Nowy modul admin\Releases: lista wersji z promocja beta→stable,
|
||||
CRUD licencji z flaga beta
|
||||
- versions.php czyta z DB (pp_update_licenses, pp_update_versions)
|
||||
zamiast hardkodowanej tablicy $license
|
||||
- Auto-discovery: nowe ZIPy automatycznie rejestrowane jako 'beta'
|
||||
- Calosc wykluczona z paczek klientow przez .updateignore
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Weryfikacja end-to-end
|
||||
|
||||
Po wdrożeniu sprawdź ręcznie:
|
||||
|
||||
1. **Nowy ZIP (beta):** Wrzuć nowy ZIP na serwer → odpytaj `versions.php` kluczem beta → pojawia się nowa wersja → w panelu admina widoczna jako `beta`
|
||||
2. **Promocja:** Kliknij "Promuj →stable" → odpytaj kluczem `beta=0` → wersja pojawia się na liście
|
||||
3. **Cofnięcie:** Kliknij "Cofnij →beta" → klient `beta=0` nie widzi wersji
|
||||
4. **Licencja:** Dodaj nową licencję przez formularz → weryfikuj w phpMyAdmin
|
||||
5. **Toggle beta:** Kliknij "stable" przy licencji → zmienia się na "beta"
|
||||
6. **Zabezpieczenie:** Wgraj nową wersję CMS do klienta → sprawdź czy `admin/templates/releases/` i `autoload/admin/*/class.Releases.php` NIE znalazły się w ZIPie (build dry-run)
|
||||
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';
|
||||
|
||||
14
phpunit.xml
Normal file
14
phpunit.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="tests/bootstrap.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>autoload/Domain</directory>
|
||||
<directory>autoload/Shared</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
123
tests/Unit/Domain/Languages/LanguagesRepositoryTest.php
Normal file
123
tests/Unit/Domain/Languages/LanguagesRepositoryTest.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?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();
|
||||
}
|
||||
|
||||
// --- languagesList ---
|
||||
|
||||
public function testLanguagesListReturnsArray(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('select')->willReturn([['id' => 'pl', 'name' => 'Polski']]);
|
||||
|
||||
$repo = new LanguagesRepository($db);
|
||||
$this->assertSame([['id' => 'pl', 'name' => 'Polski']], $repo->languagesList());
|
||||
}
|
||||
|
||||
public function testLanguagesListReturnsEmptyWhenNull(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('select')->willReturn(null);
|
||||
|
||||
$repo = new LanguagesRepository($db);
|
||||
$this->assertSame([], $repo->languagesList());
|
||||
}
|
||||
|
||||
// --- languageDetails ---
|
||||
|
||||
public function testLanguageDetailsReturnsRowWhenFound(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn(['id' => 'pl', 'name' => 'Polski']);
|
||||
|
||||
$repo = new LanguagesRepository($db);
|
||||
$this->assertSame('pl', $repo->languageDetails('pl')['id']);
|
||||
}
|
||||
|
||||
public function testLanguageDetailsReturnsNullWhenNotFound(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn(null);
|
||||
|
||||
$repo = new LanguagesRepository($db);
|
||||
$this->assertNull($repo->languageDetails('xx'));
|
||||
}
|
||||
|
||||
// --- activeLanguages ---
|
||||
|
||||
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());
|
||||
// Drugi odczyt — z cache (mock select nie zostanie wywołany drugi raz)
|
||||
$this->assertSame($expected, $repo->activeLanguages());
|
||||
}
|
||||
|
||||
public function testActiveLanguagesReturnsEmptyWhenNull(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('select')->willReturn(null);
|
||||
|
||||
$repo = new LanguagesRepository($db);
|
||||
$this->assertSame([], $repo->activeLanguages());
|
||||
}
|
||||
|
||||
// --- maxOrder ---
|
||||
|
||||
public function testMaxOrderReturnsInteger(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('max')->willReturn('5');
|
||||
|
||||
$repo = new LanguagesRepository($db);
|
||||
$this->assertSame(5, $repo->maxOrder());
|
||||
}
|
||||
|
||||
// --- translationDelete ---
|
||||
|
||||
public function testTranslationDeleteReturnsTrueOnSuccess(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('delete')->willReturn(1);
|
||||
|
||||
$repo = new LanguagesRepository($db);
|
||||
$this->assertTrue($repo->translationDelete(1));
|
||||
}
|
||||
|
||||
public function testTranslationDeleteReturnsFalseOnFailure(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('delete')->willReturn(0);
|
||||
|
||||
$repo = new LanguagesRepository($db);
|
||||
$this->assertFalse($repo->translationDelete(1));
|
||||
}
|
||||
|
||||
// --- translationDetails ---
|
||||
|
||||
public function testTranslationDetailsReturnsRowOrNull(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn(['id' => 1, 'text' => 'hello']);
|
||||
|
||||
$repo = new LanguagesRepository($db);
|
||||
$this->assertSame(['id' => 1, 'text' => 'hello'], $repo->translationDetails(1));
|
||||
}
|
||||
}
|
||||
101
tests/Unit/Domain/Settings/SettingsRepositoryTest.php
Normal file
101
tests/Unit/Domain/Settings/SettingsRepositoryTest.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
namespace Tests\Unit\Domain\Settings;
|
||||
|
||||
use Domain\Settings\SettingsRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SettingsRepositoryTest extends TestCase
|
||||
{
|
||||
private function mockDb(): object
|
||||
{
|
||||
return $this->createMock(\medoo::class);
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
\Shared\Cache\CacheHandler::reset();
|
||||
}
|
||||
|
||||
// --- allSettings ---
|
||||
|
||||
public function testAllSettingsReturnsMappedArray(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('select')->willReturn([
|
||||
['param' => 'site_name', 'value' => 'Test CMS'],
|
||||
['param' => 'email', 'value' => 'admin@test.pl'],
|
||||
]);
|
||||
|
||||
$repo = new SettingsRepository($db);
|
||||
$result = $repo->allSettings();
|
||||
|
||||
$this->assertSame('Test CMS', $result['site_name']);
|
||||
$this->assertSame('admin@test.pl', $result['email']);
|
||||
}
|
||||
|
||||
public function testAllSettingsReturnsEmptyArrayWhenDbReturnsNull(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('select')->willReturn(null);
|
||||
|
||||
$repo = new SettingsRepository($db);
|
||||
$this->assertSame([], $repo->allSettings());
|
||||
}
|
||||
|
||||
public function testAllSettingsUsesCache(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->expects($this->never())->method('select');
|
||||
|
||||
\Shared\Cache\CacheHandler::store('settings_details', ['cached' => '1']);
|
||||
|
||||
$repo = new SettingsRepository($db);
|
||||
$result = $repo->allSettings();
|
||||
|
||||
$this->assertSame('1', $result['cached']);
|
||||
}
|
||||
|
||||
// --- update ---
|
||||
|
||||
public function testUpdateCallsDbUpdateWhenParamExists(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('count')->willReturn(1);
|
||||
$db->expects($this->once())->method('update')->willReturn(true);
|
||||
$db->expects($this->never())->method('insert');
|
||||
|
||||
$repo = new SettingsRepository($db);
|
||||
$this->assertTrue($repo->update('site_name', 'Nowa Nazwa'));
|
||||
}
|
||||
|
||||
public function testUpdateCallsDbInsertWhenParamMissing(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('count')->willReturn(0);
|
||||
$db->expects($this->once())->method('insert')->willReturn(true);
|
||||
$db->expects($this->never())->method('update');
|
||||
|
||||
$repo = new SettingsRepository($db);
|
||||
$repo->update('new_param', 'value');
|
||||
}
|
||||
|
||||
// --- visitCounter ---
|
||||
|
||||
public function testVisitCounterReturnsValue(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn('1234');
|
||||
|
||||
$repo = new SettingsRepository($db);
|
||||
$this->assertSame('1234', $repo->visitCounter());
|
||||
}
|
||||
|
||||
public function testVisitCounterReturnsNullWhenEmpty(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn(null);
|
||||
|
||||
$repo = new SettingsRepository($db);
|
||||
$this->assertNull($repo->visitCounter());
|
||||
}
|
||||
}
|
||||
252
tests/Unit/Domain/User/UserRepositoryTest.php
Normal file
252
tests/Unit/Domain/User/UserRepositoryTest.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
namespace Tests\Unit\Domain\User;
|
||||
|
||||
use Domain\User\UserRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserRepositoryTest extends TestCase
|
||||
{
|
||||
private function mockDb(): object
|
||||
{
|
||||
return $this->createMock(\medoo::class);
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
\Shared\Cache\CacheHandler::reset();
|
||||
}
|
||||
|
||||
// --- find ---
|
||||
|
||||
public function testFindReturnsUserArray(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn(['id' => 1, 'login' => 'admin']);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertSame('admin', $repo->find(1)['login']);
|
||||
}
|
||||
|
||||
public function testFindReturnsNullWhenNotFound(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn(null);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertNull($repo->find(99));
|
||||
}
|
||||
|
||||
// --- findByLogin ---
|
||||
|
||||
public function testFindByLoginReturnsUser(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn(['id' => 1, 'login' => 'admin']);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertNotNull($repo->findByLogin('admin'));
|
||||
}
|
||||
|
||||
// --- all ---
|
||||
|
||||
public function testAllReturnsArray(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('select')->willReturn([['id' => 1], ['id' => 2]]);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertCount(2, $repo->all());
|
||||
}
|
||||
|
||||
public function testAllReturnsEmptyArrayWhenNull(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('select')->willReturn(null);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertSame([], $repo->all());
|
||||
}
|
||||
|
||||
// --- hasPrivilege ---
|
||||
|
||||
public function testHasPrivilegeReturnsTrueForAdminUser(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$repo = new UserRepository($db);
|
||||
// userId === 1 zawsze ma uprawnienia, bez zapytania do DB
|
||||
$db->expects($this->never())->method('count');
|
||||
$this->assertTrue($repo->hasPrivilege('articles', 1));
|
||||
}
|
||||
|
||||
public function testHasPrivilegeReturnsTrueWhenPrivilegeExists(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('count')->willReturn(1);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertTrue($repo->hasPrivilege('articles', 2));
|
||||
}
|
||||
|
||||
public function testHasPrivilegeReturnsFalseWhenPrivilegeMissing(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('count')->willReturn(0);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertFalse($repo->hasPrivilege('articles', 2));
|
||||
}
|
||||
|
||||
// --- logon ---
|
||||
|
||||
public function testLogonReturnsZeroWhenUserNotFound(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
// Pierwsze get() (sprawdź czy login istnieje) → null
|
||||
$db->method('get')->willReturn(null);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertSame(0, $repo->logon('unknown', 'pass'));
|
||||
}
|
||||
|
||||
public function testLogonReturnsMinusOneWhenAccountBlocked(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
// Pierwsze get() → użytkownik istnieje, drugie → konto zablokowane (null)
|
||||
$db->method('get')->willReturnOnConsecutiveCalls(
|
||||
['id' => 2, 'login' => 'user'],
|
||||
null
|
||||
);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertSame(-1, $repo->logon('user', 'pass'));
|
||||
}
|
||||
|
||||
public function testLogonReturnsOneOnSuccess(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturnOnConsecutiveCalls(
|
||||
['id' => 2, 'login' => 'user'], // login istnieje
|
||||
['id' => 2, 'status' => 1, 'error_logged_count' => 0], // nie zablokowany
|
||||
['id' => 2] // hasło poprawne
|
||||
);
|
||||
$db->method('update')->willReturn(true);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertSame(1, $repo->logon('user', 'pass'));
|
||||
}
|
||||
|
||||
// --- isLoginTaken ---
|
||||
|
||||
public function testIsLoginTakenReturnsTrueWhenExists(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn('user');
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertTrue($repo->isLoginTaken('user'));
|
||||
}
|
||||
|
||||
public function testIsLoginTakenReturnsFalseWhenFree(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn(null);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertFalse($repo->isLoginTaken('newuser'));
|
||||
}
|
||||
|
||||
// --- verifyTwofaCode ---
|
||||
|
||||
public function testVerifyTwofaCodeReturnsFalseWhenUserNotFound(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('get')->willReturn(null);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertFalse($repo->verifyTwofaCode(1, '123456'));
|
||||
}
|
||||
|
||||
public function testVerifyTwofaCodeReturnsFalseWhenTooManyFailedAttempts(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$user = [
|
||||
'id' => 2,
|
||||
'twofa_failed_attempts' => 5,
|
||||
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 600),
|
||||
'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
|
||||
];
|
||||
$db->method('get')->willReturn($user);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertFalse($repo->verifyTwofaCode(2, '123456'));
|
||||
}
|
||||
|
||||
public function testVerifyTwofaCodeReturnsFalseWhenExpired(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$user = [
|
||||
'id' => 2,
|
||||
'twofa_failed_attempts' => 0,
|
||||
'twofa_expires_at' => date('Y-m-d H:i:s', time() - 1),
|
||||
'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
|
||||
];
|
||||
$db->method('get')->willReturn($user);
|
||||
$db->method('update')->willReturn(true);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertFalse($repo->verifyTwofaCode(2, '123456'));
|
||||
}
|
||||
|
||||
public function testVerifyTwofaCodeReturnsTrueOnValidCode(): void
|
||||
{
|
||||
$code = '123456';
|
||||
$db = $this->mockDb();
|
||||
$user = [
|
||||
'id' => 2,
|
||||
'twofa_failed_attempts' => 0,
|
||||
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 600),
|
||||
'twofa_code_hash' => password_hash($code, PASSWORD_DEFAULT),
|
||||
];
|
||||
// find() wywołuje get() dwa razy (raz przez verifyTwofaCode, raz przez update)
|
||||
$db->method('get')->willReturn($user);
|
||||
$db->method('update')->willReturn(true);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertTrue($repo->verifyTwofaCode(2, $code));
|
||||
}
|
||||
|
||||
// --- delete ---
|
||||
|
||||
public function testDeleteReturnsTrueOnSuccess(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('delete')->willReturn(1);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$this->assertTrue($repo->delete(2));
|
||||
}
|
||||
|
||||
// --- save — walidacja ---
|
||||
|
||||
public function testSaveReturnsErrorWhenPasswordTooShort(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('delete')->willReturn(1);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$result = $repo->save(0, 'newuser', 'on', '', '123', '123', 0, []);
|
||||
|
||||
$this->assertSame('error', $result['status']);
|
||||
}
|
||||
|
||||
public function testSaveReturnsErrorWhenPasswordsMismatch(): void
|
||||
{
|
||||
$db = $this->mockDb();
|
||||
$db->method('delete')->willReturn(1);
|
||||
|
||||
$repo = new UserRepository($db);
|
||||
$result = $repo->save(0, 'newuser', 'on', '', 'password1', 'password2', 0, []);
|
||||
|
||||
$this->assertSame('error', $result['status']);
|
||||
}
|
||||
}
|
||||
21
tests/bootstrap.php
Normal file
21
tests/bootstrap.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
// Medoo ORM
|
||||
require_once __DIR__ . '/../libraries/medoo/medoo.php';
|
||||
|
||||
// Stuby — muszą być załadowane PRZED autoloaderem PSR-4,
|
||||
// żeby nie zostały nadpisane przez prawdziwe klasy
|
||||
require_once __DIR__ . '/stubs/CacheHandler.php';
|
||||
require_once __DIR__ . '/stubs/S.php';
|
||||
|
||||
// PSR-4 autoloader dla Domain\
|
||||
// Shared\ jest obsłużona przez stub powyżej — pomijamy w autoloaderze
|
||||
spl_autoload_register(function (string $class): void {
|
||||
if (strncmp($class, 'Domain\\', 7) === 0) {
|
||||
$rel = substr($class, 7);
|
||||
$file = __DIR__ . '/../autoload/Domain/' . str_replace('\\', '/', $rel) . '.php';
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
}
|
||||
}
|
||||
});
|
||||
24
tests/stubs/CacheHandler.php
Normal file
24
tests/stubs/CacheHandler.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace Shared\Cache;
|
||||
|
||||
class CacheHandler
|
||||
{
|
||||
private static array $store = [];
|
||||
|
||||
public static function reset(): void { self::$store = []; }
|
||||
|
||||
public static function fetch(string $key): mixed
|
||||
{
|
||||
return self::$store[$key] ?? false;
|
||||
}
|
||||
|
||||
public static function store(string $key, mixed $value, int $ttl = 0): void
|
||||
{
|
||||
self::$store[$key] = $value;
|
||||
}
|
||||
|
||||
public static function delete(string $key): void
|
||||
{
|
||||
unset(self::$store[$key]);
|
||||
}
|
||||
}
|
||||
8
tests/stubs/S.php
Normal file
8
tests/stubs/S.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
class S
|
||||
{
|
||||
public static function delete_cache(): void {}
|
||||
public static function htacces(): void {}
|
||||
public static function get_domain(string $domain = ''): ?string { return $domain ?: null; }
|
||||
public static function send_email(string $to, string $subject, string $body): bool { return true; }
|
||||
}
|
||||
BIN
updates/1.60/ver_1.691.zip
Normal file
BIN
updates/1.60/ver_1.691.zip
Normal file
Binary file not shown.
4
updates/1.60/ver_1.691_files.txt
Normal file
4
updates/1.60/ver_1.691_files.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
F: ../backup_20250512_232458.zip
|
||||
F: ../backup_tmp.json
|
||||
F: ../sitemap_cmsenproject-dcpl.xml
|
||||
F: ../sitemap_cmsproproject-dcpl.xml
|
||||
49
updates/1.60/ver_1.691_manifest.json
Normal file
49
updates/1.60/ver_1.691_manifest.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"changelog": "Refaktoryzacja DDD Faza 0+1: PSR-4 autoloader, Shared (CacheHandler, Helpers, Html, ImageManipulator, Tpl), Domain (LanguagesRepository, SettingsRepository, UserRepository), testy jednostkowe Domain\\, docs/",
|
||||
"version": "1.691",
|
||||
"files": {
|
||||
"added": [
|
||||
"autoload/Domain/Languages/LanguagesRepository.php",
|
||||
"autoload/Domain/Settings/SettingsRepository.php",
|
||||
"autoload/Domain/User/UserRepository.php",
|
||||
"autoload/Shared/Cache/CacheHandler.php",
|
||||
"autoload/Shared/Helpers/Helpers.php",
|
||||
"autoload/Shared/Html/Html.php",
|
||||
"autoload/Shared/Image/ImageManipulator.php",
|
||||
"autoload/Shared/Tpl/Tpl.php"
|
||||
],
|
||||
"deleted": [
|
||||
"backup_20250512_232458.zip",
|
||||
"backup_tmp.json",
|
||||
"sitemap_cmsenproject-dcpl.xml",
|
||||
"sitemap_cmsproproject-dcpl.xml"
|
||||
],
|
||||
"modified": [
|
||||
"admin/ajax.php",
|
||||
"admin/index.php",
|
||||
"ajax.php",
|
||||
"api.php",
|
||||
"autoload/admin/class.Site.php",
|
||||
"autoload/admin/factory/class.Languages.php",
|
||||
"autoload/admin/factory/class.Settings.php",
|
||||
"autoload/admin/factory/class.Users.php",
|
||||
"autoload/class.Cache.php",
|
||||
"autoload/class.Html.php",
|
||||
"autoload/class.Image.php",
|
||||
"autoload/class.S.php",
|
||||
"autoload/class.Tpl.php",
|
||||
"autoload/front/factory/class.Languages.php",
|
||||
"autoload/front/factory/class.Settings.php",
|
||||
"cron.php",
|
||||
"index.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:f53230f36d391828f4e368f3fc3420d8f9430ca507a1d4f57a0988823ac22192",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-02-27",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
BIN
updates/1.60/ver_1.692.zip
Normal file
BIN
updates/1.60/ver_1.692.zip
Normal file
Binary file not shown.
24
updates/1.60/ver_1.692_manifest.json
Normal file
24
updates/1.60/ver_1.692_manifest.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"changelog": "FIX - Tpl::__isset() dla poprawnej obslugi isset() na wlasciwosciach szablonu",
|
||||
"version": "1.692",
|
||||
"files": {
|
||||
"added": [
|
||||
|
||||
],
|
||||
"deleted": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"autoload/Domain/Settings/SettingsRepository.php",
|
||||
"autoload/Shared/Tpl/Tpl.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:bbd1c61b39b5bb4a37b618efea770bed3438d7324ab389d415d24b2d87b08bd6",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-02-28",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user