Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ec32d5d09 | |||
| 3325eaf44c | |||
| 9b31ce0d16 | |||
| 964bfa877c | |||
| 36fa3fdeae | |||
| 645037d144 | |||
| b8ab53a6f3 | |||
| dd31c062ad | |||
| 869f25d6db | |||
| b41fa58488 | |||
| 1b4c6fe66a | |||
| 320710fd02 | |||
| 11d720aa25 | |||
| 08bd6d23c9 | |||
| 28de4e88b7 | |||
| 0c1e916ed6 | |||
| 1bebdff3ac | |||
| 5e6c3e46fc | |||
| ff227fa6e0 | |||
| 2e715e803e | |||
| 8e6b29976c | |||
| 9ee4116f50 | |||
| a6b821bb75 | |||
| 9c98fe7ad2 |
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
.paul/PROJECT.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# 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 (6 repos): Articles, Languages, Layouts, Pages, Settings, User
|
||||||
|
- Shared (5 modules): Cache, Helpers, Html, Image, Tpl
|
||||||
|
- Form Edit System: FormEditViewModel, multi-tab, validation, persistence
|
||||||
|
- PHPUnit base: Bootstrap, 3 test files
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Must Have
|
||||||
|
- Centralny PSR-4 autoloader (hybrydowy z legacy)
|
||||||
|
- Wszystkie Domain repositories (Scontainers, Banners, Authors, Newsletter, SEO, Cron, Releases)
|
||||||
|
- Shared\Email + Shared\Security (CsrfToken, HMAC-SHA256)
|
||||||
|
- Admin\ namespace z DI dla wszystkich 17 modułów
|
||||||
|
- Frontend\ namespace dla wszystkich front modułów
|
||||||
|
- Bezpieczne cookies (HMAC-SHA256 zamiast hash w JSON)
|
||||||
|
|
||||||
|
### Should Have
|
||||||
|
- PHPUnit testy dla nowych repositories i controllers
|
||||||
|
- Legacy cleanup (usunięcie wrapperów po pełnej migracji)
|
||||||
|
|
||||||
|
### Nice to Have
|
||||||
|
- Admin base classes (TableListRequestFactory, FormValidator — wzór shopPRO)
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- PHP < 8.0 (produkcja) — brak match, named args, union types, str_contains()
|
||||||
|
- Referencja architektury: shopPRO (C:\visual studio code\projekty\shopPRO)
|
||||||
|
- Zachowanie 100% kompatybilności wstecznej podczas migracji (wrapper delegation)
|
||||||
|
- Medoo ORM (nie zmieniać)
|
||||||
|
- Zewnętrzne biblioteki (Mobile_Detect, geoplugin) — nie ruszać
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- 19 faz refaktoryzacji zakończonych
|
||||||
|
- Cały kod w namespace'ach Domain\, Shared\, Admin\, Frontend\
|
||||||
|
- Zero regresji — istniejąca funkcjonalność działa bez zmian
|
||||||
|
- Bezpieczne cookies (HMAC-SHA256)
|
||||||
|
- Testy PHPUnit dla kluczowych modułów
|
||||||
|
|
||||||
|
---
|
||||||
|
*Created: 2026-04-04*
|
||||||
306
.paul/ROADMAP.md
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
# Roadmap: cmsPRO
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Pełna refaktoryzacja cmsPRO do architektury DDD wzorowanej na shopPRO. Wzorzec: wrapper delegation — stare klasy delegują do nowych, zero regresji. Referencja: C:\visual studio code\projekty\shopPRO. PHP < 8.0 (produkcja).
|
||||||
|
|
||||||
|
## Current Milestone
|
||||||
|
**v0.1 Refaktoryzacja** (v0.1.0)
|
||||||
|
Status: In progress
|
||||||
|
Phases: 2 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 | Not started | - |
|
||||||
|
| 4 | Domain: Authors + Newsletter | 1 | Not started | - |
|
||||||
|
| 5 | Domain: SeoAdditional + Cron + Releases | 1 | Not started | - |
|
||||||
|
| 6 | Admin: Base Infrastructure | 1 | Not started | - |
|
||||||
|
| 7 | Admin: Articles + ArticlesArchive | 1 | Not started | - |
|
||||||
|
| 8 | Admin: Pages + Layouts | 1 | Not started | - |
|
||||||
|
| 9 | Admin: Languages + Settings | 1 | Not started | - |
|
||||||
|
| 10 | Admin: Banners + Authors + Scontainers | 1 | Not started | - |
|
||||||
|
| 11 | Admin: Newsletter + Emails + SeoAdditional | 1 | Not started | - |
|
||||||
|
| 12 | Admin: Users + Backups + Filemanager | 1 | Not started | - |
|
||||||
|
| 13 | Admin: Releases + Update | 1 | Not started | - |
|
||||||
|
| 14 | Front: Site + Articles | 1 | Not started | - |
|
||||||
|
| 15 | Front: Pages + Menu + Banners + Scontainers | 1 | Not started | - |
|
||||||
|
| 16 | Front: Remaining modules | 1-2 | Not started | - |
|
||||||
|
| 17 | Users & Security: HMAC-SHA256 | 1 | Not started | - |
|
||||||
|
| 18 | Tests | 1-2 | Not started | - |
|
||||||
|
| 19 | Legacy Cleanup | 1 | Not started | - |
|
||||||
|
|
||||||
|
## Phase Details
|
||||||
|
|
||||||
|
### Phase 1: Infrastructure & Autoloader
|
||||||
|
|
||||||
|
**Goal:** Centralny autoloader (PSR-4 + legacy), composer.json z mapowaniem, usunięcie duplikatów z entry pointów.
|
||||||
|
**Depends on:** Nothing (first phase)
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- autoload/autoloader.php (hybrydowy)
|
||||||
|
- composer.json PSR-4: Domain\, Shared\, Admin\, Frontend\
|
||||||
|
- Migracja 6 entry pointów (index.php, admin/index.php, ajax.php, api.php, cron.php, download.php)
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 01-01: PSR-4 autoloader setup i composer.json
|
||||||
|
|
||||||
|
### Phase 2: Shared: Email + Security
|
||||||
|
|
||||||
|
**Goal:** Dodać brakujące moduły Shared — Email (migracja z legacy) i Security (CsrfToken, wzór shopPRO).
|
||||||
|
**Depends on:** Phase 1 (autoloader)
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Shared\Email\Email — migracja z legacy
|
||||||
|
- Shared\Security\CsrfToken — nowy moduł (wzór shopPRO)
|
||||||
|
- Wrapper w starym class.Email.php (jeśli istnieje)
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 02-01: Email + Security modules
|
||||||
|
|
||||||
|
### Phase 3: Domain: Scontainers + Banners
|
||||||
|
|
||||||
|
**Goal:** Repository dla Scontainers i Banners — przeniesienie logiki z factory do Domain\.
|
||||||
|
**Depends on:** Phase 1 (autoloader)
|
||||||
|
**Research:** Unlikely (wzorzec ustalony)
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Domain\Scontainers\ScontainersRepository
|
||||||
|
- Domain\Banners\BannersRepository
|
||||||
|
- Wrappery w starych factory (delegacja)
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 03-01: Scontainers + Banners repositories
|
||||||
|
|
||||||
|
### Phase 4: Domain: Authors + Newsletter
|
||||||
|
|
||||||
|
**Goal:** Repository dla Authors i Newsletter.
|
||||||
|
**Depends on:** Phase 1
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Domain\Authors\AuthorsRepository
|
||||||
|
- Domain\Newsletter\NewsletterRepository
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 04-01: Authors + Newsletter repositories
|
||||||
|
|
||||||
|
### Phase 5: Domain: SeoAdditional + Cron + Releases
|
||||||
|
|
||||||
|
**Goal:** Repository dla SEO, Cron, i systemu Releases/Update.
|
||||||
|
**Depends on:** Phase 1
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Domain\SeoAdditional\SeoAdditionalRepository
|
||||||
|
- Domain\Cron\CronRepository
|
||||||
|
- Domain\Releases\ReleasesRepository (lub Update)
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 05-01: SeoAdditional + Cron + Releases repositories
|
||||||
|
|
||||||
|
### Phase 6: Admin: Base Infrastructure
|
||||||
|
|
||||||
|
**Goal:** Bazowe klasy Admin\ — kontrolery bazowe, TableListRequestFactory, FormValidator (wzór shopPRO).
|
||||||
|
**Depends on:** Phase 1 (autoloader), Form Edit System (already done)
|
||||||
|
**Research:** Likely (analiza shopPRO Admin base classes)
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Admin\Base\BaseController (lub abstrakcyjna klasa bazowa)
|
||||||
|
- Admin\Support\TableListRequestFactory
|
||||||
|
- Admin\Support\FormValidator
|
||||||
|
- Integracja z istniejącym FormEditViewModel
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 06-01: Admin base infrastructure
|
||||||
|
|
||||||
|
### Phase 7: Admin: Articles + ArticlesArchive
|
||||||
|
|
||||||
|
**Goal:** Migracja kontrolerów Articles i ArticlesArchive do Admin\ z DI.
|
||||||
|
**Depends on:** Phase 6 (Admin base), Phase 1 (Domain\Articles already exists)
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Admin\Articles\ArticlesController
|
||||||
|
- Admin\Articles\ArticlesArchiveController
|
||||||
|
- Wrapper w starym controls/class.Articles.php
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 07-01: Articles admin controllers
|
||||||
|
|
||||||
|
### Phase 8: Admin: Pages + Layouts
|
||||||
|
|
||||||
|
**Goal:** Migracja kontrolerów Pages i Layouts do Admin\.
|
||||||
|
**Depends on:** Phase 6
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Admin\Pages\PagesController
|
||||||
|
- Admin\Layouts\LayoutsController
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 08-01: Pages + Layouts admin controllers
|
||||||
|
|
||||||
|
### Phase 9: Admin: Languages + Settings
|
||||||
|
|
||||||
|
**Goal:** Migracja kontrolerów Languages i Settings do Admin\.
|
||||||
|
**Depends on:** Phase 6
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Admin\Languages\LanguagesController
|
||||||
|
- Admin\Settings\SettingsController
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 09-01: Languages + Settings admin controllers
|
||||||
|
|
||||||
|
### Phase 10: Admin: Banners + Authors + Scontainers
|
||||||
|
|
||||||
|
**Goal:** Migracja kontrolerów Banners, Authors, Scontainers do Admin\.
|
||||||
|
**Depends on:** Phase 6, Phase 3 (Domain repos), Phase 4 (Domain repos)
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Admin\Banners\BannersController
|
||||||
|
- Admin\Authors\AuthorsController
|
||||||
|
- Admin\Scontainers\ScontainersController
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 10-01: Banners + Authors + Scontainers admin controllers
|
||||||
|
|
||||||
|
### Phase 11: Admin: Newsletter + Emails + SeoAdditional
|
||||||
|
|
||||||
|
**Goal:** Migracja kontrolerów Newsletter, Emails, SeoAdditional do Admin\.
|
||||||
|
**Depends on:** Phase 6, Phase 4, Phase 5
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Admin\Newsletter\NewsletterController
|
||||||
|
- Admin\Emails\EmailsController
|
||||||
|
- Admin\SeoAdditional\SeoAdditionalController
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 11-01: Newsletter + Emails + SeoAdditional admin controllers
|
||||||
|
|
||||||
|
### Phase 12: Admin: Users + Backups + Filemanager
|
||||||
|
|
||||||
|
**Goal:** Migracja kontrolerów Users, Backups, Filemanager do Admin\.
|
||||||
|
**Depends on:** Phase 6
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Admin\Users\UsersController
|
||||||
|
- Admin\Backups\BackupsController
|
||||||
|
- Admin\Filemanager\FilemanagerController
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 12-01: Users + Backups + Filemanager admin controllers
|
||||||
|
|
||||||
|
### Phase 13: Admin: Releases + Update
|
||||||
|
|
||||||
|
**Goal:** Migracja kontrolerów Releases i Update do Admin\.
|
||||||
|
**Depends on:** Phase 6, Phase 5 (Domain repos)
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Admin\Releases\ReleasesController
|
||||||
|
- Admin\Update\UpdateController
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 13-01: Releases + Update admin controllers
|
||||||
|
|
||||||
|
### Phase 14: Front: Site + Articles
|
||||||
|
|
||||||
|
**Goal:** Migracja głównych kontrolerów front — Site i Articles do Frontend\.
|
||||||
|
**Depends on:** Phase 1 (autoloader), Domain repos
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Frontend\Site\SiteController (lub controls + factory + view)
|
||||||
|
- Frontend\Articles\ArticlesController
|
||||||
|
- LayoutEngine (jeśli potrzebny, wzór shopPRO)
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 14-01: Site + Articles frontend controllers
|
||||||
|
|
||||||
|
### Phase 15: Front: Pages + Menu + Banners + Scontainers
|
||||||
|
|
||||||
|
**Goal:** Migracja front kontrolerów Pages, Menu, Banners, Scontainers.
|
||||||
|
**Depends on:** Phase 14 (Front base)
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Frontend\Pages, Frontend\Menu, Frontend\Banners, Frontend\Scontainers
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 15-01: Pages + Menu + Banners + Scontainers frontend
|
||||||
|
|
||||||
|
### Phase 16: Front: Remaining modules
|
||||||
|
|
||||||
|
**Goal:** Migracja pozostałych front modułów — Authors, Languages, Newsletter, Search, AuditSEO, SeoAdditional, Layouts, Settings.
|
||||||
|
**Depends on:** Phase 14
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Wszystkie pozostałe front factories/controls/views
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 16-01: Remaining frontend modules (batch 1)
|
||||||
|
- [ ] 16-02: Remaining frontend modules (batch 2, if needed)
|
||||||
|
|
||||||
|
### Phase 17: Users & Security: HMAC-SHA256
|
||||||
|
|
||||||
|
**Goal:** Wymiana insecure remember-me cookies (hash w JSON) na HMAC-SHA256 signed tokens.
|
||||||
|
**Depends on:** Phase 2 (Shared\Security), Phase 12 (Admin\Users)
|
||||||
|
**Research:** Likely (strategia migracji istniejących cookies, backward compat)
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Nowy system remember-me z HMAC-SHA256
|
||||||
|
- Migracja istniejących sesji/cookies
|
||||||
|
- Security hardening w UserRepository
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 17-01: HMAC-SHA256 cookie system
|
||||||
|
|
||||||
|
### Phase 18: Tests
|
||||||
|
|
||||||
|
**Goal:** Rozbudowa PHPUnit testów dla nowych Domain repositories i Admin controllers.
|
||||||
|
**Depends on:** Phase 5 (all Domain repos), Phase 13 (all Admin controllers)
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Testy dla nowych Domain repositories
|
||||||
|
- Testy dla Admin controllers (unit)
|
||||||
|
- Rozbudowa test bootstrap
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 18-01: Domain repository tests
|
||||||
|
- [ ] 18-02: Admin controller tests
|
||||||
|
|
||||||
|
### Phase 19: Legacy Cleanup
|
||||||
|
|
||||||
|
**Goal:** Usunięcie legacy wrapperów i starych class.*.php po weryfikacji że cały kod używa nowych klas.
|
||||||
|
**Depends on:** All prior phases
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Usunięcie wrapperów z class.*.php
|
||||||
|
- Usunięcie starych controls/factory/view plików
|
||||||
|
- Finalna weryfikacja i cleanup
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 19-01: Legacy wrapper removal and cleanup
|
||||||
|
|
||||||
|
---
|
||||||
|
*Roadmap created: 2026-04-04*
|
||||||
|
*Last updated: 2026-04-04*
|
||||||
64
.paul/STATE.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Project State
|
||||||
|
|
||||||
|
## Project Reference
|
||||||
|
|
||||||
|
See: .paul/PROJECT.md (updated 2026-04-04)
|
||||||
|
|
||||||
|
**Core value:** Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi.
|
||||||
|
**Current focus:** Phase 2 complete — ready for Phase 3
|
||||||
|
|
||||||
|
## Current Position
|
||||||
|
|
||||||
|
Milestone: v0.1 Refaktoryzacja
|
||||||
|
Phase: 2 of 19 (Shared: Email + Security) — Complete
|
||||||
|
Plan: 02-01 complete
|
||||||
|
Status: Loop closed, ready for next PLAN
|
||||||
|
Last activity: 2026-04-04 — Phase 2 complete, UNIFY done
|
||||||
|
|
||||||
|
Progress:
|
||||||
|
- Milestone: [▓░░░░░░░░░] 10%
|
||||||
|
|
||||||
|
## Loop Position
|
||||||
|
|
||||||
|
Current loop state:
|
||||||
|
```
|
||||||
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
|
✓ ✓ ✓ [Loop complete - ready for next PLAN]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Metrics
|
||||||
|
|
||||||
|
**Velocity:**
|
||||||
|
- Total plans completed: 2
|
||||||
|
- Total execution time: ~18min
|
||||||
|
|
||||||
|
**By Phase:**
|
||||||
|
|
||||||
|
| Phase | Plans | Total Time | Avg/Plan |
|
||||||
|
|-------|-------|------------|----------|
|
||||||
|
| 01-infrastructure | 1/1 | ~10min | ~10min |
|
||||||
|
| 02-shared-email-security | 1/1 | ~8min | ~8min |
|
||||||
|
|
||||||
|
## Accumulated Context
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
- Centralny autoloader zamiast duplikatów
|
||||||
|
- CsrfToken: single token per session (shopPRO pattern)
|
||||||
|
- Email: PHPMailer require via __DIR__ absolute paths
|
||||||
|
- Shared layer kompletny: Cache, Helpers, Html, Image, Tpl, Email, Security
|
||||||
|
|
||||||
|
### Deferred Issues
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Blockers/Concerns
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Session Continuity
|
||||||
|
|
||||||
|
Last session: 2026-04-04
|
||||||
|
Stopped at: Phase 2 complete, loop closed
|
||||||
|
Next action: Run /paul:plan for Phase 3 (Domain: Scontainers + Banners)
|
||||||
|
Resume file: .paul/phases/02-shared-email-security/02-01-SUMMARY.md
|
||||||
|
|
||||||
|
---
|
||||||
|
*STATE.md — Updated after every significant action*
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -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*
|
||||||
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.041,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsEmptyWhenNull":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsRowWhenFound":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsNullWhenNotFound":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesQueriesDbAndCaches":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsEmptyWhenNull":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testMaxOrderReturnsInteger":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsTrueOnSuccess":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDeleteReturnsFalseOnFailure":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationDetailsReturnsRowOrNull":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsMappedArray":0.001,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenDbReturnsNull":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsUsesCache":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testUpdateCallsDbUpdateWhenParamExists":0.001,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testUpdateCallsDbInsertWhenParamMissing":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testVisitCounterReturnsValue":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testVisitCounterReturnsNullWhenEmpty":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserArray":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindByLoginReturnsUser":0.002,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testAllReturnsArray":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testAllReturnsEmptyArrayWhenNull":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsTrueForAdminUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsTrueWhenPrivilegeExists":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testHasPrivilegeReturnsFalseWhenPrivilegeMissing":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroWhenUserNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsMinusOneWhenAccountBlocked":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsOneOnSuccess":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsTrueWhenExists":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testIsLoginTakenReturnsFalseWhenFree":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenUserNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenTooManyFailedAttempts":0.075,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseWhenExpired":0.073,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueOnValidCode":0.148,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordTooShort":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorWhenPasswordsMismatch":0}}
|
||||||
@@ -115,3 +115,10 @@ initial_prompt: ""
|
|||||||
# override of the corresponding setting in serena_config.yml, see the documentation there.
|
# 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.
|
# If null or missing, the value from the global config is used.
|
||||||
symbol_info_budget:
|
symbol_info_budget:
|
||||||
|
|
||||||
|
# The language backend to use for this project.
|
||||||
|
# If not set, the global setting from serena_config.yml is used.
|
||||||
|
# Valid values: LSP, JetBrains
|
||||||
|
# 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:
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ backups/
|
|||||||
cache/
|
cache/
|
||||||
cron/
|
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
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.serena/
|
.serena/
|
||||||
|
|||||||
7
.vscode/ftp-kr.json
vendored
@@ -12,6 +12,11 @@
|
|||||||
"ignoreRemoteModification": true,
|
"ignoreRemoteModification": true,
|
||||||
"ignore": [
|
"ignore": [
|
||||||
".git",
|
".git",
|
||||||
"/.vscode"
|
"/.vscode",
|
||||||
|
"/.claude",
|
||||||
|
"/.serena",
|
||||||
|
"/docs",
|
||||||
|
"AGENTS.md",
|
||||||
|
"CLAUDE.md"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
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.
|
||||||
16
CLAUDE.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Workflow
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -1,19 +1,6 @@
|
|||||||
<?
|
<?
|
||||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||||
function __autoload_my_classes( $classname )
|
require_once __DIR__ . '/../autoload/autoloader.php';
|
||||||
{
|
|
||||||
$q = explode( '\\' , $classname );
|
|
||||||
$c = array_pop( $q );
|
|
||||||
$f = '../autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
|
||||||
if ( $c == 'Savant3' )
|
|
||||||
{
|
|
||||||
require_once( '../autoload/Savant3.php' );
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if ( file_exists( $f ) )
|
|
||||||
require_once( $f );
|
|
||||||
}
|
|
||||||
spl_autoload_register( '__autoload_my_classes' );
|
|
||||||
|
|
||||||
require_once '../config.php';
|
require_once '../config.php';
|
||||||
require_once '../libraries/medoo/medoo.php';
|
require_once '../libraries/medoo/medoo.php';
|
||||||
|
|||||||
@@ -12,15 +12,7 @@ if ( file_exists( 'ip.conf' ) )
|
|||||||
}
|
}
|
||||||
|
|
||||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||||
function __autoload_my_classes( $classname )
|
require_once __DIR__ . '/../autoload/autoloader.php';
|
||||||
{
|
|
||||||
$q = explode( '\\' , $classname );
|
|
||||||
$c = array_pop( $q );
|
|
||||||
$f = '../autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
|
||||||
if ( file_exists( $f ) )
|
|
||||||
require_once( $f );
|
|
||||||
}
|
|
||||||
spl_autoload_register( '__autoload_my_classes' );
|
|
||||||
|
|
||||||
require_once '../config.php';
|
require_once '../config.php';
|
||||||
require_once '../libraries/medoo/medoo.php';
|
require_once '../libraries/medoo/medoo.php';
|
||||||
|
|||||||
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
@@ -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();
|
||||||
|
?>
|
||||||
11
ajax.php
@@ -1,15 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||||
function __autoload_my_classes( $classname )
|
require_once __DIR__ . '/autoload/autoloader.php';
|
||||||
{
|
|
||||||
$q = explode( '\\' , $classname );
|
|
||||||
$c = array_pop( $q );
|
|
||||||
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
|
||||||
|
|
||||||
if ( file_exists( $f ) )
|
|
||||||
require_once( $f );
|
|
||||||
}
|
|
||||||
spl_autoload_register( '__autoload_my_classes' );
|
|
||||||
date_default_timezone_set( 'Europe/Warsaw' );
|
date_default_timezone_set( 'Europe/Warsaw' );
|
||||||
|
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
|
|||||||
13
api.php
@@ -1,15 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
error_reporting(E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED);
|
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||||
function __autoload_my_classes($classname)
|
require_once __DIR__ . '/autoload/autoloader.php';
|
||||||
{
|
|
||||||
$q = explode('\\', $classname);
|
|
||||||
$c = array_pop($q);
|
|
||||||
$f = 'autoload/' . implode('/', $q) . '/class.' . $c . '.php';
|
|
||||||
|
|
||||||
if (file_exists($f))
|
|
||||||
require_once($f);
|
|
||||||
}
|
|
||||||
spl_autoload_register('__autoload_my_classes');
|
|
||||||
date_default_timezone_set('Europe/Warsaw');
|
date_default_timezone_set('Europe/Warsaw');
|
||||||
|
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
|
|||||||
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' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
213
autoload/Domain/Languages/LanguagesRepository.php
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<?php
|
||||||
|
namespace Domain\Languages;
|
||||||
|
|
||||||
|
class LanguagesRepository
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct( $db )
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Odczyt
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function languagesList(): array
|
||||||
|
{
|
||||||
|
return $this->db->select( 'pp_langs', '*', [ 'ORDER' => [ 'o' => 'ASC' ] ] ) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function languageDetails( string $languageId ): ?array
|
||||||
|
{
|
||||||
|
return $this->db->get( 'pp_langs', '*', [ 'id' => $languageId ] ) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function availableDomains(): array
|
||||||
|
{
|
||||||
|
return $this->db->query(
|
||||||
|
'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL GROUP BY domain'
|
||||||
|
)->fetchAll( \PDO::FETCH_ASSOC ) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultDomain(): ?string
|
||||||
|
{
|
||||||
|
$results = $this->db->query(
|
||||||
|
'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL AND main_domain = 1'
|
||||||
|
)->fetchAll();
|
||||||
|
return $results[0][0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultLanguage( string $domain = '' ): ?string
|
||||||
|
{
|
||||||
|
if ( !$default = \Shared\Cache\CacheHandler::fetch( "default_language:$domain" ) )
|
||||||
|
{
|
||||||
|
if ( $domain )
|
||||||
|
$results = $this->db->query(
|
||||||
|
'SELECT id FROM pp_langs WHERE status = 1 AND domain = \'' . $domain . '\' ORDER BY start DESC, o ASC LIMIT 1'
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
if ( !$domain || !$this->defaultDomain() )
|
||||||
|
$results = $this->db->query(
|
||||||
|
'SELECT id FROM pp_langs WHERE status = 1 AND domain IS NULL ORDER BY start DESC, o ASC LIMIT 1'
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
$default = $results[0][0] ?? null;
|
||||||
|
\Shared\Cache\CacheHandler::store( "default_language:$domain", $default );
|
||||||
|
}
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activeLanguages(): array
|
||||||
|
{
|
||||||
|
if ( !$active = \Shared\Cache\CacheHandler::fetch( 'active_languages' ) )
|
||||||
|
{
|
||||||
|
$active = $this->db->select( 'pp_langs', [ 'id', 'name', 'domain' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ) ?: [];
|
||||||
|
\Shared\Cache\CacheHandler::store( 'active_languages', $active );
|
||||||
|
}
|
||||||
|
return $active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function langTranslations( string $language = 'pl' ): array
|
||||||
|
{
|
||||||
|
if ( !$translations = \Shared\Cache\CacheHandler::fetch( "lang_translations:$language" ) )
|
||||||
|
{
|
||||||
|
$translations = [ '0' => $language ];
|
||||||
|
|
||||||
|
$results = $this->db->select( 'pp_langs_translations', [ 'text', $language ] );
|
||||||
|
if ( is_array( $results ) )
|
||||||
|
foreach ( $results as $row )
|
||||||
|
$translations[ $row['text'] ] = $row[ $language ];
|
||||||
|
|
||||||
|
\Shared\Cache\CacheHandler::store( "lang_translations:$language", $translations );
|
||||||
|
}
|
||||||
|
return $translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function translationDetails( int $translationId ): ?array
|
||||||
|
{
|
||||||
|
return $this->db->get( 'pp_langs_translations', '*', [ 'id' => $translationId ] ) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function maxOrder(): int
|
||||||
|
{
|
||||||
|
return (int) $this->db->max( 'pp_langs', 'o' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Zapis / usuwanie
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function languageSave( string $languageId, string $name, $status, $start, $o, $domain, $main_domain ): string
|
||||||
|
{
|
||||||
|
if ( $start == 'on' && $status == 'on' && !\S::get_domain( $domain ) )
|
||||||
|
$this->db->update( 'pp_langs', [ 'start' => 0 ], [ 'id[!]' => $languageId ] );
|
||||||
|
|
||||||
|
if ( $start == 'on' && $status == 'on' && \S::get_domain( $domain ) )
|
||||||
|
$this->db->update( 'pp_langs', [ 'start' => 0 ], [
|
||||||
|
'AND' => [ 'id[!]' => $languageId, 'domain' => \S::get_domain( $domain ) ]
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( $main_domain == 'on' && $domain && $status == 'on' )
|
||||||
|
$this->db->update( 'pp_langs', [ 'main_domain' => 0 ], [ ' id[!]' => $languageId ] );
|
||||||
|
|
||||||
|
if ( $this->db->count( 'pp_langs', [ 'id' => $languageId ] ) )
|
||||||
|
{
|
||||||
|
$this->db->update( 'pp_langs', [
|
||||||
|
'status' => $status == 'on' ? 1 : 0,
|
||||||
|
'start' => $start == 'on' ? 1 : 0,
|
||||||
|
'name' => $name,
|
||||||
|
'o' => $o,
|
||||||
|
'domain' => \S::get_domain( $domain ) ?: null,
|
||||||
|
'main_domain' => $main_domain == 'on' && \S::get_domain( $domain ) ? 1 : 0,
|
||||||
|
], [ 'id' => $languageId ] );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ( $this->db->query( 'ALTER TABLE pp_langs_translations ADD ' . strtolower( $languageId ) . ' TEXT NULL DEFAULT NULL' ) )
|
||||||
|
{
|
||||||
|
$this->db->insert( 'pp_langs', [
|
||||||
|
'id' => strtolower( $languageId ),
|
||||||
|
'name' => $name,
|
||||||
|
'status' => $status == 'on' ? 1 : 0,
|
||||||
|
'start' => $start == 'on' ? 1 : 0,
|
||||||
|
'o' => $o,
|
||||||
|
'domain' => \S::get_domain( $domain ) ?: null,
|
||||||
|
'main_domain' => $main_domain == 'on' && \S::get_domain( $domain ) ? 1 : 0,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upewnij się, że każda domena ma język startowy
|
||||||
|
if ( !$this->db->count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'domain[!]' => null ] ] ) )
|
||||||
|
{
|
||||||
|
if ( !$this->db->count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'start' => 1, 'domain' => null ] ] ) )
|
||||||
|
{
|
||||||
|
if ( $idTmp = $this->db->get( 'pp_langs', 'id', [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ) )
|
||||||
|
$this->db->update( 'pp_langs', [ 'start' => 1 ], [ 'id' => $idTmp ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$domains = $this->db->select( 'pp_langs', 'domain', [ 'domain[!]' => null, 'GROUP' => 'domain' ] );
|
||||||
|
if ( is_array( $domains ) && !empty( $domains ) )
|
||||||
|
{
|
||||||
|
$this->db->update( 'pp_langs', [ 'start' => 0 ], [ 'domain' => null ] );
|
||||||
|
foreach ( $domains as $dom )
|
||||||
|
{
|
||||||
|
if ( !$this->db->count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'start' => 1, 'domain' => $dom ] ] ) )
|
||||||
|
{
|
||||||
|
if ( $idTmp = $this->db->get( 'pp_langs', 'id', [ 'AND' => [ 'status' => 1, 'domain' => $dom ], 'ORDER' => [ 'o' => 'ASC' ] ] ) )
|
||||||
|
$this->db->update( 'pp_langs', [ 'start' => 1 ], [ 'id' => $idTmp ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !$this->db->count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'main_domain' => 1 ] ] ) )
|
||||||
|
{
|
||||||
|
if ( $idTmp = $this->db->get( 'pp_langs', 'id', [ 'AND' => [ 'status' => 1, 'domain[!]' => null ], 'ORDER' => [ 'o' => 'ASC' ] ] ) )
|
||||||
|
$this->db->update( 'pp_langs', [ 'main_domain' => 1 ], [ 'id' => $idTmp ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
\S::htacces();
|
||||||
|
\S::delete_cache();
|
||||||
|
return $languageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function languageDelete( string $languageId ): bool
|
||||||
|
{
|
||||||
|
if ( $this->db->count( 'pp_langs' ) > 1 )
|
||||||
|
{
|
||||||
|
if ( $this->db->query( 'ALTER TABLE pp_langs_translations DROP ' . $languageId )
|
||||||
|
&& $this->db->delete( 'pp_langs', [ 'id' => $languageId ] ) )
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function translationSave( $translationId, string $text, array $languages = [] ): int
|
||||||
|
{
|
||||||
|
if ( $translationId )
|
||||||
|
{
|
||||||
|
$this->db->update( 'pp_langs_translations', [ 'text' => $text ], [ 'id' => $translationId ] );
|
||||||
|
foreach ( $languages as $key => $val )
|
||||||
|
$this->db->update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translationId ] );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$this->db->insert( 'pp_langs_translations', [ 'text' => $text ] );
|
||||||
|
$translationId = $this->db->id();
|
||||||
|
foreach ( $languages as $key => $val )
|
||||||
|
$this->db->update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translationId ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
\S::htacces();
|
||||||
|
\S::delete_cache();
|
||||||
|
return (int) $translationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function translationDelete( int $translationId ): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->db->delete( 'pp_langs_translations', [ 'id' => $translationId ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
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 ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
73
autoload/Domain/Settings/SettingsRepository.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
namespace Domain\Settings;
|
||||||
|
|
||||||
|
class SettingsRepository
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca wszystkie ustawienia jako tablicę asocjacyjną param => value.
|
||||||
|
* Wynik jest cache'owany (TTL 24h).
|
||||||
|
*/
|
||||||
|
public function allSettings(): array
|
||||||
|
{
|
||||||
|
if ( !$settings = \Shared\Cache\CacheHandler::fetch( 'settings_details' ) )
|
||||||
|
{
|
||||||
|
$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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $settings ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert jednego parametru.
|
||||||
|
*/
|
||||||
|
public function update( string $param, $value ): bool
|
||||||
|
{
|
||||||
|
if ( $this->db->count( 'pp_settings', [ 'param' => $param ] ) )
|
||||||
|
return (bool) $this->db->update( 'pp_settings', [ 'value' => $value ], [ 'param' => $param ] );
|
||||||
|
else
|
||||||
|
return (bool) $this->db->insert( 'pp_settings', [ 'param' => $param, 'value' => $value ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zapisuje zbiorczo ustawienia (TRUNCATE + INSERT).
|
||||||
|
* Czyści cache i regeneruje .htaccess.
|
||||||
|
*
|
||||||
|
* @param array $data Tablica asocjacyjna [ 'param' => value, ... ]
|
||||||
|
*/
|
||||||
|
public function save( array $data ): bool
|
||||||
|
{
|
||||||
|
$this->db->query( 'TRUNCATE pp_settings' );
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ( $data as $param => $value )
|
||||||
|
$rows[] = [ 'param' => $param, 'value' => $value ];
|
||||||
|
|
||||||
|
$this->db->insert( 'pp_settings', $rows );
|
||||||
|
|
||||||
|
\S::delete_cache();
|
||||||
|
\S::htacces();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca bieżącą wartość licznika odwiedzin.
|
||||||
|
*/
|
||||||
|
public function visitCounter(): ?string
|
||||||
|
{
|
||||||
|
return $this->db->get( 'pp_settings', 'value', [ 'param' => 'visits' ] ) ?: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
235
autoload/Domain/User/UserRepository.php
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<?php
|
||||||
|
namespace Domain\User;
|
||||||
|
|
||||||
|
class UserRepository
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct( $db )
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Odczyt
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function find( int $userId ): ?array
|
||||||
|
{
|
||||||
|
return $this->db->get( 'pp_users', '*', [ 'id' => $userId ] ) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByLogin( string $login ): ?array
|
||||||
|
{
|
||||||
|
return $this->db->get( 'pp_users', '*', [ 'login' => $login ] ) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
return $this->db->select( 'pp_users', '*' ) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function privileges( int $userId ): array
|
||||||
|
{
|
||||||
|
return $this->db->select( 'pp_users_privileges', '*', [ 'id_user' => $userId ] ) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasPrivilege( string $name, int $userId ): bool
|
||||||
|
{
|
||||||
|
if ( $userId === 1 )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if ( !$result = \Shared\Cache\CacheHandler::fetch( "check_privileges:$userId:$name-tmp" ) )
|
||||||
|
{
|
||||||
|
$result = $this->db->count( 'pp_users_privileges', [ 'AND' => [ 'name' => $name, 'id_user' => $userId ] ] );
|
||||||
|
\Shared\Cache\CacheHandler::store( "check_privileges:$userId:$name", $result );
|
||||||
|
}
|
||||||
|
return (bool) $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Logowanie
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weryfikuje login i hasło.
|
||||||
|
* @return int 1 = OK, 0 = złe dane, -1 = konto zablokowane
|
||||||
|
*/
|
||||||
|
public function logon( string $login, string $password ): int
|
||||||
|
{
|
||||||
|
if ( !$this->db->get( 'pp_users', '*', [ 'login' => $login ] ) )
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if ( !$this->db->get( 'pp_users', '*', [ 'AND' => [ 'login' => $login, 'status' => 1, 'error_logged_count[<]' => 5 ] ] ) )
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if ( $this->db->get( 'pp_users', '*', [
|
||||||
|
'AND' => [
|
||||||
|
'login' => $login,
|
||||||
|
'status' => 1,
|
||||||
|
'password' => md5( $password ),
|
||||||
|
'OR' => [ 'active_to[>=]' => date( 'Y-m-d' ), 'active_to' => null ]
|
||||||
|
]
|
||||||
|
] ) ) {
|
||||||
|
$this->db->update( 'pp_users', [ 'last_logged' => date( 'Y-m-d H:i:s' ), 'error_logged_count' => 0 ], [ 'login' => $login ] );
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->update( 'pp_users', [ 'last_error_logged' => date( 'Y-m-d H:i:s' ), 'error_logged_count[+]' => 1 ], [ 'login' => $login ] );
|
||||||
|
if ( $this->db->get( 'pp_users', 'error_logged_count', [ 'login' => $login ] ) >= 5 )
|
||||||
|
{
|
||||||
|
$this->db->update( 'pp_users', [ 'status' => 0 ], [ 'login' => $login ] );
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLoginTaken( string $login, int $excludeId = 0 ): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->db->get( 'pp_users', 'login', [ 'AND' => [ 'login' => $login, 'id[!]' => $excludeId ] ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 2FA
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function update( int $userId, array $data ): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->db->update( 'pp_users', $data, [ 'id' => $userId ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendTwofaCode( int $userId, bool $resend = false ): bool
|
||||||
|
{
|
||||||
|
$user = $this->find( $userId );
|
||||||
|
if ( !$user ) return false;
|
||||||
|
|
||||||
|
if ( (int)$user['twofa_enabled'] !== 1 ) return false;
|
||||||
|
|
||||||
|
$to = $user['twofa_email'] ?: $user['login'];
|
||||||
|
if ( !filter_var( $to, FILTER_VALIDATE_EMAIL ) ) return false;
|
||||||
|
|
||||||
|
if ( $resend && !empty( $user['twofa_sent_at'] ) )
|
||||||
|
{
|
||||||
|
$last = strtotime( $user['twofa_sent_at'] );
|
||||||
|
if ( $last && ( time() - $last ) < 30 ) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = random_int( 100000, 999999 );
|
||||||
|
$hash = password_hash( (string)$code, PASSWORD_DEFAULT );
|
||||||
|
|
||||||
|
$this->update( $userId, [
|
||||||
|
'twofa_code_hash' => $hash,
|
||||||
|
'twofa_expires_at' => date( 'Y-m-d H:i:s', time() + 10 * 60 ),
|
||||||
|
'twofa_sent_at' => date( 'Y-m-d H:i:s' ),
|
||||||
|
'twofa_failed_attempts' => 0,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$subject = 'Twój kod logowania 2FA';
|
||||||
|
$body = "Twój kod logowania do panelu administratora: {$code}. Kod jest ważny przez 10 minut. Jeśli to nie Ty inicjowałeś logowanie – zignoruj tę wiadomość i poinformuj administratora.";
|
||||||
|
|
||||||
|
$sent = \S::send_email( $to, $subject, $body );
|
||||||
|
if ( !$sent )
|
||||||
|
{
|
||||||
|
$headers = "MIME-Version: 1.0\r\n";
|
||||||
|
$headers .= "Content-type: text/plain; charset=UTF-8\r\n";
|
||||||
|
$headers .= "From: no-reply@" . ( $_SERVER['HTTP_HOST'] ?? 'localhost' ) . "\r\n";
|
||||||
|
$sent = mail( $to, mb_encode_mimeheader( $subject, 'UTF-8' ), $body, $headers );
|
||||||
|
}
|
||||||
|
return (bool) $sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyTwofaCode( int $userId, string $code ): bool
|
||||||
|
{
|
||||||
|
$user = $this->find( $userId );
|
||||||
|
if ( !$user ) return false;
|
||||||
|
|
||||||
|
if ( (int)$user['twofa_failed_attempts'] >= 5 ) return false;
|
||||||
|
|
||||||
|
if ( empty( $user['twofa_expires_at'] ) || time() > strtotime( $user['twofa_expires_at'] ) )
|
||||||
|
{
|
||||||
|
$this->update( $userId, [ 'twofa_code_hash' => null, 'twofa_expires_at' => null ] );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = !empty( $user['twofa_code_hash'] ) && password_verify( $code, $user['twofa_code_hash'] );
|
||||||
|
if ( $ok )
|
||||||
|
{
|
||||||
|
$this->update( $userId, [
|
||||||
|
'twofa_code_hash' => null,
|
||||||
|
'twofa_expires_at' => null,
|
||||||
|
'twofa_sent_at' => null,
|
||||||
|
'twofa_failed_attempts' => 0,
|
||||||
|
'last_logged' => date( 'Y-m-d H:i:s' ),
|
||||||
|
] );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->update( $userId, [
|
||||||
|
'twofa_failed_attempts' => (int)$user['twofa_failed_attempts'] + 1,
|
||||||
|
'last_error_logged' => date( 'Y-m-d H:i:s' ),
|
||||||
|
] );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Zapis / usuwanie
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function save(
|
||||||
|
$userId, string $login, $status, $activeTo, string $password, string $passwordRe,
|
||||||
|
$admin, $privileges, $twofaEnabled = 0, string $twofaEmail = ''
|
||||||
|
): array {
|
||||||
|
$this->db->delete( 'pp_users_privileges', [ 'id_user' => (int)$userId ] );
|
||||||
|
|
||||||
|
if ( !$userId )
|
||||||
|
{
|
||||||
|
if ( strlen( $password ) < 5 )
|
||||||
|
return [ 'status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.' ];
|
||||||
|
if ( $password !== $passwordRe )
|
||||||
|
return [ 'status' => 'error', 'msg' => 'Podane hasła są różne.' ];
|
||||||
|
|
||||||
|
$this->db->insert( 'pp_users', [
|
||||||
|
'login' => $login,
|
||||||
|
'status' => $status == 'on' ? 1 : 0,
|
||||||
|
'active_to' => $activeTo === '' ? null : $activeTo,
|
||||||
|
'admin' => $admin,
|
||||||
|
'password' => md5( $password ),
|
||||||
|
'twofa_enabled' => $twofaEnabled == 'on' ? 1 : 0,
|
||||||
|
'twofa_email' => $twofaEmail,
|
||||||
|
] );
|
||||||
|
$userId = $this->db->get( 'pp_users', 'id', [ 'ORDER' => [ 'id' => 'DESC' ] ] );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ( $password && strlen( $password ) < 5 )
|
||||||
|
return [ 'status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.' ];
|
||||||
|
if ( $password && $password !== $passwordRe )
|
||||||
|
return [ 'status' => 'error', 'msg' => 'Podane hasła są różne.' ];
|
||||||
|
|
||||||
|
if ( $password )
|
||||||
|
$this->db->update( 'pp_users', [ 'password' => md5( $password ) ], [ 'id' => (int)$userId ] );
|
||||||
|
|
||||||
|
$this->db->update( 'pp_users', [
|
||||||
|
'login' => $login,
|
||||||
|
'admin' => $admin,
|
||||||
|
'status' => $status == 'on' ? 1 : 0,
|
||||||
|
'active_to' => $activeTo === '' ? null : $activeTo,
|
||||||
|
'error_logged_count' => 0,
|
||||||
|
'twofa_enabled' => $twofaEnabled == 'on' ? 1 : 0,
|
||||||
|
'twofa_email' => $twofaEmail,
|
||||||
|
], [ 'id' => (int)$userId ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$privileges = (array)$privileges;
|
||||||
|
foreach ( $privileges as $pri )
|
||||||
|
$this->db->insert( 'pp_users_privileges', [ 'name' => $pri, 'id_user' => $userId ] );
|
||||||
|
|
||||||
|
\S::delete_cache();
|
||||||
|
return [ 'status' => 'ok', 'msg' => 'Użytkownik został zapisany.' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete( int $userId ): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->db->delete( 'pp_users', [ 'id' => $userId ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
47
autoload/Shared/Cache/CacheHandler.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shared\Cache;
|
||||||
|
|
||||||
|
class CacheHandler
|
||||||
|
{
|
||||||
|
public static function store( $key, $data, $ttl = 86400 )
|
||||||
|
{
|
||||||
|
file_put_contents( self::get_file_name( $key ), gzdeflate( serialize( array( time() + $ttl, $data ) ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function get_file_name( $key )
|
||||||
|
{
|
||||||
|
$md5 = md5( $key );
|
||||||
|
$dir = 'temp/' . $md5[0] . '/' . $md5[1] . '/';
|
||||||
|
|
||||||
|
if ( !is_dir( $dir ) )
|
||||||
|
mkdir( $dir, 0755, true );
|
||||||
|
|
||||||
|
return $dir . 's_cache_' . $md5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fetch( $key )
|
||||||
|
{
|
||||||
|
$filename = self::get_file_name( $key );
|
||||||
|
|
||||||
|
if ( !file_exists( $filename ) || !is_readable( $filename ) )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
$data = gzinflate( file_get_contents( $filename ) );
|
||||||
|
|
||||||
|
$data = @unserialize( $data );
|
||||||
|
if ( !$data )
|
||||||
|
{
|
||||||
|
unlink( $filename );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( time() > $data[0] )
|
||||||
|
{
|
||||||
|
if ( file_exists( $filename ) )
|
||||||
|
unlink( $filename );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1220
autoload/Shared/Helpers/Helpers.php
Normal file
93
autoload/Shared/Html/Html.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shared\Html;
|
||||||
|
|
||||||
|
class Html
|
||||||
|
{
|
||||||
|
public static function form_text( array $params = array() )
|
||||||
|
{
|
||||||
|
$tpl = new \Shared\Tpl\Tpl;
|
||||||
|
$tpl->params = $params;
|
||||||
|
return $tpl->render( 'html/form-text' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function input_switch( array $params = array() )
|
||||||
|
{
|
||||||
|
$tpl = new \Shared\Tpl\Tpl;
|
||||||
|
$tpl->params = $params;
|
||||||
|
return $tpl->render( 'html/input-switch' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function select( array $params = array() )
|
||||||
|
{
|
||||||
|
$tpl = new \Shared\Tpl\Tpl;
|
||||||
|
$tpl->params = $params;
|
||||||
|
return $tpl->render( 'html/select' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function textarea( array $params = array() )
|
||||||
|
{
|
||||||
|
$defaults = array(
|
||||||
|
'rows' => 4,
|
||||||
|
);
|
||||||
|
|
||||||
|
$params = array_merge( $defaults, $params );
|
||||||
|
|
||||||
|
$tpl = new \Shared\Tpl\Tpl;
|
||||||
|
$tpl->params = $params;
|
||||||
|
return $tpl->render( 'html/textarea' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function input_icon( array $params = array() )
|
||||||
|
{
|
||||||
|
$defaults = array(
|
||||||
|
'type' => 'text',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params = array_merge( $defaults, $params );
|
||||||
|
|
||||||
|
$tpl = new \Shared\Tpl\Tpl;
|
||||||
|
$tpl->params = $params;
|
||||||
|
return $tpl->render( 'html/input-icon' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function input( array $params = array() )
|
||||||
|
{
|
||||||
|
$defaults = array(
|
||||||
|
'type' => 'text',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params = array_merge( $defaults, $params );
|
||||||
|
|
||||||
|
$tpl = new \Shared\Tpl\Tpl;
|
||||||
|
$tpl->params = $params;
|
||||||
|
return $tpl->render( 'html/input' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function button( array $params = array() )
|
||||||
|
{
|
||||||
|
$defaults = array(
|
||||||
|
'class' => 'btn-sm btn-info',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params = array_merge( $defaults, $params );
|
||||||
|
|
||||||
|
$tpl = new \Shared\Tpl\Tpl;
|
||||||
|
$tpl->params = $params;
|
||||||
|
return $tpl->render( 'html/button' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function panel( array $params = array() )
|
||||||
|
{
|
||||||
|
$defaults = array(
|
||||||
|
'title' => 'panel-title',
|
||||||
|
'class' => 'panel-primary',
|
||||||
|
'content' => 'panel-content'
|
||||||
|
);
|
||||||
|
|
||||||
|
$params = array_merge( $defaults, $params );
|
||||||
|
|
||||||
|
$tpl = new \Shared\Tpl\Tpl;
|
||||||
|
$tpl->params = $params;
|
||||||
|
return $tpl->render( 'html/panel' );
|
||||||
|
}
|
||||||
|
}
|
||||||
314
autoload/Shared/Image/ImageManipulator.php
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shared\Image;
|
||||||
|
|
||||||
|
class ImageManipulator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $width;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var resource
|
||||||
|
*/
|
||||||
|
protected $image;
|
||||||
|
protected $img_src;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image manipulator constructor
|
||||||
|
*
|
||||||
|
* @param string $file OPTIONAL Path to image file or image data as string
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($file = null)
|
||||||
|
{
|
||||||
|
if (null !== $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$this->img_src = $file;
|
||||||
|
$this->setImageFile($file);
|
||||||
|
} else {
|
||||||
|
echo 'a'; exit;
|
||||||
|
$this->setImageString($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set image resource from file
|
||||||
|
*
|
||||||
|
* @param string $file Path to image file
|
||||||
|
* @return ImageManipulator for a fluent interface
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function setImageFile($file)
|
||||||
|
{
|
||||||
|
if (!(is_readable($file) && is_file($file))) {
|
||||||
|
throw new \InvalidArgumentException("Image file $file is not readable");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_resource($this->image)) {
|
||||||
|
imagedestroy($this->image);
|
||||||
|
}
|
||||||
|
|
||||||
|
list ($this->width, $this->height, $type) = getimagesize($file);
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case IMAGETYPE_GIF :
|
||||||
|
$this->image = imagecreatefromgif($file);
|
||||||
|
break;
|
||||||
|
case IMAGETYPE_JPEG :
|
||||||
|
$this->image = imagecreatefromjpeg($file);
|
||||||
|
break;
|
||||||
|
case IMAGETYPE_PNG :
|
||||||
|
$this->image = imagecreatefrompng($file);
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
throw new \InvalidArgumentException("Image type $type not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set image resource from string data
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @return ImageManipulator for a fluent interface
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function setImageString($data)
|
||||||
|
{
|
||||||
|
if (is_resource($this->image)) {
|
||||||
|
imagedestroy($this->image);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->image = imagecreatefromstring($data)) {
|
||||||
|
throw new \RuntimeException('Cannot create image from data string');
|
||||||
|
}
|
||||||
|
$this->width = imagesx($this->image);
|
||||||
|
$this->height = imagesy($this->image);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resamples the current image
|
||||||
|
*
|
||||||
|
* @param int $width New width
|
||||||
|
* @param int $height New height
|
||||||
|
* @param bool $constrainProportions Constrain current image proportions when resizing
|
||||||
|
* @return ImageManipulator for a fluent interface
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function resample( $width, $height, $constrainProportions = true )
|
||||||
|
{
|
||||||
|
if (!is_resource($this->image)) {
|
||||||
|
throw new \RuntimeException('No image set');
|
||||||
|
}
|
||||||
|
if ($constrainProportions) {
|
||||||
|
if ($this->height >= $this->width) {
|
||||||
|
$width = round($height / $this->height * $this->width);
|
||||||
|
} else {
|
||||||
|
$height = round($width / $this->width * $this->height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$temp = imagecreatetruecolor($width, $height);
|
||||||
|
|
||||||
|
imagecopyresampled($temp, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height);
|
||||||
|
|
||||||
|
if ( function_exists('exif_read_data') )
|
||||||
|
{
|
||||||
|
$exif = exif_read_data( $this->img_src );
|
||||||
|
if ( $exif && isset($exif['Orientation']) )
|
||||||
|
{
|
||||||
|
$orientation = $exif['Orientation'];
|
||||||
|
if ( $orientation != 1 )
|
||||||
|
{
|
||||||
|
$deg = 0;
|
||||||
|
switch ($orientation)
|
||||||
|
{
|
||||||
|
case 3:
|
||||||
|
$deg = 180;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
$deg = 270;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
$deg = 90;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $deg )
|
||||||
|
$temp = imagerotate( $temp, $deg, 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->_replace($temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enlarge canvas
|
||||||
|
*
|
||||||
|
* @param int $width Canvas width
|
||||||
|
* @param int $height Canvas height
|
||||||
|
* @param array $rgb RGB colour values
|
||||||
|
* @param int $xpos X-Position of image in new canvas, null for centre
|
||||||
|
* @param int $ypos Y-Position of image in new canvas, null for centre
|
||||||
|
* @return ImageManipulator for a fluent interface
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function enlargeCanvas($width, $height, array $rgb = array(), $xpos = null, $ypos = null)
|
||||||
|
{
|
||||||
|
if (!is_resource($this->image)) {
|
||||||
|
throw new \RuntimeException('No image set');
|
||||||
|
}
|
||||||
|
|
||||||
|
$width = max($width, $this->width);
|
||||||
|
$height = max($height, $this->height);
|
||||||
|
|
||||||
|
$temp = imagecreatetruecolor($width, $height);
|
||||||
|
if (count($rgb) == 3) {
|
||||||
|
$bg = imagecolorallocate($temp, $rgb[0], $rgb[1], $rgb[2]);
|
||||||
|
imagefill($temp, 0, 0, $bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $xpos) {
|
||||||
|
$xpos = round(($width - $this->width) / 2);
|
||||||
|
}
|
||||||
|
if (null === $ypos) {
|
||||||
|
$ypos = round(($height - $this->height) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
imagecopy($temp, $this->image, (int) $xpos, (int) $ypos, 0, 0, $this->width, $this->height);
|
||||||
|
return $this->_replace($temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crop image
|
||||||
|
*
|
||||||
|
* @param int|array $x1 Top left x-coordinate of crop box or array of coordinates
|
||||||
|
* @param int $y1 Top left y-coordinate of crop box
|
||||||
|
* @param int $x2 Bottom right x-coordinate of crop box
|
||||||
|
* @param int $y2 Bottom right y-coordinate of crop box
|
||||||
|
* @return ImageManipulator for a fluent interface
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function crop($x1, $y1 = 0, $x2 = 0, $y2 = 0)
|
||||||
|
{
|
||||||
|
if (!is_resource($this->image)) {
|
||||||
|
throw new \RuntimeException('No image set');
|
||||||
|
}
|
||||||
|
if (is_array($x1) && 4 == count($x1)) {
|
||||||
|
list($x1, $y1, $x2, $y2) = $x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$x1 = max($x1, 0);
|
||||||
|
$y1 = max($y1, 0);
|
||||||
|
|
||||||
|
$x2 = min($x2, $this->width);
|
||||||
|
$y2 = min($y2, $this->height);
|
||||||
|
|
||||||
|
$width = $x2 - $x1;
|
||||||
|
$height = $y2 - $y1;
|
||||||
|
|
||||||
|
$temp = imagecreatetruecolor($width, $height);
|
||||||
|
imagecopy($temp, $this->image, 0, 0, $x1, $y1, $width, $height);
|
||||||
|
|
||||||
|
return $this->_replace($temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace current image resource with a new one
|
||||||
|
*
|
||||||
|
* @param resource $res New image resource
|
||||||
|
* @return ImageManipulator for a fluent interface
|
||||||
|
* @throws \UnexpectedValueException
|
||||||
|
*/
|
||||||
|
protected function _replace($res)
|
||||||
|
{
|
||||||
|
if (!is_resource($res)) {
|
||||||
|
throw new \UnexpectedValueException('Invalid resource');
|
||||||
|
}
|
||||||
|
if (is_resource($this->image)) {
|
||||||
|
imagedestroy($this->image);
|
||||||
|
}
|
||||||
|
$this->image = $res;
|
||||||
|
$this->width = imagesx($res);
|
||||||
|
$this->height = imagesy($res);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current image to file
|
||||||
|
*
|
||||||
|
* @param string $fileName
|
||||||
|
* @return void
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function save($fileName, $type = IMAGETYPE_JPEG)
|
||||||
|
{
|
||||||
|
$dir = dirname($fileName);
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
if (!mkdir($dir, 0755, true)) {
|
||||||
|
throw new \RuntimeException('Error creating directory ' . $dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($type) {
|
||||||
|
case IMAGETYPE_GIF :
|
||||||
|
if (!imagegif($this->image, $fileName)) {
|
||||||
|
throw new \RuntimeException;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IMAGETYPE_PNG :
|
||||||
|
if (!imagepng($this->image, $fileName)) {
|
||||||
|
throw new \RuntimeException;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IMAGETYPE_JPEG :
|
||||||
|
default :
|
||||||
|
if (!imagejpeg($this->image, $fileName, 95)) {
|
||||||
|
throw new \RuntimeException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
throw new \RuntimeException('Error saving image file to ' . $fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the GD image resource
|
||||||
|
*
|
||||||
|
* @return resource
|
||||||
|
*/
|
||||||
|
public function getResource()
|
||||||
|
{
|
||||||
|
return $this->image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current image resource width
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getWidth()
|
||||||
|
{
|
||||||
|
return $this->width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current image height
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getHeight()
|
||||||
|
{
|
||||||
|
return $this->height;
|
||||||
|
}
|
||||||
|
}
|
||||||
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] );
|
||||||
|
}
|
||||||
|
}
|
||||||
80
autoload/Shared/Tpl/Tpl.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shared\Tpl;
|
||||||
|
|
||||||
|
class Tpl
|
||||||
|
{
|
||||||
|
protected $dir = 'templates/';
|
||||||
|
protected $vars = array();
|
||||||
|
|
||||||
|
function __construct( $dir = null )
|
||||||
|
{
|
||||||
|
if ( $dir !== null )
|
||||||
|
$this->dir = $dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function view( $file, $values = '' )
|
||||||
|
{
|
||||||
|
$tpl = new self;
|
||||||
|
if ( is_array( $values ) ) foreach ( $values as $key => $val )
|
||||||
|
$tpl->$key = $val;
|
||||||
|
return $tpl->render( $file );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function secureHTML( $val )
|
||||||
|
{
|
||||||
|
$out = stripslashes( $val );
|
||||||
|
$out = str_replace( "'", "'", $out );
|
||||||
|
$out = str_replace( '"', """, $out );
|
||||||
|
$out = str_replace( "<", "<", $out );
|
||||||
|
$out = str_replace( ">", ">", $out );
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render( $file )
|
||||||
|
{
|
||||||
|
if ( file_exists( 'templates_user/' . $file . '.php' ) )
|
||||||
|
{
|
||||||
|
ob_start();
|
||||||
|
include 'templates_user/' . $file . '.php';
|
||||||
|
$out = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
else if ( file_exists( 'templates/' . $file . '.php' ) )
|
||||||
|
{
|
||||||
|
ob_start();
|
||||||
|
include 'templates/' . $file . '.php';
|
||||||
|
$out = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
else if ( file_exists( $file . '.php' ) )
|
||||||
|
{
|
||||||
|
ob_start();
|
||||||
|
include $file . '.php';
|
||||||
|
$out = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return '<div class="alert alert-danger" role="alert">Nie znaleziono pliku widoku: <b>' . $this->dir . $file . '.php</b>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __set( $name, $value )
|
||||||
|
{
|
||||||
|
$this->vars[ $name ] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __isset( $name )
|
||||||
|
{
|
||||||
|
return isset( $this->vars[ $name ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __get( $name )
|
||||||
|
{
|
||||||
|
return $this->vars[ $name ];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,9 +37,11 @@ class Site
|
|||||||
|
|
||||||
if (!\admin\factory\Users::send_twofa_code((int)$user['id']))
|
if (!\admin\factory\Users::send_twofa_code((int)$user['id']))
|
||||||
{
|
{
|
||||||
\S::alert('Nie udało się wysłać kodu 2FA. Spróbuj ponownie.');
|
// E-mail nie dotarł — użytkownik podał poprawne dane, więc przepuszczamy
|
||||||
\S::delete_session('twofa_pending');
|
\S::delete_session('twofa_pending');
|
||||||
header('Location: /admin/');
|
\S::alert('Nie udało się wysłać kodu 2FA — zalogowano bez weryfikacji e-mail.', 'alert-warning');
|
||||||
|
self::finalize_admin_login($user, $domain, $cookie_name, (bool)\S::get('remember'));
|
||||||
|
header('Location: /admin/articles/view_list/');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
|
<?php
|
||||||
namespace admin\factory;
|
namespace admin\factory;
|
||||||
|
|
||||||
class Articles
|
class Articles
|
||||||
{
|
{
|
||||||
public static function duplicate_article( $article_id )
|
private static function repo(): \Domain\Articles\ArticlesRepository
|
||||||
{
|
{
|
||||||
global $mdb, $user;
|
global $mdb;
|
||||||
|
return new \Domain\Articles\ArticlesRepository( $mdb );
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function insert_missing_hash() {
|
public static function duplicate_article( $article_id )
|
||||||
global $mdb;
|
{
|
||||||
|
global $user;
|
||||||
|
return self::repo()->duplicateArticle( $article_id, (int)$user['id'] );
|
||||||
|
}
|
||||||
|
|
||||||
if ( $mdb -> count( 'pp_articles', [ 'hash' => null ] ) ) {
|
public static function insert_missing_hash()
|
||||||
$rows = $mdb -> select( 'pp_articles', [ 'id', 'date_add' ], [ 'hash' => null ] );
|
{
|
||||||
if ( is_array( $rows ) ) foreach ( $rows as $row ) {
|
return self::repo()->insertMissingHash();
|
||||||
$mdb -> update( 'pp_articles', [ 'hash' => md5( $row['id'] . $row['date_add'] ) ], [ 'id' => $row['id'] ] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function files_order_save( $article_id, $order )
|
static public function files_order_save( $article_id, $order )
|
||||||
{
|
{
|
||||||
global $mdb;
|
self::repo()->filesOrderSave( $article_id, $order );
|
||||||
|
|
||||||
$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
|
|
||||||
]
|
|
||||||
] );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function gallery_order_save( $article_id, $order )
|
public static function gallery_order_save( $article_id, $order )
|
||||||
{
|
{
|
||||||
global $mdb;
|
self::repo()->galleryOrderSave( $article_id, $order );
|
||||||
|
|
||||||
$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
|
|
||||||
]
|
|
||||||
] );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function additional_params( $language = 0 )
|
public static function additional_params( $language = 0 )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->additionalParams( $language );
|
||||||
return $mdb -> select( 'pp_articles_additional_params', '*', [ 'AND' => [ 'status' => 1, 'language' => $language ] ] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function image_alt_change( $image_id, $image_alt )
|
public static function image_alt_change( $image_id, $image_alt )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->imageAltChange( $image_id, $image_alt );
|
||||||
$result = $mdb -> update( 'pp_articles_images', [
|
|
||||||
'alt' => $image_alt
|
|
||||||
], [
|
|
||||||
'id' => $image_id
|
|
||||||
] );
|
|
||||||
\S::delete_cache();
|
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function articles_by_date_add( $date_start, $date_end )
|
public static function articles_by_date_add( $date_start, $date_end )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->articlesByDateAdd( $date_start, $date_end );
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function article_url( $article_id )
|
public static function article_url( $article_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->articleUrl( $article_id );
|
||||||
|
|
||||||
$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'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function article_pages( $article_id )
|
public static function article_pages( $article_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->articlePages( $article_id );
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function article_title( $article_id )
|
public static function article_title( $article_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->articleTitle( $article_id );
|
||||||
$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'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function articles_set_archive( $article_id )
|
public static function articles_set_archive( $article_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->articlesSetArchive( $article_id );
|
||||||
$result = $mdb -> update( 'pp_articles', [ 'status' => -1 ], [ 'id' => (int)$article_id ] );
|
|
||||||
\S::htacces();
|
|
||||||
\S::delete_cache();
|
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function file_name_change( $file_id, $file_name )
|
public static function file_name_change( $file_id, $file_name )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->fileNameChange( $file_id, $file_name );
|
||||||
$mdb -> update( 'pp_articles_files', [ 'name' => $file_name ], [ 'id' => (int)$file_id ] );
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function delete_file( $file_id )
|
public static function delete_file( $file_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->deleteFile( $file_id );
|
||||||
$mdb -> update( 'pp_articles_files', [ 'to_delete' => 1 ], [ 'id' => (int)$file_id ] );
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function delete_img( $image_id )
|
public static function delete_img( $image_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->deleteImg( $image_id );
|
||||||
$mdb -> update( 'pp_articles_images', [ 'to_delete' => 1 ], [ 'id' => (int)$image_id ] );
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function article_details( $article_id )
|
public static function article_details( $article_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->articleDetails( $article_id );
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function max_order()
|
public static function max_order()
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->maxOrder();
|
||||||
return $mdb -> max( 'pp_articles_pages', 'o' );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function article_save(
|
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,
|
$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,
|
$meta_keywords, $layout_id, $pages, $noindex, $repeat_entry, $copy_from, $social_icons, $event_date, $tags, $block_direct_access, $priority,
|
||||||
$password, $pixieset, $id_author, $params )
|
$password, $pixieset, $id_author, $params
|
||||||
|
)
|
||||||
{
|
{
|
||||||
|
global $user;
|
||||||
global $mdb, $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,
|
||||||
$event_date = explode( ' - ', $event_date );
|
$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']
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function delete_nonassigned_files()
|
public static function delete_nonassigned_files()
|
||||||
{
|
{
|
||||||
global $mdb;
|
self::repo()->deleteNonassignedFiles();
|
||||||
|
|
||||||
$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 ] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function delete_nonassigned_images()
|
public static function delete_nonassigned_images()
|
||||||
{
|
{
|
||||||
global $mdb;
|
self::repo()->deleteNonassignedImages();
|
||||||
|
|
||||||
$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 ] );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -1,181 +1,64 @@
|
|||||||
<?
|
<?php
|
||||||
namespace admin\factory;
|
namespace admin\factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Wrapper — używaj \Domain\Languages\LanguagesRepository przez DI.
|
||||||
|
*/
|
||||||
class Languages
|
class Languages
|
||||||
{
|
{
|
||||||
public static function available_domains()
|
private static function repo(): \Domain\Languages\LanguagesRepository
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
return $mdb -> query( 'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL GROUP BY domain' ) -> fetchAll( \PDO::FETCH_ASSOC );
|
return new \Domain\Languages\LanguagesRepository( $mdb );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function default_domain()
|
public static function available_domains(): array
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->availableDomains();
|
||||||
$results = $mdb -> query( 'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL AND main_domain = 1' ) -> fetchAll();
|
|
||||||
return $default_domain = $results[0][0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function translation_delete( $translation_id )
|
public static function default_domain(): ?string
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->defaultDomain();
|
||||||
return $mdb -> delete( 'pp_langs_translations', [ 'id' => $translation_id ] );
|
}
|
||||||
|
|
||||||
|
public static function translation_delete( $translation_id ): bool
|
||||||
|
{
|
||||||
|
return self::repo()->translationDelete( (int)$translation_id );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function translation_save( $translation_id, $text, $languages )
|
public static function translation_save( $translation_id, $text, $languages )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->translationSave( $translation_id, (string)$text, (array)$languages );
|
||||||
|
|
||||||
if ( $translation_id )
|
|
||||||
{
|
|
||||||
$mdb -> update( 'pp_langs_translations', [ 'text' => $text ], [ 'id' => $translation_id ] );
|
|
||||||
if ( is_array( $languages ) and !empty( $languages ) ): foreach ( $languages as $key => $val ):
|
|
||||||
$mdb -> update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translation_id ] );
|
|
||||||
endforeach; endif;
|
|
||||||
\S::htacces();
|
|
||||||
\S::delete_cache();
|
|
||||||
return $translation_id;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$mdb -> insert( 'pp_langs_translations', [ 'text' => $text ] );
|
|
||||||
if ( $translation_id = $mdb -> id() )
|
|
||||||
{
|
|
||||||
if ( is_array( $languages ) and !empty( $languages ) ): foreach ( $languages as $key => $val ):
|
|
||||||
$mdb -> update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translation_id ] );
|
|
||||||
endforeach; endif;
|
|
||||||
}
|
|
||||||
\S::htacces();
|
|
||||||
\S::delete_cache();
|
|
||||||
return $translation_id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function translation_details( $translation_id )
|
public static function translation_details( $translation_id ): ?array
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->translationDetails( (int)$translation_id );
|
||||||
return $mdb -> get( 'pp_langs_translations', '*', [ 'id' => $translation_id ] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function language_delete( $language_id )
|
public static function language_delete( $language_id ): bool
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->languageDelete( (string)$language_id );
|
||||||
|
|
||||||
if ( $mdb -> count( 'pp_langs' ) > 1 )
|
|
||||||
{
|
|
||||||
if ( $mdb -> query( 'ALTER TABLE pp_langs_translations DROP ' . $language_id )
|
|
||||||
and
|
|
||||||
$mdb -> delete( 'pp_langs', [ 'id' => $language_id ] )
|
|
||||||
)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function max_order()
|
public static function max_order(): int
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->maxOrder();
|
||||||
return $mdb -> max( 'pp_langs', 'o' );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function language_save( $language_id, $name, $status, $start, $o, $domain, $main_domain )
|
public static function language_save( $language_id, $name, $status, $start, $o, $domain, $main_domain ): string
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->languageSave( (string)$language_id, (string)$name, $status, $start, $o, $domain, $main_domain );
|
||||||
|
|
||||||
if ( $start == 'on' and $status == 'on' and !\S::get_domain( $domain ) )
|
|
||||||
$mdb -> update( 'pp_langs', [
|
|
||||||
'start' => 0
|
|
||||||
], [
|
|
||||||
'id[!]' => $language_id
|
|
||||||
] );
|
|
||||||
|
|
||||||
if ( $start == 'on' and $status == 'on' and \S::get_domain( $domain ) )
|
|
||||||
$mdb -> update( 'pp_langs', [
|
|
||||||
'start' => 0
|
|
||||||
], [
|
|
||||||
'AND' => [ 'id[!]' => $language_id, 'domain' => \S::get_domain( $domain ) ]
|
|
||||||
] );
|
|
||||||
|
|
||||||
if ( $main_domain == 'on' and $domain and $status == 'on' )
|
|
||||||
$mdb -> update( 'pp_langs', [
|
|
||||||
'main_domain' => 0
|
|
||||||
], [
|
|
||||||
' id[!]' => $language_id
|
|
||||||
] );
|
|
||||||
|
|
||||||
if ( $mdb -> count( 'pp_langs', [ 'id' => $language_id ] ) )
|
|
||||||
{
|
|
||||||
$mdb -> update( 'pp_langs', [
|
|
||||||
'status' => $status == 'on' ? 1 : 0,
|
|
||||||
'start' => $start == 'on' ? 1 : 0,
|
|
||||||
'name' => $name,
|
|
||||||
'o' => $o,
|
|
||||||
'domain' => \S::get_domain( $domain ) ? \S::get_domain( $domain ) : null,
|
|
||||||
'main_domain' => $main_domain == 'on' and \S::get_domain( $domain ) ? 1 : 0,
|
|
||||||
], [
|
|
||||||
'id' => $language_id
|
|
||||||
] );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if ( $mdb -> query( 'ALTER TABLE pp_langs_translations ADD ' . strtolower( $language_id ) . ' TEXT NULL DEFAULT NULL' ) )
|
|
||||||
{
|
|
||||||
$mdb -> insert( 'pp_langs', [
|
|
||||||
'id' => strtolower( $language_id ),
|
|
||||||
'name' => $name,
|
|
||||||
'status' => $status == 'on' ? 1 : 0,
|
|
||||||
'start' => $start == 'on' ? 1 : 0,
|
|
||||||
'o' => $o,
|
|
||||||
'domain' => \S::get_domain( $domain ) ? \S::get_domain( $domain ) : null,
|
|
||||||
'main_domain' => $main_domain == 'on' && \S::get_domain( $domain ) ? 1 : 0,
|
|
||||||
] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !$mdb -> count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'domain[!]' => null ] ] ) )
|
|
||||||
{
|
|
||||||
if ( !$mdb -> count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'start' => 1, 'domain' => null ] ] ) )
|
|
||||||
{
|
|
||||||
if ( $id_tmp = $mdb -> get( 'pp_langs', 'id', [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ) )
|
|
||||||
$mdb -> update( 'pp_langs', [ 'start' => 1 ], [ 'id' => $id_tmp ] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$domains = $mdb -> select( 'pp_langs', 'domain', [ 'domain[!]' => null, 'GROUP' => 'domain'] );
|
|
||||||
if ( is_array( $domains ) and !empty( $domains ) )
|
|
||||||
{
|
|
||||||
$mdb -> update( 'pp_langs', [ 'start' => 0 ], [ 'domain' => null ] );
|
|
||||||
foreach ( $domains as $domain )
|
|
||||||
{
|
|
||||||
if ( !$mdb -> count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'start' => 1, 'domain' => $domain ] ] ) )
|
|
||||||
{
|
|
||||||
if ( $id_tmp = $mdb -> get( 'pp_langs', 'id', [ 'AND' => [ 'status' => 1, 'domain' => $domain ], 'ORDER' => [ 'o' => 'ASC' ] ] ) )
|
|
||||||
$mdb -> update( 'pp_langs', [ 'start' => 1 ], [ 'id' => $id_tmp ] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !$mdb -> count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'main_domain' => 1 ] ] ) )
|
|
||||||
{
|
|
||||||
if ( $id_tmp = $mdb -> get( 'pp_langs', 'id', [ 'AND' => [ 'status' => 1, 'domain[!]' => null ], 'ORDER' => [ 'o' => 'ASC' ] ] ) )
|
|
||||||
$mdb -> update( 'pp_langs', [ 'main_domain' => 1 ], [ 'id' => $id_tmp ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
\S::htacces();
|
|
||||||
\S::delete_cache();
|
|
||||||
return $language_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function language_details( $language_id )
|
public static function language_details( $language_id ): ?array
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->languageDetails( (string)$language_id );
|
||||||
return $mdb -> get( 'pp_langs', '*', [ 'id' => $language_id ] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function languages_list()
|
public static function languages_list(): array
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->languagesList();
|
||||||
return $mdb -> select( 'pp_langs', '*', [ 'ORDER' => [ 'o' => 'ASC' ] ] );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
|
||||||
@@ -3,139 +3,36 @@ namespace admin\factory;
|
|||||||
|
|
||||||
class Layouts
|
class Layouts
|
||||||
{
|
{
|
||||||
public static function layout_delete( $layout_id )
|
private static function repo(): \Domain\Layouts\LayoutsRepository
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
if ( $mdb -> count( 'pp_layouts' ) > 1 )
|
return new \Domain\Layouts\LayoutsRepository( $mdb );
|
||||||
return $mdb -> delete( 'pp_layouts', [ 'id' => (int)$layout_id ] );
|
}
|
||||||
return false;
|
|
||||||
|
public static function layout_delete( $layout_id )
|
||||||
|
{
|
||||||
|
return self::repo()->layoutDelete( $layout_id );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function layout_details( $layout_id )
|
public static function layout_details( $layout_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->layoutDetails( $layout_id );
|
||||||
$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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function layout_save( $layout_id, $name, $status, $pages, $html, $css, $js, $m_html, $m_css, $m_js )
|
public static function layout_save( $layout_id, $name, $status, $pages, $html, $css, $js, $m_html, $m_css, $m_js )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->layoutSave( $layout_id, $name, $status, $pages, $html, $css, $js, $m_html, $m_css, $m_js );
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function menus_list()
|
public static function menus_list()
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->menusList();
|
||||||
$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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function layouts_list()
|
public static function layouts_list()
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->layoutsList();
|
||||||
return $mdb -> select( 'pp_layouts', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -1,120 +1,53 @@
|
|||||||
<?
|
<?php
|
||||||
|
|
||||||
namespace admin\factory;
|
namespace admin\factory;
|
||||||
|
|
||||||
class Pages
|
class Pages
|
||||||
{
|
{
|
||||||
|
|
||||||
public static $_page_types = [ 0 => 'pełne artykuły', 1 => 'wprowadzenia', 2 => 'miniaturki', 3 => 'link', 4 => 'kontakt' ];
|
public static $_page_types = [ 0 => 'pełne artykuły', 1 => 'wprowadzenia', 2 => 'miniaturki', 3 => 'link', 4 => 'kontakt' ];
|
||||||
public static $_sort_types = [
|
public static $_sort_types = [
|
||||||
0 => 'data dodania - najstarsze na początku',
|
0 => 'data dodania - najstarsze na początku',
|
||||||
1 => 'data dodania - najnowsze na początku',
|
1 => 'data dodania - najnowsze na początku',
|
||||||
2 => 'data modyfikacji - rosnąco',
|
2 => 'data modyfikacji - rosnąco',
|
||||||
3 => 'data mofyfikacji - malejąco',
|
3 => 'data mofyfikacji - malejąco',
|
||||||
4 => 'ręczne',
|
4 => 'ręczne',
|
||||||
5 => 'alfabetycznie - A - Z',
|
5 => 'alfabetycznie - A - Z',
|
||||||
6 => 'alfabetycznie - Z - A'
|
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 )
|
public static function save_articles_order( $page_id, $articles )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->saveArticlesOrder( $page_id, $articles );
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function page_articles( $page_id )
|
public static function page_articles( $page_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->pageArticles( $page_id );
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function menus_list()
|
public static function menus_list()
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->menusList();
|
||||||
return $mdb -> select( 'pp_menus', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function save_pages_order( $menu_id, $pages )
|
public static function save_pages_order( $menu_id, $pages )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->savePagesOrder( $menu_id, $pages );
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function page_delete( $page_id )
|
public static function page_delete( $page_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->pageDelete( $page_id );
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function max_order()
|
public static function max_order()
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->maxOrder();
|
||||||
return $mdb -> max( 'pp_pages', 'o' );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function page_save(
|
public static function page_save(
|
||||||
@@ -122,388 +55,70 @@ class Pages
|
|||||||
$site_title, $block_direct_access, $cache, $canonical
|
$site_title, $block_direct_access, $cache, $canonical
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
global $mdb;
|
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,
|
||||||
if ( !$parent_id )
|
$site_title, $block_direct_access, $cache, $canonical
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function update_supages_menu_id( $parent_id, $menu_id )
|
public static function update_supages_menu_id( $parent_id, $menu_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
self::repo()->updateSubpagesMenuId( (int) $parent_id, (int) $menu_id );
|
||||||
|
|
||||||
$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 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function generate_seo_link( $title, $page_id, $article_id,
|
public static function generate_seo_link( $title, $page_id, $article_id, $lang, $pid )
|
||||||
$lang, $pid )
|
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->generateSeoLink( $title, $page_id, $article_id, $lang, $pid );
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function google_url_preview( $page_id, $title, $lang, $pid, $id, $seo_link, $language_link = '' )
|
public static function google_url_preview( $page_id, $title, $lang, $pid, $id, $seo_link, $language_link = '' )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->googleUrlPreview( $page_id, $title, $lang, $pid, $id, $seo_link, $language_link );
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function menu_delete( $menu_id )
|
public static function menu_delete( $menu_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->menuDelete( $menu_id );
|
||||||
|
|
||||||
if ( $mdb -> count( 'pp_pages', [ 'menu_id' => (int) $menu_id ] ) )
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return $mdb -> delete( 'pp_menus', [ 'id' => (int) $menu_id ] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function menu_details( $menu_id )
|
public static function menu_details( $menu_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->menuDetails( $menu_id );
|
||||||
return $mdb -> get( 'pp_menus', '*', [ 'id' => (int) $menu_id ] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function menu_save( $menu_id, $name, $status )
|
public static function menu_save( $menu_id, $name, $status )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->menuSave( $menu_id, $name, $status );
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function menu_lists()
|
public static function menu_lists()
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->menuLists();
|
||||||
return $mdb -> select( 'pp_menus', '*', [ 'ORDER' => [ 'id' => 'ASC' ] ] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function page_details( $page_id )
|
public static function page_details( $page_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->pageDetails( $page_id );
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function page_url( $page_id )
|
public static function page_url( $page_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->pageUrl( $page_id );
|
||||||
|
|
||||||
$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'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function page_title( $page_id )
|
public static function page_title( $page_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->pageTitle( $page_id );
|
||||||
|
|
||||||
$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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function page_languages( $page_id )
|
public static function page_languages( $page_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->pageLanguages( $page_id );
|
||||||
return $mdb -> select( 'pp_pages_langs', '*',
|
|
||||||
[ 'AND' => [ 'page_id' => (int) $page_id, 'title[!]' => null ] ] );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function menu_pages( $menu_id, $parent_id = null )
|
public static function menu_pages( $menu_id, $parent_id = null )
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->menuPages( $menu_id, $parent_id );
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
103
autoload/admin/factory/class.Releases.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?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 discover_versions(): int
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
$known = array_flip($mdb->select('pp_update_versions', 'version', []) ?: []);
|
||||||
|
$zips = glob('../updates/*/ver_*.zip') ?: [];
|
||||||
|
$added = 0;
|
||||||
|
foreach ($zips as $path) {
|
||||||
|
preg_match('/ver_([0-9.]+)\.zip$/', $path, $m);
|
||||||
|
if (!$m) continue;
|
||||||
|
$ver = $m[1];
|
||||||
|
if (isset($known[$ver])) continue;
|
||||||
|
$mdb->insert('pp_update_versions', [
|
||||||
|
'version' => $ver,
|
||||||
|
'channel' => 'beta',
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
$known[$ver] = true;
|
||||||
|
$added++;
|
||||||
|
}
|
||||||
|
return $added;
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,147 +1,73 @@
|
|||||||
<?
|
<?php
|
||||||
namespace admin\factory;
|
namespace admin\factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Wrapper — używaj \Domain\Settings\SettingsRepository przez DI.
|
||||||
|
*/
|
||||||
class Settings
|
class Settings
|
||||||
{
|
{
|
||||||
public static function settings_update( $param, $value )
|
private static function repo(): \Domain\Settings\SettingsRepository
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
return new \Domain\Settings\SettingsRepository( $mdb );
|
||||||
|
}
|
||||||
|
|
||||||
if ( $mdb -> count( 'pp_settings', [ 'param' => $param ] ) )
|
public static function settings_details(): array
|
||||||
return $mdb -> update( 'pp_settings', [ 'value' => $value ], [ 'param' => $param ] );
|
{
|
||||||
else
|
return self::repo()->allSettings();
|
||||||
return $mdb -> insert( 'pp_settings', [ 'param' => $param, 'value' => $value ] );
|
}
|
||||||
|
|
||||||
|
public static function settings_update( $param, $value )
|
||||||
|
{
|
||||||
|
return self::repo()->update( (string)$param, $value );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function settings_save(
|
public static function settings_save(
|
||||||
$firm_name, $firm_adress, $additional_info, $contact_form, $contact_email, $email_host, $email_port, $email_login, $email_password, $google_maps,
|
$firm_name, $firm_adress, $additional_info, $contact_form, $contact_email,
|
||||||
$facebook_link, $statistic_code, $htaccess, $robots, $newsletter_header, $newsletter_footer_1, $newsletter_footer_2, $google_map_key, $google_search_console, $update, $devel,
|
$email_host, $email_port, $email_login, $email_password, $google_maps,
|
||||||
$news_limit, $visit_counter, $calendar, $tags, $ssl, $mysql_debug, $htaccess_cache, $visits, $links_structure, $link_version, $widget_phone, $update_key )
|
$facebook_link, $statistic_code, $htaccess, $robots,
|
||||||
{
|
$newsletter_header, $newsletter_footer_1, $newsletter_footer_2,
|
||||||
global $mdb;
|
$google_map_key, $google_search_console, $update, $devel,
|
||||||
|
$news_limit, $visit_counter, $calendar, $tags, $ssl, $mysql_debug,
|
||||||
|
$htaccess_cache, $visits, $links_structure, $link_version,
|
||||||
|
$widget_phone, $update_key
|
||||||
|
): bool {
|
||||||
|
$data = [
|
||||||
|
'firm_name' => $firm_name,
|
||||||
|
'firm_adress' => $firm_adress,
|
||||||
|
'additional_info' => $additional_info,
|
||||||
|
'contact_form' => $contact_form,
|
||||||
|
'contact_email' => $contact_email,
|
||||||
|
'email_host' => $email_host,
|
||||||
|
'email_port' => $email_port,
|
||||||
|
'email_login' => $email_login,
|
||||||
|
'email_password' => $email_password,
|
||||||
|
'google_maps' => $google_maps == 'on' ? 1 : 0,
|
||||||
|
'facebook_link' => $facebook_link,
|
||||||
|
'statistic_code' => $statistic_code,
|
||||||
|
'htaccess' => $htaccess,
|
||||||
|
'robots' => $robots,
|
||||||
|
'newsletter_header' => $newsletter_header,
|
||||||
|
'newsletter_footer_1' => $newsletter_footer_1,
|
||||||
|
'newsletter_footer_2' => $newsletter_footer_2,
|
||||||
|
'google_map_key' => $google_map_key,
|
||||||
|
'google_search_console'=> $google_search_console,
|
||||||
|
'update' => $update == 'on' ? 1 : 0,
|
||||||
|
'devel' => $devel == 'on' ? 1 : 0,
|
||||||
|
'news_limit' => $news_limit,
|
||||||
|
'visit_counter' => $visit_counter == 'on' ? 1 : 0,
|
||||||
|
'calendar' => $calendar == 'on' ? 1 : 0,
|
||||||
|
'tags' => $tags == 'on' ? 1 : 0,
|
||||||
|
'ssl' => $ssl == 'on' ? 1 : 0,
|
||||||
|
'mysql_debug' => $mysql_debug == 'on' ? 1 : 0,
|
||||||
|
'htaccess_cache' => $htaccess_cache == 'on' ? 1 : 0,
|
||||||
|
'visits' => $visits,
|
||||||
|
'links_structure' => $links_structure,
|
||||||
|
'link_version' => $link_version,
|
||||||
|
'widget_phone' => $widget_phone == 'on' ? 1 : 0,
|
||||||
|
'update_key' => $update_key,
|
||||||
|
];
|
||||||
|
|
||||||
$mdb -> query( 'TRUNCATE pp_settings' );
|
return self::repo()->save( $data );
|
||||||
|
|
||||||
$mdb -> insert( 'pp_settings', [
|
|
||||||
[
|
|
||||||
'param' => 'firm_name',
|
|
||||||
'value' => $firm_name,
|
|
||||||
], [
|
|
||||||
'param' => 'firm_adress',
|
|
||||||
'value' => $firm_adress
|
|
||||||
], [
|
|
||||||
'param' => 'additional_info',
|
|
||||||
'value' => $additional_info
|
|
||||||
], [
|
|
||||||
'param' => 'contact_form',
|
|
||||||
'value' => $contact_form
|
|
||||||
], [
|
|
||||||
'param' => 'contact_email',
|
|
||||||
'value' => $contact_email
|
|
||||||
], [
|
|
||||||
'param' => 'email_host',
|
|
||||||
'value' => $email_host
|
|
||||||
], [
|
|
||||||
'param' => 'email_port',
|
|
||||||
'value' => $email_port
|
|
||||||
], [
|
|
||||||
'param' => 'email_login',
|
|
||||||
'value' => $email_login
|
|
||||||
], [
|
|
||||||
'param' => 'email_password',
|
|
||||||
'value' => $email_password
|
|
||||||
], [
|
|
||||||
'param' => 'google_maps',
|
|
||||||
'value' => $google_maps == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
"param" => 'facebook_link',
|
|
||||||
'value' => $facebook_link
|
|
||||||
], [
|
|
||||||
'param' => 'statistic_code',
|
|
||||||
'value' => $statistic_code
|
|
||||||
], [
|
|
||||||
'param' => 'htaccess',
|
|
||||||
'value' => $htaccess
|
|
||||||
], [
|
|
||||||
'param' => 'robots',
|
|
||||||
'value' => $robots
|
|
||||||
], [
|
|
||||||
'param' => 'newsletter_header',
|
|
||||||
'value' => $newsletter_header
|
|
||||||
], [
|
|
||||||
'param' => 'newsletter_footer_1',
|
|
||||||
'value' => $newsletter_footer_1
|
|
||||||
], [
|
|
||||||
'param' => 'newsletter_footer_2',
|
|
||||||
'value' => $newsletter_footer_2
|
|
||||||
], [
|
|
||||||
'param' => 'google_map_key',
|
|
||||||
'value' => $google_map_key
|
|
||||||
], [
|
|
||||||
'param' => 'google_search_console',
|
|
||||||
'value' => $google_search_console
|
|
||||||
], [
|
|
||||||
'param' => 'update',
|
|
||||||
'value' => $update == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
'param' => 'devel',
|
|
||||||
'value' => $devel == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
'param' => 'news_limit',
|
|
||||||
'value' => $news_limit
|
|
||||||
], [
|
|
||||||
'param' => 'visit_counter',
|
|
||||||
'value' => $visit_counter == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
'param' => 'calendar',
|
|
||||||
'value' => $calendar == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
'param' => 'tags',
|
|
||||||
'value' => $tags == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
'param' => 'ssl',
|
|
||||||
'value' => $ssl == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
'param' => 'mysql_debug',
|
|
||||||
'value' => $mysql_debug == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
'param' => 'htaccess_cache',
|
|
||||||
'value' => $htaccess_cache == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
'param' => 'visits',
|
|
||||||
'value' => $visits
|
|
||||||
], [
|
|
||||||
'param' => 'links_structure',
|
|
||||||
'value' => $links_structure
|
|
||||||
], [
|
|
||||||
'param' => 'link_version',
|
|
||||||
'value' => $link_version
|
|
||||||
], [
|
|
||||||
'param' => 'widget_phone',
|
|
||||||
'value' => $widget_phone == 'on' ? 1 : 0
|
|
||||||
], [
|
|
||||||
'param' => 'update_key',
|
|
||||||
'value' => $update_key
|
|
||||||
]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
\S::set_message( 'Ustawienia zostały zapisane' );
|
|
||||||
\S::delete_cache();
|
|
||||||
\S::htacces();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function settings_details()
|
|
||||||
{
|
|
||||||
global $mdb;
|
|
||||||
|
|
||||||
$results = $mdb -> select( 'pp_settings', '*', [ 'ORDER' => [ 'id' => 'ASC' ] ] );
|
|
||||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
|
||||||
$settings[$row['param']] = $row['value'];
|
|
||||||
|
|
||||||
return $settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
?>
|
|
||||||
@@ -1,306 +1,83 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace admin\factory;
|
namespace admin\factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Wrapper — używaj \Domain\User\UserRepository przez DI.
|
||||||
|
*/
|
||||||
class Users
|
class Users
|
||||||
{
|
{
|
||||||
public static function user_delete($user_id)
|
private static function repo(): \Domain\User\UserRepository
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
return new \Domain\User\UserRepository( $mdb );
|
||||||
return $mdb->delete('pp_users', ['id' => (int)$user_id]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function user_details($user_id)
|
public static function user_delete( $user_id ): bool
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->delete( (int)$user_id );
|
||||||
return $mdb->get('pp_users', '*', ['id' => (int)$user_id]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function user_privileges($user_id)
|
public static function user_details( $user_id ): ?array
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->find( (int)$user_id );
|
||||||
return $mdb->select('pp_users_privileges', '*', ['id_user' => (int)$user_id]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function user_save($user_id, $login, $status, $active_to, $password, $password_re, $admin, $privileges, $twofa_enabled = 0, $twofa_email = '' )
|
public static function user_privileges( $user_id ): array
|
||||||
{
|
{
|
||||||
global $mdb, $lang;
|
return self::repo()->privileges( (int)$user_id );
|
||||||
|
|
||||||
$mdb->delete('pp_users_privileges', ['id_user' => (int) $user_id]);
|
|
||||||
|
|
||||||
if (!$user_id)
|
|
||||||
{
|
|
||||||
if (strlen($password) < 5)
|
|
||||||
return $response = ['status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.'];
|
|
||||||
|
|
||||||
if ($password != $password_re)
|
|
||||||
return $response = ['status' => 'error', 'msg' => 'Podane hasła są różne'];
|
|
||||||
|
|
||||||
if ($mdb->insert(
|
|
||||||
'pp_users',
|
|
||||||
[
|
|
||||||
'login' => $login,
|
|
||||||
'status' => $status == 'on' ? 1 : 0,
|
|
||||||
'active_to' => $active_to == '' ? NULL : $active_to,
|
|
||||||
'admin' => $admin,
|
|
||||||
'password' => md5($password),
|
|
||||||
'twofa_enabled' => $twofa_enabled == 'on' ? 1 : 0,
|
|
||||||
'twofa_email' => $twofa_email
|
|
||||||
]
|
|
||||||
))
|
|
||||||
$id_user = $mdb->get('pp_users', 'id', ['ORDER' => ['id' => 'DESC']]);
|
|
||||||
|
|
||||||
if (is_array($privileges))
|
|
||||||
{
|
|
||||||
foreach ($privileges as $pri)
|
|
||||||
{
|
|
||||||
$mdb->insert(
|
|
||||||
'pp_users_privileges',
|
|
||||||
[
|
|
||||||
'name' => $pri,
|
|
||||||
'id_user' => $id_user
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$mdb->insert(
|
|
||||||
'pp_users_privileges',
|
|
||||||
[
|
|
||||||
'name' => $privileges,
|
|
||||||
'id_user' => $id_user
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $response = ['status' => 'ok', 'msg' => 'Użytkownik został zapisany.'];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
if ($password and strlen($password) < 5)
|
|
||||||
return $response = ['status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.'];
|
|
||||||
|
|
||||||
if ($password and $password != $password_re)
|
|
||||||
return $response = ['status' => 'error', 'msg' => 'Podane hasła są różne'];
|
|
||||||
|
|
||||||
if ($password)
|
|
||||||
$mdb->update('pp_users', [
|
|
||||||
'password' => md5($password)
|
|
||||||
], [
|
|
||||||
'id' => (int) $user_id
|
|
||||||
]);
|
|
||||||
|
|
||||||
$mdb->update('pp_users', [
|
|
||||||
'login' => $login,
|
|
||||||
'admin' => $admin,
|
|
||||||
'status' => $status == 'on' ? 1 : 0,
|
|
||||||
'active_to' => $active_to == '' ? NULL : $active_to,
|
|
||||||
'error_logged_count' => 0,
|
|
||||||
'twofa_enabled' => $twofa_enabled == 'on' ? 1 : 0,
|
|
||||||
'twofa_email' => $twofa_email
|
|
||||||
], [
|
|
||||||
'id' => (int) $user_id
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (is_array($privileges))
|
|
||||||
{
|
|
||||||
foreach ($privileges as $pri)
|
|
||||||
{
|
|
||||||
$mdb->insert('pp_users_privileges', [
|
|
||||||
'name' => $pri,
|
|
||||||
'id_user' => $user_id
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$mdb->insert('pp_users_privileges', [
|
|
||||||
'name' => $privileges,
|
|
||||||
'id_user' => $user_id
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return $response = ['status' => 'ok', 'msg' => 'Uzytkownik został zapisany.'];
|
|
||||||
}
|
|
||||||
\S::delete_cache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function check_login($login, $user_id)
|
public static function user_save(
|
||||||
{
|
$user_id, $login, $status, $active_to, $password, $password_re,
|
||||||
global $mdb;
|
$admin, $privileges, $twofa_enabled = 0, $twofa_email = ''
|
||||||
|
): array {
|
||||||
if ($mdb->get('pp_users', 'login', ['AND' => ['login' => $login, 'id[!]' => (int)$user_id]]))
|
return self::repo()->save(
|
||||||
return $response = ['status' => 'error', 'msg' => 'Podany login jest już zajęty.'];
|
$user_id, (string)$login, $status, $active_to,
|
||||||
|
(string)$password, (string)$password_re,
|
||||||
return $response = ['status' => 'ok'];
|
$admin, $privileges, $twofa_enabled, (string)$twofa_email
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function logon($login, $password)
|
public static function check_login( $login, $user_id ): array
|
||||||
{
|
{
|
||||||
global $mdb;
|
if ( self::repo()->isLoginTaken( (string)$login, (int)$user_id ) )
|
||||||
|
return [ 'status' => 'error', 'msg' => 'Podany login jest już zajęty.' ];
|
||||||
|
|
||||||
if (!$mdb->get('pp_users', '*', ['login' => $login]))
|
return [ 'status' => 'ok' ];
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (!$mdb->get('pp_users', '*', ['AND' => ['login' => $login, 'status' => 1, 'error_logged_count[<]' => 5]]))
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
if ($mdb->get('pp_users', '*', [
|
|
||||||
'AND' => [
|
|
||||||
'login' => $login,
|
|
||||||
'status' => 1,
|
|
||||||
'password' => md5($password),
|
|
||||||
'OR' => ['active_to[>=]' => date('Y-m-d'), 'active_to' => null]
|
|
||||||
]
|
|
||||||
]))
|
|
||||||
{
|
|
||||||
$mdb->update('pp_users', ['last_logged' => date('Y-m-d H:i:s'), 'error_logged_count' => 0], ['login' => $login]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$mdb->update('pp_users', ['last_error_logged' => date('Y-m-d H:i:s'), 'error_logged_count[+]' => 1], ['login' => $login]);
|
|
||||||
if ($mdb->get('pp_users', 'error_logged_count', ['login' => $login]) >= 5)
|
|
||||||
{
|
|
||||||
$mdb->update('pp_users', ['status' => 0], ['login' => $login]);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function details($login)
|
public static function logon( $login, $password ): int
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->logon( (string)$login, (string)$password );
|
||||||
return $mdb->get('pp_users', '*', ['login' => $login]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function check_privileges($name, $user_id)
|
public static function details( $login ): ?array
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->findByLogin( (string)$login );
|
||||||
|
|
||||||
if ($user_id == 1)
|
|
||||||
return true;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!$privilages = \Cache::fetch("check_privileges:$user_id:$name-tmp"))
|
|
||||||
{
|
|
||||||
$privilages = $mdb->count('pp_users_privileges', ['AND' => ['name' => $name, 'id_user' => (int)$user_id]]);
|
|
||||||
\Cache::store("check_privileges:$user_id:$name", $privilages);
|
|
||||||
}
|
|
||||||
return $privilages;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function get_by_id(int $userId): ?array
|
public static function check_privileges( $name, $user_id ): bool
|
||||||
{
|
{
|
||||||
|
return self::repo()->hasPrivilege( (string)$name, (int)$user_id );
|
||||||
global $mdb;
|
|
||||||
return $mdb->get('pp_users', '*', ['id' => $userId]) ?: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function send_twofa_code(int $userId, bool $resend = false): bool
|
public static function get_by_id( int $userId ): ?array
|
||||||
{
|
{
|
||||||
|
return self::repo()->find( $userId );
|
||||||
$user = self::get_by_id($userId);
|
|
||||||
if (!$user)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if ((int)$user['twofa_enabled'] !== 1)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$to = $user['twofa_email'] ?: $user['login'];
|
|
||||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($resend && !empty($user['twofa_sent_at']))
|
|
||||||
{
|
|
||||||
$last = strtotime($user['twofa_sent_at']);
|
|
||||||
if ($last && (time() - $last) < 30)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$code = random_int(100000, 999999);
|
|
||||||
$hash = password_hash((string)$code, PASSWORD_DEFAULT);
|
|
||||||
|
|
||||||
self::update_by_id($userId, [
|
|
||||||
'twofa_code_hash' => $hash,
|
|
||||||
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 10 * 60), // 10 minut
|
|
||||||
'twofa_sent_at' => date('Y-m-d H:i:s'),
|
|
||||||
'twofa_failed_attempts' => 0,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$subject = 'Twój kod logowania 2FA';
|
|
||||||
$body = "Twój kod logowania do panelu administratora: {$code}. Kod jest ważny przez 10 minut. Jeśli to nie Ty inicjowałeś logowanie – zignoruj tę wiadomość i poinformuj administratora.";
|
|
||||||
|
|
||||||
$sent = \S::send_email($to, $subject, $body);
|
|
||||||
|
|
||||||
if (!$sent) {
|
|
||||||
$headers = "MIME-Version: 1.0\r\n";
|
|
||||||
$headers .= "Content-type: text/plain; charset=UTF-8\r\n";
|
|
||||||
$headers .= "From: no-reply@" . ($_SERVER['HTTP_HOST'] ?? 'localhost') . "\r\n";
|
|
||||||
$encodedSubject = mb_encode_mimeheader($subject, 'UTF-8');
|
|
||||||
|
|
||||||
$sent = mail($to, $encodedSubject, $body, $headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $sent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function update_by_id(int $userId, array $data): bool
|
public static function send_twofa_code( int $userId, bool $resend = false ): bool
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->sendTwofaCode( $userId, $resend );
|
||||||
return (bool)$mdb->update('pp_users', $data, ['id' => $userId]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function verify_twofa_code(int $userId, string $code): bool
|
public static function update_by_id( int $userId, array $data ): bool
|
||||||
{
|
{
|
||||||
$user = self::get_by_id( $userId );
|
return self::repo()->update( $userId, $data );
|
||||||
if (!$user) return false;
|
}
|
||||||
|
|
||||||
if ((int)$user['twofa_failed_attempts'] >= 5)
|
public static function verify_twofa_code( int $userId, string $code ): bool
|
||||||
{
|
{
|
||||||
return false; // zbyt wiele prób
|
return self::repo()->verifyTwofaCode( $userId, $code );
|
||||||
}
|
|
||||||
|
|
||||||
// sprawdź ważność
|
|
||||||
if (empty($user['twofa_expires_at']) || time() > strtotime($user['twofa_expires_at']))
|
|
||||||
{
|
|
||||||
// wyczyść po wygaśnięciu
|
|
||||||
self::update_by_id($userId, [
|
|
||||||
'twofa_code_hash' => null,
|
|
||||||
'twofa_expires_at' => null,
|
|
||||||
]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ok = (!empty($user['twofa_code_hash']) && password_verify($code, $user['twofa_code_hash']));
|
|
||||||
if ($ok)
|
|
||||||
{
|
|
||||||
// sukces: czyścimy wszystko
|
|
||||||
self::update_by_id($userId, [
|
|
||||||
'twofa_code_hash' => null,
|
|
||||||
'twofa_expires_at' => null,
|
|
||||||
'twofa_sent_at' => null,
|
|
||||||
'twofa_failed_attempts' => 0,
|
|
||||||
'last_logged' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// zła próba — inkrementacja
|
|
||||||
self::update_by_id($userId, [
|
|
||||||
'twofa_failed_attempts' => (int)$user['twofa_failed_attempts'] + 1,
|
|
||||||
'last_error_logged' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
@@ -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' );
|
||||||
@@ -1,46 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
class Cache
|
/**
|
||||||
|
* Wrapper delegujący do \Shared\Cache\CacheHandler.
|
||||||
|
* Zachowany dla wstecznej kompatybilności — stopniowo zastępować \Cache na \Shared\Cache\CacheHandler.
|
||||||
|
*/
|
||||||
|
class Cache extends \Shared\Cache\CacheHandler
|
||||||
{
|
{
|
||||||
public static function store( $key, $data, $ttl = 86400 )
|
|
||||||
{
|
|
||||||
file_put_contents( self::get_file_name( $key ), gzdeflate( serialize( array( time() + $ttl, $data ) ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function get_file_name( $key )
|
|
||||||
{
|
|
||||||
$md5 = md5( $key );
|
|
||||||
$dir = 'temp/' . $md5[0] . '/' . $md5[1] . '/';
|
|
||||||
|
|
||||||
if ( !is_dir( $dir ) )
|
|
||||||
mkdir( $dir , 0755 , true );
|
|
||||||
|
|
||||||
return $dir . 's_cache_' . $md5;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fetch( $key )
|
|
||||||
{
|
|
||||||
$filename = self::get_file_name( $key );
|
|
||||||
|
|
||||||
if ( !file_exists( $filename ) || !is_readable( $filename ) )
|
|
||||||
return false;
|
|
||||||
|
|
||||||
$data = gzinflate( file_get_contents( $filename ) );
|
|
||||||
|
|
||||||
$data = @unserialize( $data );
|
|
||||||
if ( !$data )
|
|
||||||
{
|
|
||||||
unlink( $filename );
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( time() > $data[0] )
|
|
||||||
{
|
|
||||||
if ( file_exists( $filename ) )
|
|
||||||
unlink( $filename );
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
?>
|
|
||||||
@@ -1,91 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
class Html
|
/**
|
||||||
|
* Wrapper delegujący do \Shared\Html\Html.
|
||||||
|
* Zachowany dla wstecznej kompatybilności — stopniowo zastępować \Html na \Shared\Html\Html.
|
||||||
|
*/
|
||||||
|
class Html extends \Shared\Html\Html
|
||||||
{
|
{
|
||||||
public static function form_text( array $params = array() )
|
|
||||||
{
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
$tpl -> params = $params;
|
|
||||||
return $tpl -> render( 'html/form-text' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function input_switch( array $params = array() )
|
|
||||||
{
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
$tpl -> params = $params;
|
|
||||||
return $tpl -> render( 'html/input-switch' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function select( array $params = array() )
|
|
||||||
{
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
$tpl -> params = $params;
|
|
||||||
return $tpl -> render( 'html/select' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function textarea( array $params = array() )
|
|
||||||
{
|
|
||||||
$defaults = array(
|
|
||||||
'rows' => 4,
|
|
||||||
);
|
|
||||||
|
|
||||||
$params = array_merge( $defaults, $params );
|
|
||||||
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
$tpl -> params = $params;
|
|
||||||
return $tpl -> render( 'html/textarea' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function input_icon( array $params = array() )
|
|
||||||
{
|
|
||||||
$defaults = array(
|
|
||||||
'type' => 'text',
|
|
||||||
);
|
|
||||||
|
|
||||||
$params = array_merge( $defaults, $params );
|
|
||||||
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
$tpl -> params = $params;
|
|
||||||
return $tpl -> render( 'html/input-icon' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function input( array $params = array() )
|
|
||||||
{
|
|
||||||
$defaults = array(
|
|
||||||
'type' => 'text',
|
|
||||||
);
|
|
||||||
|
|
||||||
$params = array_merge( $defaults, $params );
|
|
||||||
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
$tpl -> params = $params;
|
|
||||||
return $tpl -> render( 'html/input' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function button( array $params = array() )
|
|
||||||
{
|
|
||||||
$defaults = array(
|
|
||||||
'class' => 'btn-sm btn-info',
|
|
||||||
);
|
|
||||||
|
|
||||||
$params = array_merge( $defaults, $params );
|
|
||||||
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
$tpl -> params = $params;
|
|
||||||
return $tpl -> render( 'html/button' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function panel( array $params = array() )
|
|
||||||
{
|
|
||||||
$defaults = array(
|
|
||||||
'title' => 'panel-title',
|
|
||||||
'class' => 'panel-primary',
|
|
||||||
'content' => 'panel-content'
|
|
||||||
);
|
|
||||||
|
|
||||||
$params = array_merge( $defaults, $params );
|
|
||||||
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
$tpl -> params = $params;
|
|
||||||
return $tpl -> render( 'html/panel' );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,312 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
class ImageManipulator
|
/**
|
||||||
|
* Wrapper delegujący do \Shared\Image\ImageManipulator.
|
||||||
|
* Zachowany dla wstecznej kompatybilności — stopniowo zastępować new ImageManipulator na new \Shared\Image\ImageManipulator.
|
||||||
|
*/
|
||||||
|
class ImageManipulator extends \Shared\Image\ImageManipulator
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $width;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $height;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var resource
|
|
||||||
*/
|
|
||||||
protected $image;
|
|
||||||
protected $img_src;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Image manipulator constructor
|
|
||||||
*
|
|
||||||
* @param string $file OPTIONAL Path to image file or image data as string
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct($file = null)
|
|
||||||
{
|
|
||||||
if (null !== $file) {
|
|
||||||
if (is_file($file)) {
|
|
||||||
$this -> img_src = $file;
|
|
||||||
$this->setImageFile($file);
|
|
||||||
} else {
|
|
||||||
echo 'a'; exit;
|
|
||||||
$this->setImageString($file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set image resource from file
|
|
||||||
*
|
|
||||||
* @param string $file Path to image file
|
|
||||||
* @return ImageManipulator for a fluent interface
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
*/
|
|
||||||
public function setImageFile($file)
|
|
||||||
{
|
|
||||||
if (!(is_readable($file) && is_file($file))) {
|
|
||||||
throw new InvalidArgumentException("Image file $file is not readable");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_resource($this->image)) {
|
|
||||||
imagedestroy($this->image);
|
|
||||||
}
|
|
||||||
|
|
||||||
list ($this->width, $this->height, $type) = getimagesize($file);
|
|
||||||
|
|
||||||
switch ($type) {
|
|
||||||
case IMAGETYPE_GIF :
|
|
||||||
$this->image = imagecreatefromgif($file);
|
|
||||||
break;
|
|
||||||
case IMAGETYPE_JPEG :
|
|
||||||
$this->image = imagecreatefromjpeg($file);
|
|
||||||
break;
|
|
||||||
case IMAGETYPE_PNG :
|
|
||||||
$this->image = imagecreatefrompng($file);
|
|
||||||
break;
|
|
||||||
default :
|
|
||||||
throw new InvalidArgumentException("Image type $type not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set image resource from string data
|
|
||||||
*
|
|
||||||
* @param string $data
|
|
||||||
* @return ImageManipulator for a fluent interface
|
|
||||||
* @throws RuntimeException
|
|
||||||
*/
|
|
||||||
public function setImageString($data)
|
|
||||||
{
|
|
||||||
if (is_resource($this->image)) {
|
|
||||||
imagedestroy($this->image);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->image = imagecreatefromstring($data)) {
|
|
||||||
throw new RuntimeException('Cannot create image from data string');
|
|
||||||
}
|
|
||||||
$this->width = imagesx($this->image);
|
|
||||||
$this->height = imagesy($this->image);
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resamples the current image
|
|
||||||
*
|
|
||||||
* @param int $width New width
|
|
||||||
* @param int $height New height
|
|
||||||
* @param bool $constrainProportions Constrain current image proportions when resizing
|
|
||||||
* @return ImageManipulator for a fluent interface
|
|
||||||
* @throws RuntimeException
|
|
||||||
*/
|
|
||||||
public function resample( $width, $height, $constrainProportions = true )
|
|
||||||
{
|
|
||||||
if (!is_resource($this->image)) {
|
|
||||||
throw new RuntimeException('No image set');
|
|
||||||
}
|
|
||||||
if ($constrainProportions) {
|
|
||||||
if ($this->height >= $this->width) {
|
|
||||||
$width = round($height / $this->height * $this->width);
|
|
||||||
} else {
|
|
||||||
$height = round($width / $this->width * $this->height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$temp = imagecreatetruecolor($width, $height);
|
|
||||||
|
|
||||||
imagecopyresampled($temp, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height);
|
|
||||||
|
|
||||||
if ( function_exists('exif_read_data') )
|
|
||||||
{
|
|
||||||
$exif = exif_read_data( $this -> img_src );
|
|
||||||
if ( $exif && isset($exif['Orientation']) )
|
|
||||||
{
|
|
||||||
$orientation = $exif['Orientation'];
|
|
||||||
if ( $orientation != 1 )
|
|
||||||
{
|
|
||||||
$deg = 0;
|
|
||||||
switch ($orientation)
|
|
||||||
{
|
|
||||||
case 3:
|
|
||||||
$deg = 180;
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
$deg = 270;
|
|
||||||
break;
|
|
||||||
case 8:
|
|
||||||
$deg = 90;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $deg )
|
|
||||||
$temp = imagerotate( $temp, $deg, 0 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $this->_replace($temp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enlarge canvas
|
|
||||||
*
|
|
||||||
* @param int $width Canvas width
|
|
||||||
* @param int $height Canvas height
|
|
||||||
* @param array $rgb RGB colour values
|
|
||||||
* @param int $xpos X-Position of image in new canvas, null for centre
|
|
||||||
* @param int $ypos Y-Position of image in new canvas, null for centre
|
|
||||||
* @return ImageManipulator for a fluent interface
|
|
||||||
* @throws RuntimeException
|
|
||||||
*/
|
|
||||||
public function enlargeCanvas($width, $height, array $rgb = array(), $xpos = null, $ypos = null)
|
|
||||||
{
|
|
||||||
if (!is_resource($this->image)) {
|
|
||||||
throw new RuntimeException('No image set');
|
|
||||||
}
|
|
||||||
|
|
||||||
$width = max($width, $this->width);
|
|
||||||
$height = max($height, $this->height);
|
|
||||||
|
|
||||||
$temp = imagecreatetruecolor($width, $height);
|
|
||||||
if (count($rgb) == 3) {
|
|
||||||
$bg = imagecolorallocate($temp, $rgb[0], $rgb[1], $rgb[2]);
|
|
||||||
imagefill($temp, 0, 0, $bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $xpos) {
|
|
||||||
$xpos = round(($width - $this->width) / 2);
|
|
||||||
}
|
|
||||||
if (null === $ypos) {
|
|
||||||
$ypos = round(($height - $this->height) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
imagecopy($temp, $this->image, (int) $xpos, (int) $ypos, 0, 0, $this->width, $this->height);
|
|
||||||
return $this->_replace($temp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crop image
|
|
||||||
*
|
|
||||||
* @param int|array $x1 Top left x-coordinate of crop box or array of coordinates
|
|
||||||
* @param int $y1 Top left y-coordinate of crop box
|
|
||||||
* @param int $x2 Bottom right x-coordinate of crop box
|
|
||||||
* @param int $y2 Bottom right y-coordinate of crop box
|
|
||||||
* @return ImageManipulator for a fluent interface
|
|
||||||
* @throws RuntimeException
|
|
||||||
*/
|
|
||||||
public function crop($x1, $y1 = 0, $x2 = 0, $y2 = 0)
|
|
||||||
{
|
|
||||||
if (!is_resource($this->image)) {
|
|
||||||
throw new RuntimeException('No image set');
|
|
||||||
}
|
|
||||||
if (is_array($x1) && 4 == count($x1)) {
|
|
||||||
list($x1, $y1, $x2, $y2) = $x1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$x1 = max($x1, 0);
|
|
||||||
$y1 = max($y1, 0);
|
|
||||||
|
|
||||||
$x2 = min($x2, $this->width);
|
|
||||||
$y2 = min($y2, $this->height);
|
|
||||||
|
|
||||||
$width = $x2 - $x1;
|
|
||||||
$height = $y2 - $y1;
|
|
||||||
|
|
||||||
$temp = imagecreatetruecolor($width, $height);
|
|
||||||
imagecopy($temp, $this->image, 0, 0, $x1, $y1, $width, $height);
|
|
||||||
|
|
||||||
return $this->_replace($temp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace current image resource with a new one
|
|
||||||
*
|
|
||||||
* @param resource $res New image resource
|
|
||||||
* @return ImageManipulator for a fluent interface
|
|
||||||
* @throws UnexpectedValueException
|
|
||||||
*/
|
|
||||||
protected function _replace($res)
|
|
||||||
{
|
|
||||||
if (!is_resource($res)) {
|
|
||||||
throw new UnexpectedValueException('Invalid resource');
|
|
||||||
}
|
|
||||||
if (is_resource($this->image)) {
|
|
||||||
imagedestroy($this->image);
|
|
||||||
}
|
|
||||||
$this->image = $res;
|
|
||||||
$this->width = imagesx($res);
|
|
||||||
$this->height = imagesy($res);
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save current image to file
|
|
||||||
*
|
|
||||||
* @param string $fileName
|
|
||||||
* @return void
|
|
||||||
* @throws RuntimeException
|
|
||||||
*/
|
|
||||||
public function save($fileName, $type = IMAGETYPE_JPEG)
|
|
||||||
{
|
|
||||||
$dir = dirname($fileName);
|
|
||||||
if (!is_dir($dir)) {
|
|
||||||
if (!mkdir($dir, 0755, true)) {
|
|
||||||
throw new RuntimeException('Error creating directory ' . $dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch ($type) {
|
|
||||||
case IMAGETYPE_GIF :
|
|
||||||
if (!imagegif($this->image, $fileName)) {
|
|
||||||
throw new RuntimeException;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case IMAGETYPE_PNG :
|
|
||||||
if (!imagepng($this->image, $fileName)) {
|
|
||||||
throw new RuntimeException;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case IMAGETYPE_JPEG :
|
|
||||||
default :
|
|
||||||
if (!imagejpeg($this->image, $fileName, 95)) {
|
|
||||||
throw new RuntimeException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception $ex) {
|
|
||||||
throw new RuntimeException('Error saving image file to ' . $fileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the GD image resource
|
|
||||||
*
|
|
||||||
* @return resource
|
|
||||||
*/
|
|
||||||
public function getResource()
|
|
||||||
{
|
|
||||||
return $this->image;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current image resource width
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getWidth()
|
|
||||||
{
|
|
||||||
return $this->width;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current image height
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHeight()
|
|
||||||
{
|
|
||||||
return $this->height;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
1257
autoload/class.S.php
@@ -1,73 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
class Tpl
|
/**
|
||||||
|
* Wrapper delegujący do \Shared\Tpl\Tpl.
|
||||||
|
* Zachowany dla wstecznej kompatybilności — stopniowo zastępować \Tpl na \Shared\Tpl\Tpl.
|
||||||
|
*/
|
||||||
|
class Tpl extends \Shared\Tpl\Tpl
|
||||||
{
|
{
|
||||||
protected $dir = 'templates/';
|
|
||||||
protected $vars = array();
|
|
||||||
|
|
||||||
function __construct( $dir = null )
|
|
||||||
{
|
|
||||||
if ( $dir !== null )
|
|
||||||
$this -> dir = $dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function view( $file, $values = '' )
|
|
||||||
{
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
if ( is_array( $values ) ) foreach ( $values as $key => $val )
|
|
||||||
$tpl -> $key = $val;
|
|
||||||
return $tpl -> render( $file );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function secureHTML( $val )
|
|
||||||
{
|
|
||||||
$out = stripslashes( $val );
|
|
||||||
$out = str_replace( "'", "'", $out );
|
|
||||||
$out = str_replace( '"', """, $out );
|
|
||||||
$out = str_replace( "<", "<", $out );
|
|
||||||
$out = str_replace( ">", ">", $out );
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render( $file )
|
|
||||||
{
|
|
||||||
if ( file_exists( 'templates_user/' . $file . '.php' ) )
|
|
||||||
{
|
|
||||||
ob_start();
|
|
||||||
include 'templates_user/' . $file . '.php';
|
|
||||||
$out = ob_get_contents();
|
|
||||||
ob_end_clean();
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
else if ( file_exists( 'templates/' . $file . '.php' ) )
|
|
||||||
{
|
|
||||||
ob_start();
|
|
||||||
include 'templates/' . $file . '.php';
|
|
||||||
$out = ob_get_contents();
|
|
||||||
ob_end_clean();
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
else if ( file_exists( $file . '.php' ) )
|
|
||||||
{
|
|
||||||
ob_start();
|
|
||||||
include $file . '.php';
|
|
||||||
$out = ob_get_contents();
|
|
||||||
ob_end_clean();
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return '<div class="alert alert-danger" role="alert">Nie znaleziono pliku widoku: <b>' . $this -> dir . $file . '.php</b>';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __set( $name, $value )
|
|
||||||
{
|
|
||||||
$this -> vars[ $name ] = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __get( $name )
|
|
||||||
{
|
|
||||||
return $this -> vars[ $name ];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,34 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace front\factory;
|
namespace front\factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Wrapper — używaj \Domain\Languages\LanguagesRepository przez DI.
|
||||||
|
*/
|
||||||
class Languages
|
class Languages
|
||||||
{
|
{
|
||||||
public static function default_domain()
|
private static function repo(): \Domain\Languages\LanguagesRepository
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
$results = $mdb -> query( 'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL AND main_domain = 1' ) -> fetchAll();
|
return new \Domain\Languages\LanguagesRepository( $mdb );
|
||||||
return $default_domain = $results[0][0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function default_language( $domain = '' )
|
public static function default_domain(): ?string
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->defaultDomain();
|
||||||
if ( !$default_language = \Cache::fetch( "default_language:$domain" ) )
|
|
||||||
{
|
|
||||||
if ( $domain )
|
|
||||||
$results = $mdb -> query( 'SELECT id FROM pp_langs WHERE status = 1 AND domain = \'' . $domain . '\' ORDER BY start DESC, o ASC LIMIT 1' ) -> fetchAll();
|
|
||||||
if ( !$domain or !\front\factory\Languages::default_domain() )
|
|
||||||
$results = $mdb -> query( 'SELECT id FROM pp_langs WHERE status = 1 AND domain IS NULL ORDER BY start DESC, o ASC LIMIT 1' ) -> fetchAll();
|
|
||||||
$default_language = $results[0][0];
|
|
||||||
|
|
||||||
\Cache::store( "default_language:$domain", $default_language );
|
|
||||||
}
|
|
||||||
return $default_language;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function active_languages()
|
public static function default_language( $domain = '' ): ?string
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->defaultLanguage( (string)$domain );
|
||||||
|
|
||||||
if ( !$active_languages = \Cache::fetch( 'active_languages' ) )
|
|
||||||
{
|
|
||||||
$active_languages = $mdb -> select( 'pp_langs', [ 'id', 'name', 'domain' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
|
||||||
\Cache::store( 'active_languages', $active_languages );
|
|
||||||
}
|
|
||||||
return $active_languages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function lang_translations( $language = 'pl' )
|
public static function active_languages(): array
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->activeLanguages();
|
||||||
|
}
|
||||||
|
|
||||||
if ( !$translations = \Cache::fetch( "lang_translations:$language" ) )
|
public static function lang_translations( $language = 'pl' ): array
|
||||||
{
|
{
|
||||||
$translations[ '0' ] = $language;
|
return self::repo()->langTranslations( (string)$language );
|
||||||
|
|
||||||
$results = $mdb -> select( 'pp_langs_translations', [ 'text', $language ] );
|
|
||||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
|
||||||
$translations[ $row['text'] ] = $row[ $language ];
|
|
||||||
|
|
||||||
\Cache::store( "lang_translations:$language", $translations );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $translations;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace front\factory;
|
namespace front\factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Wrapper — używaj \Domain\Settings\SettingsRepository przez DI.
|
||||||
|
*/
|
||||||
class Settings
|
class Settings
|
||||||
{
|
{
|
||||||
public static function settings_details()
|
private static function repo(): \Domain\Settings\SettingsRepository
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
return new \Domain\Settings\SettingsRepository( $mdb );
|
||||||
if ( !$settings = \Cache::fetch( 'settings_details' ) )
|
|
||||||
{
|
|
||||||
$results = $mdb -> select( 'pp_settings', '*' );
|
|
||||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
|
||||||
$settings[ $row['param'] ] = $row['value'];
|
|
||||||
|
|
||||||
\Cache::store( 'settings_details', $settings );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function visit_counter()
|
public static function settings_details(): array
|
||||||
{
|
{
|
||||||
global $mdb;
|
return self::repo()->allSettings();
|
||||||
return $mdb -> get( 'pp_settings', 'value', [ 'param' => 'visits'] );
|
}
|
||||||
|
|
||||||
|
public static function visit_counter(): ?string
|
||||||
|
{
|
||||||
|
return self::repo()->visitCounter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
BIN
composer.phar
Normal file
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
$database['host'] = 'localhost';
|
$database['host'] = 'localhost';
|
||||||
|
$database['host_remote'] = 'host117523.hostido.net.pl';
|
||||||
$database['user'] = 'host117523_cmspro';
|
$database['user'] = 'host117523_cmspro';
|
||||||
$database['password'] = '3sJADeqKHLqHddfavDeR';
|
$database['password'] = '3sJADeqKHLqHddfavDeR';
|
||||||
$database['name'] = 'host117523_cmspro';
|
$database['name'] = 'host117523_cmspro';
|
||||||
|
|||||||
11
cron.php
@@ -1,15 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING );
|
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING );
|
||||||
function __autoload_my_classes( $classname )
|
require_once __DIR__ . '/autoload/autoloader.php';
|
||||||
{
|
|
||||||
$q = explode( '\\' , $classname );
|
|
||||||
$c = array_pop( $q );
|
|
||||||
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
|
||||||
|
|
||||||
if ( file_exists( $f ) )
|
|
||||||
require_once( $f );
|
|
||||||
}
|
|
||||||
spl_autoload_register( '__autoload_my_classes' );
|
|
||||||
date_default_timezone_set( 'Europe/Warsaw' );
|
date_default_timezone_set( 'Europe/Warsaw' );
|
||||||
|
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
|
|||||||
178
docs/FORM_EDIT_SYSTEM.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Form Edit System - Dokumentacja użycia
|
||||||
|
|
||||||
|
## Architektura
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Controller │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ edit() │ │ save() │ │
|
||||||
|
│ │ - buduje VM │ │ - walidacja │ │
|
||||||
|
│ │ - renderuje │ │ - zapis │ │
|
||||||
|
│ └────────┬────────┘ └─────────────────┘ │
|
||||||
|
└───────────┼─────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ FormEditViewModel │
|
||||||
|
│ - title, formId, data, fields, tabs, actions │
|
||||||
|
│ - validationErrors, persist, languages │
|
||||||
|
└───────────┬─────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ components/form-edit.php (szablon) │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ FormFieldRenderer - renderuje każde pole │ │
|
||||||
|
│ │ ├─ input, select, textarea, switch │ │
|
||||||
|
│ │ ├─ date, datetime, editor, image │ │
|
||||||
|
│ │ └─ lang_section (zagnieżdżone pola) │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pliki systemu
|
||||||
|
|
||||||
|
| Plik | Opis |
|
||||||
|
|------|------|
|
||||||
|
| `autoload/admin/ViewModels/Forms/FormFieldType.php` | Stale typow pol |
|
||||||
|
| `autoload/admin/ViewModels/Forms/FormField.php` | Factory methods per typ |
|
||||||
|
| `autoload/admin/ViewModels/Forms/FormTab.php` | Zakladki |
|
||||||
|
| `autoload/admin/ViewModels/Forms/FormAction.php` | Akcje (zapisz, anuluj) |
|
||||||
|
| `autoload/admin/ViewModels/Forms/FormEditViewModel.php` | ViewModel formularza |
|
||||||
|
| `autoload/admin/Support/Forms/FormValidator.php` | Walidacja pol |
|
||||||
|
| `autoload/admin/Support/Forms/FormRequestHandler.php` | Obsluga POST + persist |
|
||||||
|
| `autoload/admin/Support/Forms/FormFieldRenderer.php` | Renderowanie HTML |
|
||||||
|
| `admin/templates/components/form-edit.php` | Uniwersalny szablon |
|
||||||
|
|
||||||
|
## Przykład użycia w kontrolerze
|
||||||
|
|
||||||
|
```php
|
||||||
|
use admin\ViewModels\Forms\FormEditViewModel;
|
||||||
|
use admin\ViewModels\Forms\FormField;
|
||||||
|
use admin\ViewModels\Forms\FormTab;
|
||||||
|
use admin\ViewModels\Forms\FormAction;
|
||||||
|
use admin\Support\Forms\FormRequestHandler;
|
||||||
|
|
||||||
|
class BannerController
|
||||||
|
{
|
||||||
|
public function edit(): string
|
||||||
|
{
|
||||||
|
$banner = $this->repository->find($id);
|
||||||
|
$languages = \admin\factory\Languages::languages_list();
|
||||||
|
|
||||||
|
$viewModel = new FormEditViewModel(
|
||||||
|
formId: 'banner-edit',
|
||||||
|
title: 'Edycja banera',
|
||||||
|
data: $banner,
|
||||||
|
tabs: [
|
||||||
|
new FormTab('settings', 'Ustawienia', 'fa-wrench'),
|
||||||
|
new FormTab('content', 'Zawartość', 'fa-file'),
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
// Zakładka Ustawienia
|
||||||
|
FormField::text('name', [
|
||||||
|
'label' => 'Nazwa',
|
||||||
|
'tab' => 'settings',
|
||||||
|
'required' => true,
|
||||||
|
]),
|
||||||
|
FormField::switch('status', [
|
||||||
|
'label' => 'Aktywny',
|
||||||
|
'tab' => 'settings',
|
||||||
|
]),
|
||||||
|
FormField::date('date_start', [
|
||||||
|
'label' => 'Data rozpoczęcia',
|
||||||
|
'tab' => 'settings',
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Sekcja językowa w zakładce Zawartość
|
||||||
|
FormField::langSection('translations', 'content', [
|
||||||
|
FormField::image('src', ['label' => 'Obraz']),
|
||||||
|
FormField::text('url', ['label' => 'Url']),
|
||||||
|
FormField::editor('text', ['label' => 'Treść']),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
FormAction::save('/admin/banners/save', '/admin/banners'),
|
||||||
|
FormAction::cancel('/admin/banners'),
|
||||||
|
],
|
||||||
|
languages: $languages,
|
||||||
|
persist: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return \Tpl::view('components/form-edit', ['form' => $viewModel]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(): void
|
||||||
|
{
|
||||||
|
$formHandler = new FormRequestHandler();
|
||||||
|
$viewModel = $this->buildFormViewModel(); // jak w edit()
|
||||||
|
|
||||||
|
$result = $formHandler->handleSubmit($viewModel, $_POST);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
// Błędy walidacji - zapisane automatycznie do sesji
|
||||||
|
echo json_encode(['success' => false, 'errors' => $result['errors']]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sukces - persist wyczyszczony automatycznie
|
||||||
|
$this->repository->save($result['data']);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dostępne typy pól
|
||||||
|
|
||||||
|
| Typ | Metoda | Opcje |
|
||||||
|
|-----|--------|-------|
|
||||||
|
| `text` | `FormField::text(name, ['label' => '...', 'required' => true])` | placeholder, help |
|
||||||
|
| `number` | `FormField::number(name, [...])` | - |
|
||||||
|
| `email` | `FormField::email(name, [...])` | walidacja formatu |
|
||||||
|
| `password` | `FormField::password(name, [...])` | - |
|
||||||
|
| `date` | `FormField::date(name, [...])` | datetimepicker |
|
||||||
|
| `datetime` | `FormField::datetime(name, [...])` | datetimepicker z czasem |
|
||||||
|
| `switch` | `FormField::switch(name, [...])` | checked (bool) |
|
||||||
|
| `select` | `FormField::select(name, ['options' => [...]])` | options: [key => label] |
|
||||||
|
| `textarea` | `FormField::textarea(name, ['rows' => 4])` | rows |
|
||||||
|
| `editor` | `FormField::editor(name, ['toolbar' => 'MyTool'])` | CKEditor |
|
||||||
|
| `image` | `FormField::image(name, ['filemanager' => true])` | filemanager URL |
|
||||||
|
| `file` | `FormField::file(name, [...])` | filemanager |
|
||||||
|
| `hidden` | `FormField::hidden(name, value)` | - |
|
||||||
|
| `color` | `FormField::color(name, ['label' => '...'])` | HTML5 color picker + text input |
|
||||||
|
| `lang_section` | `FormField::langSection(name, 'tab', [fields])` | pola per język |
|
||||||
|
|
||||||
|
## Walidacja
|
||||||
|
|
||||||
|
Walidacja jest automatyczna na podstawie właściwości pól:
|
||||||
|
- `required` - pole wymagane
|
||||||
|
- `type` = `email` - walidacja formatu e-mail
|
||||||
|
- `type` = `number` - walidacja liczby
|
||||||
|
- `type` = `date` - walidacja formatu YYYY-MM-DD
|
||||||
|
|
||||||
|
Dla sekcji językowych walidacja jest powtarzana dla każdego aktywnego języka.
|
||||||
|
|
||||||
|
## Persist (zapamiętywanie danych)
|
||||||
|
|
||||||
|
Gdy `persist = true`:
|
||||||
|
1. Przy błędzie walidacji dane są zapisywane w `$_SESSION['form_persist'][$formId]`
|
||||||
|
2. Formularz automatycznie przywraca dane z sesji przy ponownym wyświetleniu
|
||||||
|
3. Po udanym zapisie sesja jest czyszczona automatycznie przez `FormRequestHandler`
|
||||||
|
|
||||||
|
## Przerabianie istniejących formularzy
|
||||||
|
|
||||||
|
1. **Kontroler** - zamień `view\Xxx::edit()` na `FormEditViewModel`
|
||||||
|
2. **Repository** - dostosuj `save()` do formatu z `FormRequestHandler` (lub dodaj wsparcie dla obu formatów)
|
||||||
|
3. **Szablon** - usuń stary szablon lub zostaw jako fallback
|
||||||
|
4. **Testy** - zaktualizuj testy jeśli zmienił się format danych
|
||||||
|
|
||||||
|
## Aktualizacja 2026-02-15 (ver. 0.275)
|
||||||
|
|
||||||
|
- Modul `ShopCategory` zostal zmigrowany do warstwy Domain + DI, ale formularz kategorii nadal korzysta z legacy `gridEdit`.
|
||||||
|
- W ramach migracji wydzielono skrypty UI do osobnych partiali `*-custom-script.php` (lista, browse, edycja, produkty), co upraszcza dalsze przepiecie formularza na `components/form-edit`.
|
||||||
|
- Po migracji `ShopCategory` kolejnym kandydatem do pelnej migracji formularza na Form Edit System pozostaje modul `Order` (zgodnie z `REFACTORING_PLAN.md`).
|
||||||
|
|
||||||
|
---
|
||||||
|
*Dokument aktualizowany: 2026-02-15*
|
||||||
24
docs/MEMORY.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Pamięć projektu cnsPRO
|
||||||
|
|
||||||
|
Notatki i wnioski zebrane podczas pracy z kodem. Aktualizowane na bieżąco.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Serwer produkcyjny
|
||||||
|
|
||||||
|
- PHP < 8.0 — unikać `match`, named arguments, union types, `str_contains()` itp.
|
||||||
|
- Zamiast `match` używać operatorów trójargumentowych (ternary) lub `if/else`
|
||||||
|
|
||||||
|
## Redis cache — konwencje
|
||||||
|
|
||||||
|
- TTL domyślnie 86400 (24h)
|
||||||
|
- Klucze produktów: `shop\product:{id}:{lang}:{permutation_hash}`
|
||||||
|
- Wzorzec czyszczenia: `CacheHandler::deletePattern("shop\\product:{$id}:*")`
|
||||||
|
- Dane w cache są serializowane — wymagają `unserialize()` po `get()`
|
||||||
|
|
||||||
|
## Aktualizacje klienckie
|
||||||
|
|
||||||
|
- Pliki `*.md` NIGDY nie trafiają do ZIP aktualizacji
|
||||||
|
- `updates/changelog.php` to plik serwisowy repozytorium, nie runtime klienta
|
||||||
|
- Główny `.htaccess` wdrażany osobno, poza ZIP aktualizacji
|
||||||
|
- W archiwum ZIP NIE powinno być folderu z nazwą wersji — struktura zaczyna się od katalogów projektu
|
||||||
126
docs/PROJECT_STRUCTURE.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Struktura projektu cmsPRO
|
||||||
|
|
||||||
|
## Punkty wejścia
|
||||||
|
|
||||||
|
| Plik | Opis |
|
||||||
|
|------|------|
|
||||||
|
| `index.php` | Router frontendu |
|
||||||
|
| `admin/index.php` | Router panelu admina |
|
||||||
|
| `ajax.php` | AJAX frontend |
|
||||||
|
| `admin/ajax.php` | AJAX admin |
|
||||||
|
| `api.php` | Publiczne API |
|
||||||
|
| `cron.php` | Zadania cykliczne (newsletter) |
|
||||||
|
| `download.php` | Chronione pobieranie plików |
|
||||||
|
|
||||||
|
Każdy punkt wejścia ładuje centralny autoloader (hybrydowy PSR-4 + legacy):
|
||||||
|
```php
|
||||||
|
require_once __DIR__ . '/autoload/autoloader.php';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wzorzec architektoniczny — Static Factory (MVCish)
|
||||||
|
|
||||||
|
```
|
||||||
|
autoload/{admin|front}/
|
||||||
|
├── controls/class.{Module}.php ← obsługa requestów
|
||||||
|
├── factory/class.{Module}.php ← logika biznesowa + DB
|
||||||
|
└── view/class.{Module}.php ← generowanie HTML
|
||||||
|
```
|
||||||
|
|
||||||
|
Przestrzenie nazw: `\admin\controls`, `\admin\factory`, `\admin\view`,
|
||||||
|
`\front\controls`, `\front\factory`, `\front\view`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Refaktoryzacja DDD — stan aktualny
|
||||||
|
|
||||||
|
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
|
||||||
|
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/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/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
autoload/Domain/
|
||||||
|
├── Languages/LanguagesRepository.php ← \Domain\Languages\LanguagesRepository ✓
|
||||||
|
├── Settings/SettingsRepository.php ← \Domain\Settings\SettingsRepository ✓
|
||||||
|
├── User/UserRepository.php ← \Domain\User\UserRepository ✓
|
||||||
|
├── Pages/PagesRepository.php ← \Domain\Pages\PagesRepository ✓
|
||||||
|
├── Layouts/LayoutsRepository.php ← \Domain\Layouts\LayoutsRepository ✓
|
||||||
|
└── Articles/ArticlesRepository.php ← \Domain\Articles\ArticlesRepository ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
Następne: `Domain\Scontainers`, `Domain\Banners`, `Domain\Authors`, `Domain\Newsletter`, ...
|
||||||
|
---
|
||||||
|
|
||||||
|
## Katalogi
|
||||||
|
|
||||||
|
| Katalog | Zawartość |
|
||||||
|
|---------|-----------|
|
||||||
|
| `autoload/` | Klasy PHP (modele, kontrolery, fabryki, widoki, Shared, Domain) |
|
||||||
|
| `admin/templates/` | Szablony panelu admina (17 modułów) |
|
||||||
|
| `templates/` | Szablony frontendu (systemowe, tylko do odczytu) |
|
||||||
|
| `templates_user/` | Szablony frontendu (nadpisywalne przez użytkownika) |
|
||||||
|
| `layout/` | SCSS → CSS (style.scss → style.css) |
|
||||||
|
| `upload/` | Pliki użytkownika (article_images/, article_files/, filemanager/) |
|
||||||
|
| `libraries/` | Zewnętrzne biblioteki (Medoo, CKEditor, Bootstrap, jQuery…) |
|
||||||
|
| `plugins/` | Hooki (special-actions.php, -middle.php, -end.php) |
|
||||||
|
| `migrations/` | Pliki SQL per wersja (np. `0.304.sql`) |
|
||||||
|
| `updates/` | Paczki ZIP aktualizacji |
|
||||||
|
| `temp/` | Cache plikowy (gzip, 24h, generowany automatycznie) |
|
||||||
|
| `docs/` | Dokumentacja techniczna |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kluczowe klasy
|
||||||
|
|
||||||
|
| Klasa | Opis |
|
||||||
|
|-------|------|
|
||||||
|
| `\Shared\Helpers\Helpers` (`class.S.php`) | Megautylita: sesja, cookie, email, SEO, detekcja botów |
|
||||||
|
| `\Shared\Tpl\Tpl` (`class.Tpl.php`) | Silnik szablonów |
|
||||||
|
| `\Shared\Cache\CacheHandler` (`class.Cache.php`) | Cache plikowy |
|
||||||
|
| `\Shared\Html\Html` (`class.Html.php`) | Builder komponentów formularzy |
|
||||||
|
| `\Shared\Image\ImageManipulator` (`class.Image.php`) | Manipulacja obrazami + WebP |
|
||||||
|
| `class.Article.php` | Model artykułu (ArrayAccess, lazy multilang) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Baza danych
|
||||||
|
|
||||||
|
Prefiks tabel: `pp_`. ORM: Medoo (globalny `$mdb`). Konfiguracja: `config.php`.
|
||||||
|
|
||||||
|
Główne tabele: `pp_users`, `pp_articles`, `pp_articles_langs`, `pp_pages`,
|
||||||
|
`pp_pages_langs`, `pp_languages`, `pp_settings`, `pp_newsletter`,
|
||||||
|
`pp_newsletter_users`, `pp_tags`, `pp_banners`, `pp_layouts`, `pp_backups`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System wielojęzyczny
|
||||||
|
|
||||||
|
- Sesja: `$_SESSION['current-lang']`
|
||||||
|
- Tabela: `pp_languages`
|
||||||
|
- Składnia w treści: `[LANG:klucz]`
|
||||||
|
- Cache tłumaczeń: `$_SESSION['lang-{lang_id}']`
|
||||||
|
|
||||||
66
docs/TESTING.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Testowanie cmsPRO
|
||||||
|
|
||||||
|
## Szybki start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instalacja PHPUnit (jednorazowo)
|
||||||
|
composer install
|
||||||
|
|
||||||
|
# Uruchomienie testów
|
||||||
|
./vendor/bin/phpunit
|
||||||
|
|
||||||
|
# Konkretny plik
|
||||||
|
./vendor/bin/phpunit tests/Unit/Domain/Settings/SettingsRepositoryTest.php
|
||||||
|
|
||||||
|
# Konkretny test
|
||||||
|
./vendor/bin/phpunit --filter testAllSettingsReturnsMappedArray
|
||||||
|
```
|
||||||
|
|
||||||
|
## Aktualny stan
|
||||||
|
|
||||||
|
```text
|
||||||
|
Testy jednostkowe dla Domain\ (Faza 2 DDD)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguracja
|
||||||
|
|
||||||
|
- **PHPUnit 10** via `composer`
|
||||||
|
- **Bootstrap:** `tests/bootstrap.php`
|
||||||
|
- **Config:** `phpunit.xml`
|
||||||
|
|
||||||
|
## Struktura testów
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── bootstrap.php ← autoloader + stuby (CacheHandler, S)
|
||||||
|
└── Unit/
|
||||||
|
└── Domain/
|
||||||
|
├── Languages/LanguagesRepositoryTest.php
|
||||||
|
├── Settings/SettingsRepositoryTest.php
|
||||||
|
└── User/UserRepositoryTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stuby (bootstrap.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.
|
||||||
|
|
||||||
|
## Mockowanie Medoo
|
||||||
|
|
||||||
|
```php
|
||||||
|
$db = $this->createMock(\medoo::class);
|
||||||
|
$db->method('get')->willReturn(['id' => 1]);
|
||||||
|
|
||||||
|
$repo = new SettingsRepository($db);
|
||||||
|
$value = $repo->visitCounter();
|
||||||
|
|
||||||
|
$this->assertSame('1', $value);
|
||||||
|
```
|
||||||
73
docs/UPDATE_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Instrukcja tworzenia aktualizacji shopPRO
|
||||||
|
|
||||||
|
## Nowy sposób (od v0.301) — automatyczny build script
|
||||||
|
|
||||||
|
### Wymagania
|
||||||
|
- Git z tagami wersji (np. `v0.299`, `v0.300`)
|
||||||
|
- PowerShell
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Pracuj normalnie: commit, push, commit, push...
|
||||||
|
2. Gdy wersja gotowa:
|
||||||
|
→ git tag v0.XXX
|
||||||
|
→ ./build-update.ps1 -ToTag v0.XXX -ChangelogEntry "NEW - opis"
|
||||||
|
3. Upload plików z updates/0.XX/ na serwer aktualizacji
|
||||||
|
```
|
||||||
|
|
||||||
|
### Użycie build-update.ps1
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Podgląd zmian (bez tworzenia plików)
|
||||||
|
./build-update.ps1 -ToTag v0.301 -DryRun
|
||||||
|
|
||||||
|
# Budowanie paczki (auto-detect poprzedniego tagu)
|
||||||
|
./build-update.ps1 -ToTag v0.301 -ChangelogEntry "NEW - opis zmiany"
|
||||||
|
|
||||||
|
# Z jawnym tagiem źródłowym
|
||||||
|
./build-update.ps1 -FromTag v0.300 -ToTag v0.301 -ChangelogEntry "NEW - opis"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Co robi skrypt automatycznie
|
||||||
|
1. `git diff --name-status` między tagami → listy dodanych/zmodyfikowanych/usuniętych plików
|
||||||
|
2. Filtrowanie przez `.updateignore` (pliki deweloperskie, konfiguracyjne itp.)
|
||||||
|
3. Kopiowanie plików do temp, tworzenie ZIP
|
||||||
|
4. SHA256 checksum ZIP-a
|
||||||
|
5. Generowanie `ver_X.XXX_manifest.json`
|
||||||
|
6. Generowanie legacy `_sql.txt` i `_files.txt` (okres przejściowy)
|
||||||
|
7. Aktualizacja `versions.php` i `changelog.php`
|
||||||
|
8. Cleanup
|
||||||
|
|
||||||
|
### Pliki wynikowe
|
||||||
|
- `updates/0.XX/ver_X.XXX.zip` — paczka z plikami
|
||||||
|
- `updates/0.XX/ver_X.XXX_manifest.json` — manifest z checksumem, listą zmian, SQL
|
||||||
|
- `updates/0.XX/ver_X.XXX_sql.txt` — legacy SQL (okres przejściowy)
|
||||||
|
- `updates/0.XX/ver_X.XXX_files.txt` — legacy lista plików do usunięcia (okres przejściowy)
|
||||||
|
|
||||||
|
### Migracje SQL
|
||||||
|
Pliki SQL umieszczaj w `migrations/{version}.sql` (np. `migrations/0.301.sql`).
|
||||||
|
Build script automatycznie je wczyta i umieści w manifeście + legacy `_sql.txt`.
|
||||||
|
|
||||||
|
### Format manifestu
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "0.301",
|
||||||
|
"date": "2026-02-22",
|
||||||
|
"checksum_zip": "sha256:abc123...",
|
||||||
|
"files": {
|
||||||
|
"modified": ["autoload/Domain/Order/OrderRepository.php"],
|
||||||
|
"added": ["autoload/Domain/Order/NewHelper.php"],
|
||||||
|
"deleted": ["autoload/shop/OldClass.php"]
|
||||||
|
},
|
||||||
|
"directories_deleted": [],
|
||||||
|
"sql": ["ALTER TABLE pp_x ADD COLUMN y INT DEFAULT 0"],
|
||||||
|
"changelog": "NEW - opis zmiany"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### .updateignore
|
||||||
|
Plik w katalogu głównym projektu, wzorce plików wykluczonych z paczek (jak `.gitignore`).
|
||||||
|
|
||||||
|
### INFO
|
||||||
|
pamiętaj że push czasem zwraca błąd autoryzacji, wtedy spróbuj ponownie
|
||||||
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
@@ -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
@@ -1,15 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
error_reporting( E_ALL & ~E_NOTICE & ~E_WARNING );
|
error_reporting( E_ALL & ~E_NOTICE & ~E_WARNING );
|
||||||
function __autoload_my_classes( $classname )
|
require_once __DIR__ . '/autoload/autoloader.php';
|
||||||
{
|
|
||||||
$q = explode( '\\' , $classname );
|
|
||||||
$c = array_pop( $q );
|
|
||||||
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
|
|
||||||
|
|
||||||
if ( file_exists( $f ) )
|
|
||||||
require_once( $f );
|
|
||||||
}
|
|
||||||
spl_autoload_register( '__autoload_my_classes' );
|
|
||||||
date_default_timezone_set( 'Europe/Warsaw' );
|
date_default_timezone_set( 'Europe/Warsaw' );
|
||||||
|
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
|
|||||||
11
index.php
@@ -1,15 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||||
function __autoload_my_classes( $classname )
|
require_once __DIR__ . '/autoload/autoloader.php';
|
||||||
{
|
|
||||||
$q = explode( '\\', $classname );
|
|
||||||
$c = array_pop( $q );
|
|
||||||
$f = 'autoload/' . implode( '/', $q ) . '/class.' . $c . '.php';
|
|
||||||
|
|
||||||
if ( file_exists( $f ) )
|
|
||||||
require_once( $f );
|
|
||||||
}
|
|
||||||
spl_autoload_register( '__autoload_my_classes' );
|
|
||||||
date_default_timezone_set( 'Europe/Warsaw' );
|
date_default_timezone_set( 'Europe/Warsaw' );
|
||||||
|
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
|
|||||||
96
log.txt
@@ -1,96 +0,0 @@
|
|||||||
Array
|
|
||||||
(
|
|
||||||
)
|
|
||||||
Array
|
|
||||||
(
|
|
||||||
[title] => "Udar mózgu: Kluczowe znaczenie wczesnej diagnostyki i interwencji medycznej"
|
|
||||||
[entry] => Udar mózgu stanowi jedno z najpoważniejszych zagrożeń dla zdrowia, dotykając każdego roku wiele osób w Polsce. Wczesne rozpoznanie jego objawów oraz niezwłoczna interwencja medyczna mogą uratować życie i zminimalizować ryzyko trwałych uszkodzeń neurologicznych.
|
|
||||||
[text] => <p><span style="font-weight:bold;">Udar mózgu</span> to jedno z najgroźniejszych zagrożeń dla zdrowia człowieka. Każdego roku dotyka on tysiące Polaków, a prawidłowa i szybka reakcja może uratować życie oraz zapobiec trwałym uszkodzeniom neurologicznym. Niestety, wiele osób nie rozpoznaje wczesnych objawów, które wysyła nasz organizm przed udarem. <span style="font-style:italic;">Neurologowie</span> ostrzegają, że pierwsze symptomy mogą być bardzo subtelne, lecz ich zignorowanie niesie poważne konsekwencje.</p>
|
|
||||||
|
|
||||||
<p><span style="font-weight:bold;">Czym jest udar mózgu?</span> Udar mózgu to nagłe zaburzenie krążenia krwi w obrębie mózgu. Może on mieć charakter niedokrwienny (najczęstszy, spowodowany zatkaniem naczynia krwionośnego) lub krwotoczny (wynikający z pęknięcia naczynia i krwawienia do mózgu). Obie formy prowadzą do niedotlenienia obszaru mózgu i szybkiego obumierania komórek nerwowych.</p>
|
|
||||||
|
|
||||||
<p><span style="font-weight:bold;">Najczęściej występujące czynniki ryzyka udaru:</span></p>
|
|
||||||
<ul>
|
|
||||||
<li>nadciśnienie tętnicze</li>
|
|
||||||
<li>cukrzyca</li>
|
|
||||||
<li>choroby serca, w szczególności migotanie przedsionków</li>
|
|
||||||
<li>palenie papierosów</li>
|
|
||||||
<li>otyłość</li>
|
|
||||||
<li>niezdrowa dieta i brak aktywności fizycznej</li>
|
|
||||||
<li>przewlekły stres</li>
|
|
||||||
<li>nadużywanie alkoholu</li>
|
|
||||||
<li>podeszły wiek</li>
|
|
||||||
<li>czynniki genetyczne</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><span style="font-weight:bold;">Organizm ostrzega przed udarem – na jakie symptomy uważać?</span> Wiele osób sądzi, że udar pojawia się nagle i bez zapowiedzi. Tymczasem ciało często wysyła sygnały ostrzegawcze. Wczesne rozpoznanie pozwala szybko zareagować i zapobiec powikłaniom.</p>
|
|
||||||
|
|
||||||
<p><span style="text-decoration:underline;">Najczęściej występujące symptomy zwiastujące udar:</span></p>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<span style="font-weight:bold;">Nagła, jednostronna słabość lub drętwienie:</span>
|
|
||||||
<span>Dochodzi do niej najczęściej w obrębie twarzy, ramienia bądź nogi. Uczucie osłabienia lub brak czucia po jednej stronie ciała to klasyczny znak ostrzegawczy. Zdarza się też opadanie kącika ust.</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span style="font-weight:bold;">Problemy z mową i rozumieniem:</span>
|
|
||||||
<span>Chory ma trudności z wypowiadaniem słów, zrozumieniem tego, co się do niego mówi, lub doświadcza bełkotliwej mowy. Może mieć również trudności ze znalezieniem właściwych słów.</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span style="font-weight:bold;">Nagłe zaburzenia widzenia:</span>
|
|
||||||
<span>Pojawia się pogorszenie ostrości wzroku, podwójne widzenie lub częściowa utrata wzroku – najczęściej w jednym oku lub po jednej stronie pola widzenia.</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span style="font-weight:bold;">Silny, nagły ból głowy:</span>
|
|
||||||
<span>Zwłaszcza jeśli towarzyszą mu nudności, wymioty, zaburzenia świadomości lub sztywność karku. Takie bóle głowy mogą wskazywać na udar krwotoczny.</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span style="font-weight:bold;">Zaburzenia równowagi, zawroty głowy, trudności w chodzeniu:</span>
|
|
||||||
<span>Osoba może mieć uczucie wirowania, trudności z koordynacją, traci równowagę oraz przewraca się bez wyraźnej przyczyny.</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span style="font-weight:bold;">Nagła utrata przytomności lub splątanie:</span>
|
|
||||||
<span>Osoba staje się zdezorientowana, trudna do nawiązania kontaktu, a nawet może zemdleć.</span>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<p>Nie należy lekceważyć nawet przejściowych objawów, które same ustępują. Mogą one oznaczać tzw. przejściowy atak niedokrwienny (TIA), będący <span style="font-weight:bold;">ostrzegawczym sygnałem</span> przed pełnoobjawowym udarem.</p>
|
|
||||||
|
|
||||||
<p><span style="font-weight:bold;">Jak zachować się, gdy pojawią się objawy udaru?</span></p>
|
|
||||||
<ul>
|
|
||||||
<li>Niezwłocznie wezwij pogotowie ratunkowe (numer 112 lub 999).</li>
|
|
||||||
<li>Nie próbuj samodzielnie dojechać do szpitala – leczenie musi rozpocząć się jak najszybciej, najlepiej w ambulansie, gdzie podejmowane są pierwsze działania ratujące życie.</li>
|
|
||||||
<li>Osoba z podejrzeniem udaru powinna być ułożona w bezpiecznej pozycji, nie podawać jej jedzenia ani picia.</li>
|
|
||||||
<li>Poinformuj ratowników o wszystkich zaobserwowanych objawach i ich czasie wystąpienia.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><span style="font-weight:bold;">Leczenie i rehabilitacja</span></p>
|
|
||||||
<p>Najważniejsze jest rozpoczęcie terapii jak najszybciej po wystąpieniu objawów – istnieje tzw. „<span style="font-style:italic;">złota godzina</span>”, w której wdrożenie leczenia trombolitycznego (rozpuszczającego zakrzepy) istotnie poprawia rokowanie pacjenta. Rehabilitacja neurologiczna powinna rozpocząć się jak najszybciej, co sprzyja powrotowi sprawności i minimalizuje skutki udaru.</p>
|
|
||||||
|
|
||||||
<p><span style="font-weight:bold;">Profilaktyka udaru – co możemy zrobić?</span></p>
|
|
||||||
<ul>
|
|
||||||
<li>Kontroluj ciśnienie tętnicze krwi i regularnie przyjmuj leki przepisane przez lekarza.</li>
|
|
||||||
<li>Wyeliminuj czynniki ryzyka: rzuć palenie, unikaj nadmiernego spożycia alkoholu, dbaj o prawidłową masę ciała.</li>
|
|
||||||
<li>Zadbaj o zbilansowaną dietę bogatą w warzywa, owoce, pełnoziarniste produkty i zdrowe tłuszcze.</li>
|
|
||||||
<li>Systematycznie uprawiaj aktywność fizyczną.</li>
|
|
||||||
<li>Regularnie badaj poziom cholesterolu i cukru we krwi.</li>
|
|
||||||
<li>Lecz choroby przewlekłe takie jak cukrzyca i schorzenia serca.</li>
|
|
||||||
<li>Unikaj przewlekłego stresu i zadbaj o higienę snu.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><span style="font-weight:bold;">Podsumowanie</span></p>
|
|
||||||
<p>Udar mózgu może dotknąć każdego, szczególnie osoby z grup ryzyka. Wczesne rozpoznanie objawów i szybkie udzielenie pomocy daje największą szansę na przeżycie i powrót do sprawności. Nie lekceważ nawet krótkotrwałych symptomów – każdy z nich może być ostrzeżeniem wysyłanym przez Twój organizm. Edukacja i profilaktyka to najskuteczniejsze narzędzia w walce ze skutkami udaru.</p>
|
|
||||||
[page_id] => 41
|
|
||||||
[action] => add_article
|
|
||||||
)
|
|
||||||
Array
|
|
||||||
(
|
|
||||||
[main_image] => Array
|
|
||||||
(
|
|
||||||
[name] => FLUX.1-dev
|
|
||||||
[type] => image/jpeg
|
|
||||||
[tmp_name] => /tmp/phpafhT1D
|
|
||||||
[error] => 0
|
|
||||||
[size] => 115301
|
|
||||||
)
|
|
||||||
|
|
||||||
)
|
|
||||||
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>
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
||||||
<url>
|
|
||||||
<loc>https://cmsen.project-dc.pl</loc>
|
|
||||||
<lastmod>2021-12-15</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>1</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://cmsen.project-dc.pl/home</loc>
|
|
||||||
<lastmod>2021-12-15</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>1</priority>
|
|
||||||
</url>
|
|
||||||
</urlset>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
||||||
<url>
|
|
||||||
<loc>https://cmspro.project-dc.pl</loc>
|
|
||||||
<lastmod>2021-12-15</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>1</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://cmspro.project-dc.pl/home</loc>
|
|
||||||
<lastmod>2021-12-15</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>1</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://cmspro.project-dc.pl/s-49-test</loc>
|
|
||||||
<lastmod>2021-12-15</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>1</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://cmspro.project-dc.pl/strona-testowa</loc>
|
|
||||||
<lastmod>2021-12-15</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>1</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://cmspro.project-dc.pl/s-42-strona-druga-2</loc>
|
|
||||||
<lastmod>2021-12-15</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>1</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://cmspro.project-dc.pl/s-48-prowadzenie-spraw-przed-prezesem-urzedu-ochrony-danych-osobowych-prezesem-urzedu-konkurencji-i-konsumentow-oraz-krajowa-izba-odwolawcza</loc>
|
|
||||||
<lastmod>2021-12-15</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>1</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://cmspro.project-dc.pl/a-11-test</loc>
|
|
||||||
<lastmod>2021-12-15</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>0.6</priority>
|
|
||||||
</url>
|
|
||||||
</urlset>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
]ŽÑ
|
|
||||||
Â0Eÿ%_°ÎIÝíoø.QË,tE–„±7Õ*Ó<<3C>œ$·e´X`¬5öз½-<2D>c˜÷„±Ç"ºGáJNTSK®L>,è@‰G_ñè%Ó+dÓ
|
|
||||||
zPä4Ì<x!<21>µ?±
|
|
||||||
¦+`‹9•ͪï±^«>sJ~¢ÍÓ;<3B>L—B
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Kエ2イェホエ2ーホエ2477エ07166qャ<71>ャk
|
|
||||||
BIN
temp/2moto.jpg
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
temp/2moto.png
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,4 +0,0 @@
|
|||||||
ŤTËŽÓJÝ#ń–¬ c;ďžÁ!BH<42>tĹEČŞt—ížŘݦ»3ĆFlŘđ
|
|
||||||
<EFBFBD>_ąKćż(;ńŤÍ‰HQuęqR}ęt‹ŘgÉ‚KÉÂĺ2\®ÖŃzŮţ¸6]łĎ–™źHSÄ
|
|
||||||
|
|
||||||
ô/-<0B>ßš0<ůA´v™3„<>Njy,U˘[w´bţ¦ÜľőôősÖ=˛ÎhUŘ\”Űű÷şŇ<C59F>ů\+Üʼn6Eçc~ŘŮé9†ČĽť1żŞŞ'Ľ°ĄŃú^#wŹź”]<¤žşä8ÓÖuÔ]{Ăĺ<šNÚŁz˘ĐÝ))µ9–0?š÷w>†rťJőW
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
K<EFBFBD>2<EFBFBD><EFBFBD>δ2<EFBFBD>δ2477<EFBFBD>07166q<><71><EFBFBD><0C><><0C><>k
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1 +0,0 @@
|
|||||||
e<EFBFBD><EFBFBD>
|
|
||||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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; }
|
||||||
|
}
|
||||||