4 Commits

Author SHA1 Message Date
c73d69664f build(update): paczka 1.696 — Domain layer kompletny (Phase 5)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 00:58:35 +02:00
f7c7c0bb88 feat(05-domain-seoadditional-cron-releases): Domain layer kompletny — SeoAdditional + Cron + Releases
Phase 5 complete:
- Domain\SeoAdditional\SeoAdditionalRepository (elementDelete, elementSave, elementDetails)
- Domain\Cron\CronRepository (3 pub + 12 private helper methods)
- Domain\Releases\ReleasesRepository (9 metod: wersje, licencje, discover)
- Domain\Releases\UpdateRepository (auto-update, konstruktor($db, $settings))
- 4 legacy factory wrappers zaktualizowane do wrapper delegation

Domain layer: 13/13 repozytoriów kompletnych.

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 00:46:01 +02:00
cfd2e5fb57 update 2026-04-04 18:29:10 +02:00
26 changed files with 2167 additions and 765 deletions

View File

@@ -7,7 +7,7 @@ Autorski system CMS z panelem administracyjnym (17 modułów admin, 13 modułów
Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi. Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi.
## Already Completed ## Already Completed
- Domain (10 repos): Articles, Languages, Layouts, Pages, Settings, User, Scontainers, Banners, Authors, Newsletter - Domain (13 repos): Articles, Languages, Layouts, Pages, Settings, User, Scontainers, Banners, Authors, Newsletter, SeoAdditional, Cron, Releases+Update
- Shared (7 modules): Cache, Helpers, Html, Image, Tpl, Email, Security - Shared (7 modules): Cache, Helpers, Html, Image, Tpl, Email, Security
- Form Edit System: FormEditViewModel, multi-tab, validation, persistence - Form Edit System: FormEditViewModel, multi-tab, validation, persistence
- PHPUnit base: Bootstrap, 3 test files - PHPUnit base: Bootstrap, 3 test files
@@ -17,7 +17,7 @@ Autorski system CMS umożliwiający zarządzanie treściami i stronami interneto
### Must Have ### Must Have
- Centralny PSR-4 autoloader (hybrydowy z legacy) - Centralny PSR-4 autoloader (hybrydowy z legacy)
- Wszystkie Domain repositories (Scontainers, Banners, Authors, Newsletter, SEO, Cron, Releases) - Wszystkie Domain repositories — Phase 5 complete
- Shared\Email + Shared\Security (CsrfToken, HMAC-SHA256) - Shared\Email + Shared\Security (CsrfToken, HMAC-SHA256)
- Admin\ namespace z DI dla wszystkich 17 modułów - Admin\ namespace z DI dla wszystkich 17 modułów
- Frontend\ namespace dla wszystkich front modułów - Frontend\ namespace dla wszystkich front modułów
@@ -46,4 +46,4 @@ Autorski system CMS umożliwiający zarządzanie treściami i stronami interneto
--- ---
*Created: 2026-04-04* *Created: 2026-04-04*
*Last updated: 2026-04-04 after Phase 4* *Last updated: 2026-04-26 after Phase 5*

View File

@@ -6,7 +6,7 @@ Pełna refaktoryzacja cmsPRO do architektury DDD wzorowanej na shopPRO. Wzorzec:
## Current Milestone ## Current Milestone
**v0.1 Refaktoryzacja** (v0.1.0) **v0.1 Refaktoryzacja** (v0.1.0)
Status: In progress Status: In progress
Phases: 4 of 19 complete Phases: 5 of 19 complete
## Already Completed (before PAUL) ## Already Completed (before PAUL)
- **Domain (6 repos):** Articles, Languages, Layouts, Pages, Settings, User - **Domain (6 repos):** Articles, Languages, Layouts, Pages, Settings, User
@@ -22,7 +22,8 @@ Phases: 4 of 19 complete
| 2 | Shared: Email + Security | 1 | Complete | 2026-04-04 | | 2 | Shared: Email + Security | 1 | Complete | 2026-04-04 |
| 3 | Domain: Scontainers + Banners | 1 | Complete | 2026-04-04 | | 3 | Domain: Scontainers + Banners | 1 | Complete | 2026-04-04 |
| 4 | Domain: Authors + Newsletter | 1 | Complete | 2026-04-04 | | 4 | Domain: Authors + Newsletter | 1 | Complete | 2026-04-04 |
| 5 | Domain: SeoAdditional + Cron + Releases | 1 | Not started | - | | 04h | **HOTFIX:** HTTPS update endpoint (out-of-roadmap) | 1 | Complete | 2026-04-26 |
| 5 | Domain: SeoAdditional + Cron + Releases | 1 | Complete | 2026-04-26 |
| 6 | Admin: Base Infrastructure | 1 | Not started | - | | 6 | Admin: Base Infrastructure | 1 | Not started | - |
| 7 | Admin: Articles + ArticlesArchive | 1 | Not started | - | | 7 | Admin: Articles + ArticlesArchive | 1 | Not started | - |
| 8 | Admin: Pages + Layouts | 1 | Not started | - | | 8 | Admin: Pages + Layouts | 1 | Not started | - |

View File

@@ -2,21 +2,21 @@
## Project Reference ## Project Reference
See: .paul/PROJECT.md (updated 2026-04-04) See: .paul/PROJECT.md (updated 2026-04-26)
**Core value:** Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi. **Core value:** Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi.
**Current focus:** Phase 4 complete — ready for Phase 5 **Current focus:** Phase 5 complete — ready for Phase 6 (Admin: Base Infrastructure)
## Current Position ## Current Position
Milestone: v0.1 Refaktoryzacja Milestone: v0.1 Refaktoryzacja
Phase: 4 of 19 (Domain: Authors + Newsletter) — Complete Phase: 6 (Admin: Base Infrastructure) — Not started
Plan: 04-01 complete Plan: Not started
Status: Loop closed, ready for next PLAN Status: Ready to plan Phase 6
Last activity: 2026-04-04 — Phase 4 complete, UNIFY done Last activity: 2026-04-26 — Phase 5 complete, transitioned to Phase 6
Progress: Progress:
- Milestone: [▓▓░░░░░░░] 20% - Milestone: [▓▓░░░░░░░] 26% (5 of 19 phases)
## Loop Position ## Loop Position
@@ -29,8 +29,8 @@ PLAN ──▶ APPLY ──▶ UNIFY
## Performance Metrics ## Performance Metrics
**Velocity:** **Velocity:**
- Total plans completed: 4 - Total plans completed: 5
- Total execution time: ~22min - Total execution time: ~27min
**By Phase:** **By Phase:**
@@ -40,17 +40,22 @@ PLAN ──▶ APPLY ──▶ UNIFY
| 02-shared-email-security | 1/1 | ~8min | ~8min | | 02-shared-email-security | 1/1 | ~8min | ~8min |
| 03-domain-scontainers-banners | 1/1 | ~2min | ~2min | | 03-domain-scontainers-banners | 1/1 | ~2min | ~2min |
| 04-domain-authors-newsletter | 1/1 | ~2min | ~2min | | 04-domain-authors-newsletter | 1/1 | ~2min | ~2min |
| 04h-hotfix-https-updates | 1/1 | ~90min | ~90min |
| 05-domain-seoadditional-cron-releases | 1/1 | ~5min | ~5min |
## Accumulated Context ## Accumulated Context
### Decisions ### Decisions
- 2026-04-26: Phase 5 — UpdateRepository przyjmuje ($db, $settings) w konstruktorze — settings potrzebny do update_key i wersji.
- 2026-04-26: Phase 5 — Cron helper methods (get_site_meta_*) stały się private w CronRepository — były wywoływane tylko wewnętrznie.
- 2026-04-26: Phase 5 — class.Cron.php zachowuje brak namespace (klasa globalna) — cron.php używa bezpośrednio.
- 2026-04-26: Hotfix 04h — full-patch wszystkich 121 paczek (zamiast minimal-patch). Powód: paczki nadpisują class.S.php w różnych wersjach, częściowy patch ryzykuje regresję podczas chain-update.
- Centralny autoloader zamiast duplikatów - Centralny autoloader zamiast duplikatów
- CsrfToken: single token per session (shopPRO pattern) - CsrfToken: single token per session (shopPRO pattern)
- Email: PHPMailer require via __DIR__ absolute paths - Email: PHPMailer require via __DIR__ absolute paths
- Shared layer kompletny: Cache, Helpers, Html, Image, Tpl, Email, Security - Shared layer kompletny: Cache, Helpers, Html, Image, Tpl, Email, Security
- Wrapper delegation: factory creates new repo per call (no singleton) - Wrapper delegation: factory creates new repo per call (no singleton)
- Front repos: $lang[0] passed explicitly, repos don't use globals - Front repos: $lang[0] passed explicitly, repos don't use globals
- Front caching: migrated from \Cache:: to \Shared\Cache\CacheHandler::
- Newsletter: globals ($settings, $lang) passed as explicit params to repo methods - Newsletter: globals ($settings, $lang) passed as explicit params to repo methods
### Deferred Issues ### Deferred Issues
@@ -61,10 +66,10 @@ None.
## Session Continuity ## Session Continuity
Last session: 2026-04-04 Last session: 2026-04-26
Stopped at: Phase 4 complete, loop closed Stopped at: Phase 5 complete, loop closed
Next action: Run /paul:plan for Phase 5 (Domain: SeoAdditional + Cron + Releases) Next action: /paul:plan dla Phase 6 (Admin: Base Infrastructure)
Resume file: .paul/phases/04-domain-authors-newsletter/04-01-SUMMARY.md Resume file: .paul/ROADMAP.md
--- ---
*STATE.md — Updated after every significant action* *STATE.md — Updated after every significant action*

View File

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

33
.paul/codebase/README.md Normal file
View File

@@ -0,0 +1,33 @@
# Codebase Map — cmsPRO
> Generated: 2026-04-26 | Auto-generated by /paul:map-codebase
## Documents
| File | Contents |
|------|---------|
| [overview.md](overview.md) | Project summary, modules, entry points, refactoring status |
| [stack.md](stack.md) | PHP runtime, database, frontend libs, server config, external services |
| [architecture.md](architecture.md) | Directory map, patterns, routing, caching, namespaces |
| [conventions.md](conventions.md) | Naming, class patterns, PHPDoc, return types, DB access |
| [testing.md](testing.md) | PHPUnit setup, test structure, stubs, adding new tests |
| [integrations.md](integrations.md) | Email, geolocation, analytics, update server, file manager |
| [concerns.md](concerns.md) | Technical debt prioritized CRITICAL → HIGH → MEDIUM → LOW |
## Quick Reference
- **Architecture**: Controls → (deprecated) Factories → Domain Repositories → Medoo/MySQL
- **New code goes in**: `autoload/Domain/{Entity}/{Entity}Repository.php`
- **Tests go in**: `tests/Unit/Domain/{Entity}/{Entity}RepositoryTest.php`
- **Global helper**: `\S::method()` (legacy) or `\Shared\Helpers\Helpers::method()` (preferred)
- **Templates**: `templates/{module}/template.php` (user override: `templates_user/`)
- **CSRF**: `\Shared\Security\CsrfToken::getToken()` / `::validate($token)`
- **Cache**: `\Shared\Cache\CacheHandler::store($key, $data, $ttl)` / `::fetch($key)`
## Top Issues to Fix
1. **CRITICAL**: `unserialize()` on cookie — `admin/ajax/pages.php:36,49`
2. **CRITICAL**: Path traversal in updates — `autoload/admin/factory/class.Update.php:76-80`
3. **HIGH**: Missing input validation everywhere
4. **HIGH**: Password hash in auto-login cookie — `admin/index.php:59-61`
5. **MEDIUM**: God class Helpers.php (1220 lines) — needs splitting

View File

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

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

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

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
# cmsPRO — Project Overview
> Generated: 2026-04-26 | Milestone: v0.1 Refaktoryzacja
## What is cmsPRO?
cmsPRO is a Polish-language PHP CMS with a **hybrid transitional architecture**. The codebase is actively being refactored from a legacy procedural/OOP mixed approach toward a clean Domain-Driven Design structure with Repository pattern.
## Core Capabilities
| Module | Description |
|--------|-------------|
| Articles | CRUD, multi-language, versioning, scheduling, galleries, tags, SEO |
| Pages | Static pages with layouts, caching, inline editing |
| Newsletter | Subscription, templates, cron-based batch sending |
| Layouts | HTML/CSS template system with Savant3 rendering |
| Users | Admin users, privileges matrix, 2FA support |
| Languages | Multi-language content, URL routing, session caching |
| Banners | Homepage banners with multi-language support |
| Scontainers | Reusable content blocks/widgets |
| Authors | Author management for articles |
| SEO | Meta tags, slugs, noindex, robots.txt, sitemap |
| File Manager | Upload, browse, thumbnail generation |
| Settings | DB-stored site config, WebP toggle, lazy loading |
| Updates | Versioned ZIP updates with license validation |
| Backups | DB backup/restore utilities |
## Entry Points
| File | Purpose |
|------|---------|
| `index.php` | Frontend entry point and router |
| `admin/index.php` | Admin panel entry point |
| `ajax.php` | Frontend AJAX handler |
| `admin/ajax.php` | Admin AJAX handler (routes to `admin/ajax/*.php`) |
| `api.php` | API endpoint |
| `cron.php` | Scheduled tasks (newsletter batch sending) |
| `download.php` | File download handler |
## Current Refactoring Status
The project is in **Phase 5 of Milestone v0.1 Refaktoryzacja**.
Migration pattern:
- **Done**: Domain repositories created for all 10 main entities
- **Done**: Factory classes converted to deprecated wrappers delegating to repositories
- **In progress**: SeoAdditional, Cron, Releases domains
- **Pending**: Remove factory layer, inject repositories directly into controls
## Version
- Current app version: **1.695**
- Update channel: stable/beta via `updates/` ZIP packages
- License validation via `pp_update_licenses` table

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,98 +6,63 @@ class Releases
public static function get_versions(): array public static function get_versions(): array
{ {
global $mdb; global $mdb;
$rows = $mdb->select('pp_update_versions', '*', ['ORDER' => ['version' => 'DESC']]); $repo = new \Domain\Releases\ReleasesRepository($mdb);
if (!$rows) return []; return $repo->getVersions();
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 public static function promote(string $version): void
{ {
global $mdb; global $mdb;
$mdb->update('pp_update_versions', $repo = new \Domain\Releases\ReleasesRepository($mdb);
['channel' => 'stable', 'promoted_at' => date('Y-m-d H:i:s')], $repo->promote($version);
['version' => $version]
);
} }
public static function demote(string $version): void public static function demote(string $version): void
{ {
global $mdb; global $mdb;
$mdb->update('pp_update_versions', $repo = new \Domain\Releases\ReleasesRepository($mdb);
['channel' => 'beta', 'promoted_at' => null], $repo->demote($version);
['version' => $version]
);
} }
public static function discover_versions(): int public static function discover_versions(): int
{ {
global $mdb; global $mdb;
$known = array_flip($mdb->select('pp_update_versions', 'version', []) ?: []); $repo = new \Domain\Releases\ReleasesRepository($mdb);
$zips = glob('../updates/*/ver_*.zip') ?: []; return $repo->discoverVersions();
$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 public static function get_licenses(): array
{ {
global $mdb; global $mdb;
return $mdb->select('pp_update_licenses', '*', ['ORDER' => ['domain' => 'ASC']]) ?: []; $repo = new \Domain\Releases\ReleasesRepository($mdb);
return $repo->getLicenses();
} }
public static function get_license(int $id): array public static function get_license(int $id): array
{ {
global $mdb; global $mdb;
return $mdb->get('pp_update_licenses', '*', ['id' => $id]) ?: []; $repo = new \Domain\Releases\ReleasesRepository($mdb);
return $repo->getLicense($id);
} }
public static function save_license(array $data): void public static function save_license(array $data): void
{ {
global $mdb; global $mdb;
$row = [ $repo = new \Domain\Releases\ReleasesRepository($mdb);
'key' => trim($data['key'] ?? ''), $repo->saveLicense($data);
'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 public static function delete_license(int $id): void
{ {
global $mdb; global $mdb;
$mdb->delete('pp_update_licenses', ['id' => $id]); $repo = new \Domain\Releases\ReleasesRepository($mdb);
$repo->deleteLicense($id);
} }
public static function toggle_beta(int $id): void public static function toggle_beta(int $id): void
{ {
global $mdb; global $mdb;
$license = $mdb->get('pp_update_licenses', ['id', 'beta'], ['id' => $id]); $repo = new \Domain\Releases\ReleasesRepository($mdb);
if ($license) $repo->toggleBeta($id);
$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';
} }
} }

View File

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

View File

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

View File

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

View File

@@ -60,19 +60,27 @@ Stare klasy (`class.S.php`, `class.Cache.php`, itd.) są teraz cienkimi
wrapperami — zachowana pełna kompatybilność wsteczna. wrapperami — zachowana pełna kompatybilność wsteczna.
Helpers::send_email() → Email, Helpers::get_token()/is_token_valid() → CsrfToken. Helpers::send_email() → Email, Helpers::get_token()/is_token_valid() → CsrfToken.
### Faza 2 (w toku) - Domain Repositories (`autoload/Domain/`) ### Faza 2 ✓ — Domain Repositories (`autoload/Domain/`) — KOMPLETNE (13/13)
``` ```
autoload/Domain/ autoload/Domain/
├── Languages/LanguagesRepository.php ← \Domain\Languages\LanguagesRepository ✓ ├── Articles/ArticlesRepository.php ← \Domain\Articles\ArticlesRepository
├── Settings/SettingsRepository.php ← \Domain\Settings\SettingsRepository ✓ ├── Authors/AuthorsRepository.php ← \Domain\Authors\AuthorsRepository
├── User/UserRepository.php ← \Domain\User\UserRepository ├── Banners/BannersRepository.php ← \Domain\Banners\BannersRepository ✓
├── Pages/PagesRepository.php ← \Domain\Pages\PagesRepository ✓ ├── Cron/CronRepository.php ← \Domain\Cron\CronRepository
├── Layouts/LayoutsRepository.php ← \Domain\Layouts\LayoutsRepository ├── Languages/LanguagesRepository.php ← \Domain\Languages\LanguagesRepository ✓
── Articles/ArticlesRepository.php ← \Domain\Articles\ArticlesRepository ✓ ── Layouts/LayoutsRepository.php ← \Domain\Layouts\LayoutsRepository
├── Newsletter/NewsletterRepository.php ← \Domain\Newsletter\NewsletterRepository ✓
├── Pages/PagesRepository.php ← \Domain\Pages\PagesRepository ✓
├── Releases/ReleasesRepository.php ← \Domain\Releases\ReleasesRepository ✓
├── Releases/UpdateRepository.php ← \Domain\Releases\UpdateRepository ✓
├── Scontainers/ScontainersRepository.php ← \Domain\Scontainers\ScontainersRepository ✓
├── SeoAdditional/SeoAdditionalRepository.php ← \Domain\SeoAdditional\SeoAdditionalRepository ✓
├── Settings/SettingsRepository.php ← \Domain\Settings\SettingsRepository ✓
└── User/UserRepository.php ← \Domain\User\UserRepository ✓
``` ```
Następne: `Domain\Scontainers`, `Domain\Banners`, `Domain\Authors`, `Domain\Newsletter`, ... Następne: `Admin\` namespace (Fazy 613), `Frontend\` namespace (Fazy 1416).
--- ---
## Katalogi ## Katalogi

View File

@@ -0,0 +1,33 @@
{
"changelog": "NEW - aktualizacja konfiguracji Claude, Serena i CLAUDE.md",
"version": "1.695",
"files": {
"added": [
"autoload/Domain/Authors/AuthorsRepository.php",
"autoload/Domain/Banners/BannersRepository.php",
"autoload/Domain/Newsletter/NewsletterRepository.php",
"autoload/Domain/Scontainers/ScontainersRepository.php"
],
"deleted": [
],
"modified": [
"autoload/admin/factory/class.Authors.php",
"autoload/admin/factory/class.Banners.php",
"autoload/admin/factory/class.Newsletter.php",
"autoload/admin/factory/class.Scontainers.php",
"autoload/front/factory/class.Authors.php",
"autoload/front/factory/class.Banners.php",
"autoload/front/factory/class.Newsletter.php",
"autoload/front/factory/class.Scontainers.php"
]
},
"checksum_zip": "sha256:f8b50187c72ced5d00937c74939a4e4300bc6e40c074639d7a9ff8662e4cddd0",
"sql": [
],
"date": "2026-04-04",
"directories_deleted": [
]
}

View File

@@ -11,7 +11,7 @@ $mdb = new medoo( [
'charset' => 'utf8' 'charset' => 'utf8'
] ); ] );
$current_ver = 1695; // aktualizowane automatycznie przez build-update.ps1 $current_ver = 1696; // aktualizowane automatycznie przez build-update.ps1
// 1. Skan filesystem — lista istniejÄ‚ââ¬ĹľÄ˘â‚¬Ă¦cych ZIPÄ„ââ¬ĹˇĂ„ąââ¬Ĺˇw // 1. Skan filesystem — lista istniejÄ‚ââ¬ĹľÄ˘â‚¬Ă¦cych ZIPÄ„ââ¬ĹˇĂ„ąââ¬Ĺˇw
$versions = []; $versions = [];