17 Commits

Author SHA1 Message Date
dd31c062ad feat(releases): powrót do zakładki Licencje po zapisie + wykrywanie wersji z dysku
- Redirecty save_license/delete_license/toggle_beta kierują teraz na #licenses
- Dodano akcję discover_versions: skanuje updates/*/ver_*.zip przez glob(),
  rejestruje nieznane wersje jako beta w pp_update_versions
- Przycisk "Wykryj wersje z dysku" w zakładce Wersje
- Tpl::__isset() dla poprawnej obsługi isset() na właściwościach szablonu
- Usunięto tymczasowy plik diagnostyczny _diag_licenses.php

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 01:46:32 +01:00
869f25d6db tmp: render check 2026-02-28 01:08:55 +01:00
b41fa58488 tmp: template check 2026-02-28 01:06:37 +01:00
1b4c6fe66a tmp: render diagnostic 2026-02-28 01:05:43 +01:00
320710fd02 tmp: factory simulation diagnostic 2026-02-28 01:00:10 +01:00
11d720aa25 tmp: extended diagnostic 2026-02-28 00:56:49 +01:00
08bd6d23c9 tmp: diagnostic script 2026-02-28 00:53:13 +01:00
28de4e88b7 Fix: przenieś additional-menu.php do admin/templates/ (prawidłowa ścieżka)
main-layout.php uruchamia się z CWD admin/, więc szukał pliku w
admin/templates/additional-menu.php. Plik był błędnie umieszczony
w templates/ (korzeń projektu) — menu dewelopera nie wyświetlało się.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 00:47:10 +01:00
0c1e916ed6 feat: modul Releases — dwukanalowy system aktualizacji (beta/stable)
- admin\factory\Releases: logika biznesowa (wersje, licencje, toggle beta)
- admin\controls\Releases: handlery HTTP (promote, demote, save/delete/toggle licencji)
- admin\view\Releases: renderowanie przez \Tpl
- admin/templates/releases/main-view.php: dwa taby (Wersje + Licencje),
  tabela wersji z przyciskami promocji, CRUD licencji z formularzem inline
- templates/additional-menu.php: link "Releases & Licencje" w menu dewelopera
- updates/versions.php: przebudowa — czyta z DB (pp_update_licenses,
  pp_update_versions), auto-discovery nowych ZIPow jako beta
- config.php: dodano host_remote dla polaczen zdalnych

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 00:39:34 +01:00
1bebdff3ac chore: wyklucz modul Releases i menu dewelopera z paczek klientow 2026-02-28 00:31:36 +01:00
5e6c3e46fc docs: plan implementacji modulu Releases (dwukanalowy system aktualizacji)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 00:26:09 +01:00
ff227fa6e0 docs: design dwukanałowego systemu aktualizacji + zarządzanie licencjami
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 00:18:14 +01:00
2e715e803e Fix: testy + bugfix SettingsRepository::allSettings() + migracja phpunit.xml
- SettingsRepository::allSettings() — inicjalizacja $settings = [] przed pętlą
  (bug: false ?? [] zwracało false gdy cache pusty a DB null)
- Stuby wydzielone do tests/stubs/CacheHandler.php + S.php
- phpunit.xml zmigurowany do schematu PHPUnit 10 (coverage → source)
- composer.lock dodany do repozytorium

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 23:56:00 +01:00
8e6b29976c v1.691: testy jednostkowe Domain\, infrastruktura PHPUnit, paczka aktualizacji
- Dodano testy: SettingsRepositoryTest, LanguagesRepositoryTest, UserRepositoryTest
- Infrastruktura: phpunit.xml, composer.json (phpunit/phpunit ^10), tests/bootstrap.php
- Bootstrap stuby: \Shared\Cache\CacheHandler (in-memory), \S
- Zaktualizowano docs/TESTING.md dla cmsPRO
- Paczka: updates/1.60/ver_1.691.zip + manifest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 23:50:33 +01:00
9ee4116f50 Refaktoryzacja Faza 0+1: PSR-4 autoloader + Shared/Domain klasy
- Dodano PSR-4 autoloader do wszystkich 6 punktów wejścia
- Shared\: CacheHandler, Helpers, Html, ImageManipulator, Tpl
- Domain\: LanguagesRepository, SettingsRepository, UserRepository
- Stare class.*.php → cienkie wrappery (kompatybilność wsteczna)
- Dodano dokumentację: docs/PROJECT_STRUCTURE.md + pozostałe docs/
- Dodano CLAUDE.md z workflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 23:43:55 +01:00
a6b821bb75 Remove log and sitemap files for deprecated projects 2026-02-22 22:08:06 +01:00
9c98fe7ad2 Dodano plik zip z aktualizacją do wersji 1.690 2026-02-22 22:06:13 +01:00
82 changed files with 6815 additions and 2985 deletions

View File

@@ -115,3 +115,10 @@ initial_prompt: ""
# override of the corresponding setting in serena_config.yml, see the documentation there.
# If null or missing, the value from the global config is used.
symbol_info_budget:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:

View File

@@ -48,6 +48,15 @@ backups/
cache/
cron/
# Moduł zarządzania releaseami (tylko serwer dewelopera)
autoload/admin/controls/class.Releases.php
autoload/admin/factory/class.Releases.php
autoload/admin/view/class.Releases.php
admin/templates/releases/
# Menu dewelopera
admin/templates/additional-menu.php
# IDE
.vscode/
.serena/

16
CLAUDE.md Normal file
View File

@@ -0,0 +1,16 @@
# Workflow
## KONIEC PRACY
Gdy użytkownik napisze `KONIEC PRACY`, wykonaj kolejno:
1. Przeprowadzenie testów.
2. Aktualizacja dokumentacji technicznej, jeśli zmiany tego wymagają:
- `docs/PROJECT_STRUCTURE.md`
- `docs/FORM_EDIT_SYSTEM.md`
3. Migracje SQL (jeśli były zmiany w bazie danych):
- Plik: `migrations/{version}.sql` (np. `migrations/0.304.sql`)
- **NIE** w `updates/` — build script sam wczyta z `migrations/`
- Sprawdź czy plik istnieje i jest poprawnie nazwany przed commitem
4. Commit.
5. Push.

View File

@@ -4,14 +4,20 @@ function __autoload_my_classes( $classname )
{
$q = explode( '\\' , $classname );
$c = array_pop( $q );
$f = '../autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( $c == 'Savant3' )
{
require_once( '../autoload/Savant3.php' );
return true;
}
if ( file_exists( $f ) )
require_once( $f );
// 1. Legacy: class.ClassName.php
$f = '../autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = '../autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );

View File

@@ -16,9 +16,14 @@ function __autoload_my_classes( $classname )
{
$q = explode( '\\' , $classname );
$c = array_pop( $q );
// 1. Legacy: class.ClassName.php
$f = '../autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) )
require_once( $f );
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = '../autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );

View File

@@ -0,0 +1,11 @@
<?php
// Menu tylko na serwerze dewelopera — wykluczone z .updateignore
?>
<div class="title">Developer</div>
<ul>
<li>
<a href="/admin/releases/main_view/">
<img src="/admin/css/icons/settings-20-filled.svg">Releases &amp; Licencje
</a>
</li>
</ul>

View File

@@ -0,0 +1,312 @@
<?php
global $gdb;
ob_start();
?>
<style>
.releases-tabs-nav { margin-bottom: 0; border-bottom: 1px solid #ddd; }
.releases-tabs-nav li { display: inline-block; margin-bottom: -1px; }
.releases-tabs-nav li a {
display: block; padding: 8px 16px; text-decoration: none; color: #555;
border: 1px solid transparent; border-radius: 3px 3px 0 0;
background: #f5f5f5; margin-right: 2px; cursor: pointer;
}
.releases-tabs-nav li.active a {
color: #333; background: #fff;
border-color: #ddd #ddd #fff;
}
.releases-tab-pane { display: none; padding: 18px 0 0; }
.releases-tab-pane.active { display: block; }
.license-form-wrap { display: none; margin-bottom: 20px; }
</style>
<ul class="releases-tabs-nav" id="releases-tabs-nav">
<li class="active"><a href="#" data-tab="tab-versions">Wersje</a></li>
<li><a href="#" data-tab="tab-licenses">Licencje</a></li>
</ul>
<!-- TAB: Wersje -->
<div class="releases-tab-pane active" id="tab-versions">
<div style="margin-bottom: 12px;">
<form method="post" action="/admin/releases/discover_versions/" style="display:inline"
onsubmit="return confirm('Wykryć wersje z dysku i zarejestrować jako stable?')">
<button type="submit" class="btn btn-info btn-sm">
<i class="fa fa-search"></i> Wykryj wersje z dysku
</button>
</form>
</div>
<table class="table table-bordered table-striped table-hover table-condensed">
<thead>
<tr>
<th>Wersja</th>
<th class="text-center" style="width:100px;">Kanał</th>
<th style="width:150px;">Data dodania</th>
<th style="width:150px;">Data promocji</th>
<th class="text-center" style="width:60px;">ZIP</th>
<th class="text-center" style="width:140px;">Akcje</th>
</tr>
</thead>
<tbody>
<?php if (empty($this->versions)): ?>
<tr><td colspan="6" class="text-center text-muted">Brak wersji w bazie.</td></tr>
<?php else: foreach ($this->versions as $v): ?>
<tr>
<td><strong><?= htmlspecialchars($v['version']) ?></strong></td>
<td class="text-center">
<?php if ($v['channel'] === 'stable'): ?>
<span class="label label-success">stable</span>
<?php else: ?>
<span class="label label-warning">beta</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($v['created_at'] ?? '') ?></td>
<td><?= $v['promoted_at'] ? htmlspecialchars($v['promoted_at']) : '<span class="text-muted">—</span>' ?></td>
<td class="text-center">
<?php if ($v['zip_exists']): ?>
<span class="text-success"><i class="fa fa-check"></i></span>
<?php else: ?>
<span class="text-danger"><i class="fa fa-times"></i></span>
<?php endif; ?>
</td>
<td class="text-center">
<?php if ($v['channel'] === 'beta'): ?>
<form method="post" action="/admin/releases/promote/" style="display:inline">
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
<button type="submit" class="btn btn-success btn-xs"
onclick="return confirm('Promować <?= htmlspecialchars($v['version'], ENT_QUOTES) ?> do stable?')">
Promuj &rarr;stable
</button>
</form>
<?php else: ?>
<form method="post" action="/admin/releases/demote/" style="display:inline">
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
<button type="submit" class="btn btn-warning btn-xs"
onclick="return confirm('Cofnąć <?= htmlspecialchars($v['version'], ENT_QUOTES) ?> do beta?')">
Cofnij &rarr;beta
</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; endif; ?>
</tbody>
</table>
</div>
<!-- TAB: Licencje -->
<div class="releases-tab-pane" id="tab-licenses">
<div style="margin-bottom: 12px;">
<a href="#" class="btn btn-success btn-sm" id="btn-add-license">
<i class="fa fa-plus-circle"></i> Dodaj licencję
</a>
</div>
<!-- Formularz dodawania / edycji -->
<div class="license-form-wrap panel panel-default" id="license-form-wrap">
<div class="panel-heading">
<strong id="license-form-title">Nowa licencja</strong>
</div>
<div class="panel-body">
<form method="post" action="/admin/releases/save_license/" id="license-form">
<input type="hidden" name="id" id="lic-id" value="">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label>Domena</label>
<input type="text" name="domain" id="lic-domain" class="form-control" placeholder="np. example.com" required>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label>Klucz licencji</label>
<input type="text" name="key" id="lic-key" class="form-control" placeholder="Klucz UUID / losowy ciąg (pusty = domyślny)">
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<label>Ważna do daty</label>
<input type="date" name="valid_to_date" id="lic-valid-date" class="form-control">
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label>Ważna do wersji</label>
<input type="text" name="valid_to_version" id="lic-valid-ver" class="form-control" placeholder="np. 1.700">
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label>Dostęp beta</label>
<select name="beta" id="lic-beta" class="form-control">
<option value="0">Nie</option>
<option value="1">Tak</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label>Notatka</label>
<input type="text" name="note" id="lic-note" class="form-control" placeholder="Opcjonalna notatka">
</div>
<button type="submit" class="btn btn-system btn-sm">
<i class="fa fa-save"></i> Zapisz licencję
</button>
<a href="#" class="btn btn-default btn-sm" id="btn-cancel-license">Anuluj</a>
</form>
</div>
</div>
<!-- Tabela licencji -->
<table class="table table-bordered table-striped table-hover table-condensed">
<thead>
<tr>
<th>Domena</th>
<th style="width:120px;">Klucz</th>
<th style="width:120px;">Do daty</th>
<th style="width:100px;">Do wersji</th>
<th class="text-center" style="width:70px;">Beta</th>
<th>Notatka</th>
<th class="text-center" style="width:60px;">Edytuj</th>
<th class="text-center" style="width:60px;">Usuń</th>
</tr>
</thead>
<tbody>
<?php if (empty($this->licenses)): ?>
<tr><td colspan="8" class="text-center text-muted">Brak licencji w bazie.</td></tr>
<?php else: foreach ($this->licenses as $lic): ?>
<tr>
<td><?= htmlspecialchars($lic['domain']) ?></td>
<td>
<code title="<?= htmlspecialchars($lic['key']) ?>">
<?= htmlspecialchars(substr($lic['key'], 0, 8)) ?>…
</code>
</td>
<td><?= $lic['valid_to_date'] ? htmlspecialchars($lic['valid_to_date']) : '<span class="text-muted">—</span>' ?></td>
<td><?= $lic['valid_to_version'] ? htmlspecialchars($lic['valid_to_version']) : '<span class="text-muted">—</span>' ?></td>
<td class="text-center">
<form method="post" action="/admin/releases/toggle_beta/" style="display:inline">
<input type="hidden" name="id" value="<?= (int)$lic['id'] ?>">
<button type="submit"
class="label <?= $lic['beta'] ? 'label-info' : 'label-default' ?>"
title="Kliknij, aby przełączyć"
style="cursor:pointer;border:none;background:none">
<?= $lic['beta'] ? 'tak' : 'nie' ?>
</button>
</form>
</td>
<td><?= htmlspecialchars($lic['note'] ?? '') ?></td>
<td class="text-center">
<a href="#"
class="btn btn-default btn-xs btn-edit-license"
data-id="<?= (int)$lic['id'] ?>"
data-domain="<?= htmlspecialchars($lic['domain'], ENT_QUOTES) ?>"
data-key="<?= htmlspecialchars($lic['key'], ENT_QUOTES) ?>"
data-valid-date="<?= htmlspecialchars($lic['valid_to_date'] ?? '', ENT_QUOTES) ?>"
data-valid-ver="<?= htmlspecialchars($lic['valid_to_version'] ?? '', ENT_QUOTES) ?>"
data-beta="<?= (int)$lic['beta'] ?>"
data-note="<?= htmlspecialchars($lic['note'] ?? '', ENT_QUOTES) ?>">
<i class="fa fa-pencil"></i>
</a>
</td>
<td class="text-center">
<form method="post" action="/admin/releases/delete_license/" style="display:inline"
onsubmit="return confirm('Usunąć licencję dla <?= htmlspecialchars($lic['domain'], ENT_QUOTES) ?>?')">
<input type="hidden" name="id" value="<?= (int)$lic['id'] ?>">
<button type="submit" class="btn btn-danger btn-xs">
<i class="fa fa-trash"></i>
</button>
</form>
</td>
</tr>
<?php endforeach; endif; ?>
</tbody>
</table>
</div>
<script>
(function () {
// Tab switching
document.querySelectorAll('#releases-tabs-nav a[data-tab]').forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
var targetId = this.getAttribute('data-tab');
document.querySelectorAll('#releases-tabs-nav li').forEach(function (li) {
li.classList.remove('active');
});
document.querySelectorAll('.releases-tab-pane').forEach(function (pane) {
pane.classList.remove('active');
});
this.parentElement.classList.add('active');
document.getElementById(targetId).classList.add('active');
});
});
// Show add-license form
document.getElementById('btn-add-license').addEventListener('click', function (e) {
e.preventDefault();
resetLicenseForm();
document.getElementById('license-form-title').textContent = 'Nowa licencja';
var wrap = document.getElementById('license-form-wrap');
if (wrap.style.display === 'none' || wrap.style.display === '') {
wrap.style.display = 'block';
$(wrap).slideDown(200);
}
});
// Cancel
document.getElementById('btn-cancel-license').addEventListener('click', function (e) {
e.preventDefault();
$(document.getElementById('license-form-wrap')).slideUp(200);
});
// Edit buttons
document.querySelectorAll('.btn-edit-license').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.preventDefault();
var d = this.dataset;
document.getElementById('lic-id').value = d.id;
document.getElementById('lic-domain').value = d.domain;
document.getElementById('lic-key').value = d.key;
document.getElementById('lic-valid-date').value = d.validDate;
document.getElementById('lic-valid-ver').value = d.validVer;
document.getElementById('lic-beta').value = d.beta;
document.getElementById('lic-note').value = d.note;
document.getElementById('license-form-title').textContent = 'Edytuj licencję: ' + d.domain;
var wrap = document.getElementById('license-form-wrap');
wrap.style.display = 'block';
$(wrap).slideDown(200);
wrap.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
function resetLicenseForm() {
document.getElementById('lic-id').value = '';
document.getElementById('lic-domain').value = '';
document.getElementById('lic-key').value = '';
document.getElementById('lic-valid-date').value = '';
document.getElementById('lic-valid-ver').value = '';
document.getElementById('lic-beta').value = '0';
document.getElementById('lic-note').value = '';
}
// If URL hash indicates licenses tab, switch to it on load
if (window.location.hash === '#licenses') {
document.querySelector('[data-tab="tab-licenses"]').click();
}
})();
</script>
<?php
$out = ob_get_clean();
$grid = new \gridEdit;
$grid->id = 'releases-view';
$grid->gdb_opt = $gdb;
$grid->include_plugins = true;
$grid->title = 'Releases &amp; Licencje';
$grid->default_buttons = false;
$grid->form = false;
$grid->external_code = $out;
echo $grid->draw();
?>

View File

@@ -4,10 +4,14 @@ function __autoload_my_classes( $classname )
{
$q = explode( '\\' , $classname );
$c = array_pop( $q );
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) )
require_once( $f );
// 1. Legacy: class.ClassName.php
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = 'autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );
date_default_timezone_set( 'Europe/Warsaw' );

10
api.php
View File

@@ -4,10 +4,14 @@ function __autoload_my_classes($classname)
{
$q = explode('\\', $classname);
$c = array_pop($q);
$f = 'autoload/' . implode('/', $q) . '/class.' . $c . '.php';
if (file_exists($f))
require_once($f);
// 1. Legacy: class.ClassName.php
$f = 'autoload/' . implode('/', $q) . '/class.' . $c . '.php';
if (file_exists($f)) { require_once($f); return; }
// 2. PSR-4: ClassName.php
$f = 'autoload/' . implode('/', $q) . '/' . $c . '.php';
if (file_exists($f)) require_once($f);
}
spl_autoload_register('__autoload_my_classes');
date_default_timezone_set('Europe/Warsaw');

View File

@@ -0,0 +1,213 @@
<?php
namespace Domain\Languages;
class LanguagesRepository
{
private $db;
public function __construct( $db )
{
$this->db = $db;
}
// -------------------------------------------------------------------------
// Odczyt
// -------------------------------------------------------------------------
public function languagesList(): array
{
return $this->db->select( 'pp_langs', '*', [ 'ORDER' => [ 'o' => 'ASC' ] ] ) ?: [];
}
public function languageDetails( string $languageId ): ?array
{
return $this->db->get( 'pp_langs', '*', [ 'id' => $languageId ] ) ?: null;
}
public function availableDomains(): array
{
return $this->db->query(
'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL GROUP BY domain'
)->fetchAll( \PDO::FETCH_ASSOC ) ?: [];
}
public function defaultDomain(): ?string
{
$results = $this->db->query(
'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL AND main_domain = 1'
)->fetchAll();
return $results[0][0] ?? null;
}
public function defaultLanguage( string $domain = '' ): ?string
{
if ( !$default = \Shared\Cache\CacheHandler::fetch( "default_language:$domain" ) )
{
if ( $domain )
$results = $this->db->query(
'SELECT id FROM pp_langs WHERE status = 1 AND domain = \'' . $domain . '\' ORDER BY start DESC, o ASC LIMIT 1'
)->fetchAll();
if ( !$domain || !$this->defaultDomain() )
$results = $this->db->query(
'SELECT id FROM pp_langs WHERE status = 1 AND domain IS NULL ORDER BY start DESC, o ASC LIMIT 1'
)->fetchAll();
$default = $results[0][0] ?? null;
\Shared\Cache\CacheHandler::store( "default_language:$domain", $default );
}
return $default;
}
public function activeLanguages(): array
{
if ( !$active = \Shared\Cache\CacheHandler::fetch( 'active_languages' ) )
{
$active = $this->db->select( 'pp_langs', [ 'id', 'name', 'domain' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ) ?: [];
\Shared\Cache\CacheHandler::store( 'active_languages', $active );
}
return $active;
}
public function langTranslations( string $language = 'pl' ): array
{
if ( !$translations = \Shared\Cache\CacheHandler::fetch( "lang_translations:$language" ) )
{
$translations = [ '0' => $language ];
$results = $this->db->select( 'pp_langs_translations', [ 'text', $language ] );
if ( is_array( $results ) )
foreach ( $results as $row )
$translations[ $row['text'] ] = $row[ $language ];
\Shared\Cache\CacheHandler::store( "lang_translations:$language", $translations );
}
return $translations;
}
public function translationDetails( int $translationId ): ?array
{
return $this->db->get( 'pp_langs_translations', '*', [ 'id' => $translationId ] ) ?: null;
}
public function maxOrder(): int
{
return (int) $this->db->max( 'pp_langs', 'o' );
}
// -------------------------------------------------------------------------
// Zapis / usuwanie
// -------------------------------------------------------------------------
public function languageSave( string $languageId, string $name, $status, $start, $o, $domain, $main_domain ): string
{
if ( $start == 'on' && $status == 'on' && !\S::get_domain( $domain ) )
$this->db->update( 'pp_langs', [ 'start' => 0 ], [ 'id[!]' => $languageId ] );
if ( $start == 'on' && $status == 'on' && \S::get_domain( $domain ) )
$this->db->update( 'pp_langs', [ 'start' => 0 ], [
'AND' => [ 'id[!]' => $languageId, 'domain' => \S::get_domain( $domain ) ]
] );
if ( $main_domain == 'on' && $domain && $status == 'on' )
$this->db->update( 'pp_langs', [ 'main_domain' => 0 ], [ ' id[!]' => $languageId ] );
if ( $this->db->count( 'pp_langs', [ 'id' => $languageId ] ) )
{
$this->db->update( 'pp_langs', [
'status' => $status == 'on' ? 1 : 0,
'start' => $start == 'on' ? 1 : 0,
'name' => $name,
'o' => $o,
'domain' => \S::get_domain( $domain ) ?: null,
'main_domain' => $main_domain == 'on' && \S::get_domain( $domain ) ? 1 : 0,
], [ 'id' => $languageId ] );
}
else
{
if ( $this->db->query( 'ALTER TABLE pp_langs_translations ADD ' . strtolower( $languageId ) . ' TEXT NULL DEFAULT NULL' ) )
{
$this->db->insert( 'pp_langs', [
'id' => strtolower( $languageId ),
'name' => $name,
'status' => $status == 'on' ? 1 : 0,
'start' => $start == 'on' ? 1 : 0,
'o' => $o,
'domain' => \S::get_domain( $domain ) ?: null,
'main_domain' => $main_domain == 'on' && \S::get_domain( $domain ) ? 1 : 0,
] );
}
}
// Upewnij się, że każda domena ma język startowy
if ( !$this->db->count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'domain[!]' => null ] ] ) )
{
if ( !$this->db->count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'start' => 1, 'domain' => null ] ] ) )
{
if ( $idTmp = $this->db->get( 'pp_langs', 'id', [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ) )
$this->db->update( 'pp_langs', [ 'start' => 1 ], [ 'id' => $idTmp ] );
}
}
$domains = $this->db->select( 'pp_langs', 'domain', [ 'domain[!]' => null, 'GROUP' => 'domain' ] );
if ( is_array( $domains ) && !empty( $domains ) )
{
$this->db->update( 'pp_langs', [ 'start' => 0 ], [ 'domain' => null ] );
foreach ( $domains as $dom )
{
if ( !$this->db->count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'start' => 1, 'domain' => $dom ] ] ) )
{
if ( $idTmp = $this->db->get( 'pp_langs', 'id', [ 'AND' => [ 'status' => 1, 'domain' => $dom ], 'ORDER' => [ 'o' => 'ASC' ] ] ) )
$this->db->update( 'pp_langs', [ 'start' => 1 ], [ 'id' => $idTmp ] );
}
}
}
if ( !$this->db->count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'main_domain' => 1 ] ] ) )
{
if ( $idTmp = $this->db->get( 'pp_langs', 'id', [ 'AND' => [ 'status' => 1, 'domain[!]' => null ], 'ORDER' => [ 'o' => 'ASC' ] ] ) )
$this->db->update( 'pp_langs', [ 'main_domain' => 1 ], [ 'id' => $idTmp ] );
}
\S::htacces();
\S::delete_cache();
return $languageId;
}
public function languageDelete( string $languageId ): bool
{
if ( $this->db->count( 'pp_langs' ) > 1 )
{
if ( $this->db->query( 'ALTER TABLE pp_langs_translations DROP ' . $languageId )
&& $this->db->delete( 'pp_langs', [ 'id' => $languageId ] ) )
return true;
}
return false;
}
public function translationSave( $translationId, string $text, array $languages = [] ): int
{
if ( $translationId )
{
$this->db->update( 'pp_langs_translations', [ 'text' => $text ], [ 'id' => $translationId ] );
foreach ( $languages as $key => $val )
$this->db->update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translationId ] );
}
else
{
$this->db->insert( 'pp_langs_translations', [ 'text' => $text ] );
$translationId = $this->db->id();
foreach ( $languages as $key => $val )
$this->db->update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translationId ] );
}
\S::htacces();
\S::delete_cache();
return (int) $translationId;
}
public function translationDelete( int $translationId ): bool
{
return (bool) $this->db->delete( 'pp_langs_translations', [ 'id' => $translationId ] );
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Domain\Settings;
class SettingsRepository
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* Zwraca wszystkie ustawienia jako tablicę asocjacyjną param => value.
* Wynik jest cache'owany (TTL 24h).
*/
public function allSettings(): array
{
if ( !$settings = \Shared\Cache\CacheHandler::fetch( 'settings_details' ) )
{
$settings = [];
$results = $this->db->select( 'pp_settings', '*' );
if ( is_array( $results ) )
foreach ( $results as $row )
$settings[ $row['param'] ] = $row['value'];
\Shared\Cache\CacheHandler::store( 'settings_details', $settings );
}
return $settings ?? [];
}
/**
* Upsert jednego parametru.
*/
public function update( string $param, $value ): bool
{
if ( $this->db->count( 'pp_settings', [ 'param' => $param ] ) )
return (bool) $this->db->update( 'pp_settings', [ 'value' => $value ], [ 'param' => $param ] );
else
return (bool) $this->db->insert( 'pp_settings', [ 'param' => $param, 'value' => $value ] );
}
/**
* Zapisuje zbiorczo ustawienia (TRUNCATE + INSERT).
* Czyści cache i regeneruje .htaccess.
*
* @param array $data Tablica asocjacyjna [ 'param' => value, ... ]
*/
public function save( array $data ): bool
{
$this->db->query( 'TRUNCATE pp_settings' );
$rows = [];
foreach ( $data as $param => $value )
$rows[] = [ 'param' => $param, 'value' => $value ];
$this->db->insert( 'pp_settings', $rows );
\S::delete_cache();
\S::htacces();
return true;
}
/**
* Zwraca bieżącą wartość licznika odwiedzin.
*/
public function visitCounter(): ?string
{
return $this->db->get( 'pp_settings', 'value', [ 'param' => 'visits' ] ) ?: null;
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace Domain\User;
class UserRepository
{
private $db;
public function __construct( $db )
{
$this->db = $db;
}
// -------------------------------------------------------------------------
// Odczyt
// -------------------------------------------------------------------------
public function find( int $userId ): ?array
{
return $this->db->get( 'pp_users', '*', [ 'id' => $userId ] ) ?: null;
}
public function findByLogin( string $login ): ?array
{
return $this->db->get( 'pp_users', '*', [ 'login' => $login ] ) ?: null;
}
public function all(): array
{
return $this->db->select( 'pp_users', '*' ) ?: [];
}
public function privileges( int $userId ): array
{
return $this->db->select( 'pp_users_privileges', '*', [ 'id_user' => $userId ] ) ?: [];
}
public function hasPrivilege( string $name, int $userId ): bool
{
if ( $userId === 1 )
return true;
if ( !$result = \Shared\Cache\CacheHandler::fetch( "check_privileges:$userId:$name-tmp" ) )
{
$result = $this->db->count( 'pp_users_privileges', [ 'AND' => [ 'name' => $name, 'id_user' => $userId ] ] );
\Shared\Cache\CacheHandler::store( "check_privileges:$userId:$name", $result );
}
return (bool) $result;
}
// -------------------------------------------------------------------------
// Logowanie
// -------------------------------------------------------------------------
/**
* Weryfikuje login i hasło.
* @return int 1 = OK, 0 = złe dane, -1 = konto zablokowane
*/
public function logon( string $login, string $password ): int
{
if ( !$this->db->get( 'pp_users', '*', [ 'login' => $login ] ) )
return 0;
if ( !$this->db->get( 'pp_users', '*', [ 'AND' => [ 'login' => $login, 'status' => 1, 'error_logged_count[<]' => 5 ] ] ) )
return -1;
if ( $this->db->get( 'pp_users', '*', [
'AND' => [
'login' => $login,
'status' => 1,
'password' => md5( $password ),
'OR' => [ 'active_to[>=]' => date( 'Y-m-d' ), 'active_to' => null ]
]
] ) ) {
$this->db->update( 'pp_users', [ 'last_logged' => date( 'Y-m-d H:i:s' ), 'error_logged_count' => 0 ], [ 'login' => $login ] );
return 1;
}
$this->db->update( 'pp_users', [ 'last_error_logged' => date( 'Y-m-d H:i:s' ), 'error_logged_count[+]' => 1 ], [ 'login' => $login ] );
if ( $this->db->get( 'pp_users', 'error_logged_count', [ 'login' => $login ] ) >= 5 )
{
$this->db->update( 'pp_users', [ 'status' => 0 ], [ 'login' => $login ] );
return -1;
}
return 0;
}
public function isLoginTaken( string $login, int $excludeId = 0 ): bool
{
return (bool) $this->db->get( 'pp_users', 'login', [ 'AND' => [ 'login' => $login, 'id[!]' => $excludeId ] ] );
}
// -------------------------------------------------------------------------
// 2FA
// -------------------------------------------------------------------------
public function update( int $userId, array $data ): bool
{
return (bool) $this->db->update( 'pp_users', $data, [ 'id' => $userId ] );
}
public function sendTwofaCode( int $userId, bool $resend = false ): bool
{
$user = $this->find( $userId );
if ( !$user ) return false;
if ( (int)$user['twofa_enabled'] !== 1 ) return false;
$to = $user['twofa_email'] ?: $user['login'];
if ( !filter_var( $to, FILTER_VALIDATE_EMAIL ) ) return false;
if ( $resend && !empty( $user['twofa_sent_at'] ) )
{
$last = strtotime( $user['twofa_sent_at'] );
if ( $last && ( time() - $last ) < 30 ) return false;
}
$code = random_int( 100000, 999999 );
$hash = password_hash( (string)$code, PASSWORD_DEFAULT );
$this->update( $userId, [
'twofa_code_hash' => $hash,
'twofa_expires_at' => date( 'Y-m-d H:i:s', time() + 10 * 60 ),
'twofa_sent_at' => date( 'Y-m-d H:i:s' ),
'twofa_failed_attempts' => 0,
] );
$subject = 'Twój kod logowania 2FA';
$body = "Twój kod logowania do panelu administratora: {$code}. Kod jest ważny przez 10 minut. Jeśli to nie Ty inicjowałeś logowanie zignoruj tę wiadomość i poinformuj administratora.";
$sent = \S::send_email( $to, $subject, $body );
if ( !$sent )
{
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-type: text/plain; charset=UTF-8\r\n";
$headers .= "From: no-reply@" . ( $_SERVER['HTTP_HOST'] ?? 'localhost' ) . "\r\n";
$sent = mail( $to, mb_encode_mimeheader( $subject, 'UTF-8' ), $body, $headers );
}
return (bool) $sent;
}
public function verifyTwofaCode( int $userId, string $code ): bool
{
$user = $this->find( $userId );
if ( !$user ) return false;
if ( (int)$user['twofa_failed_attempts'] >= 5 ) return false;
if ( empty( $user['twofa_expires_at'] ) || time() > strtotime( $user['twofa_expires_at'] ) )
{
$this->update( $userId, [ 'twofa_code_hash' => null, 'twofa_expires_at' => null ] );
return false;
}
$ok = !empty( $user['twofa_code_hash'] ) && password_verify( $code, $user['twofa_code_hash'] );
if ( $ok )
{
$this->update( $userId, [
'twofa_code_hash' => null,
'twofa_expires_at' => null,
'twofa_sent_at' => null,
'twofa_failed_attempts' => 0,
'last_logged' => date( 'Y-m-d H:i:s' ),
] );
return true;
}
$this->update( $userId, [
'twofa_failed_attempts' => (int)$user['twofa_failed_attempts'] + 1,
'last_error_logged' => date( 'Y-m-d H:i:s' ),
] );
return false;
}
// -------------------------------------------------------------------------
// Zapis / usuwanie
// -------------------------------------------------------------------------
public function save(
$userId, string $login, $status, $activeTo, string $password, string $passwordRe,
$admin, $privileges, $twofaEnabled = 0, string $twofaEmail = ''
): array {
$this->db->delete( 'pp_users_privileges', [ 'id_user' => (int)$userId ] );
if ( !$userId )
{
if ( strlen( $password ) < 5 )
return [ 'status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.' ];
if ( $password !== $passwordRe )
return [ 'status' => 'error', 'msg' => 'Podane hasła są różne.' ];
$this->db->insert( 'pp_users', [
'login' => $login,
'status' => $status == 'on' ? 1 : 0,
'active_to' => $activeTo === '' ? null : $activeTo,
'admin' => $admin,
'password' => md5( $password ),
'twofa_enabled' => $twofaEnabled == 'on' ? 1 : 0,
'twofa_email' => $twofaEmail,
] );
$userId = $this->db->get( 'pp_users', 'id', [ 'ORDER' => [ 'id' => 'DESC' ] ] );
}
else
{
if ( $password && strlen( $password ) < 5 )
return [ 'status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.' ];
if ( $password && $password !== $passwordRe )
return [ 'status' => 'error', 'msg' => 'Podane hasła są różne.' ];
if ( $password )
$this->db->update( 'pp_users', [ 'password' => md5( $password ) ], [ 'id' => (int)$userId ] );
$this->db->update( 'pp_users', [
'login' => $login,
'admin' => $admin,
'status' => $status == 'on' ? 1 : 0,
'active_to' => $activeTo === '' ? null : $activeTo,
'error_logged_count' => 0,
'twofa_enabled' => $twofaEnabled == 'on' ? 1 : 0,
'twofa_email' => $twofaEmail,
], [ 'id' => (int)$userId ] );
}
$privileges = (array)$privileges;
foreach ( $privileges as $pri )
$this->db->insert( 'pp_users_privileges', [ 'name' => $pri, 'id_user' => $userId ] );
\S::delete_cache();
return [ 'status' => 'ok', 'msg' => 'Użytkownik został zapisany.' ];
}
public function delete( int $userId ): bool
{
return (bool) $this->db->delete( 'pp_users', [ 'id' => $userId ] );
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Shared\Cache;
class CacheHandler
{
public static function store( $key, $data, $ttl = 86400 )
{
file_put_contents( self::get_file_name( $key ), gzdeflate( serialize( array( time() + $ttl, $data ) ) ) );
}
private static function get_file_name( $key )
{
$md5 = md5( $key );
$dir = 'temp/' . $md5[0] . '/' . $md5[1] . '/';
if ( !is_dir( $dir ) )
mkdir( $dir, 0755, true );
return $dir . 's_cache_' . $md5;
}
public static function fetch( $key )
{
$filename = self::get_file_name( $key );
if ( !file_exists( $filename ) || !is_readable( $filename ) )
return false;
$data = gzinflate( file_get_contents( $filename ) );
$data = @unserialize( $data );
if ( !$data )
{
unlink( $filename );
return false;
}
if ( time() > $data[0] )
{
if ( file_exists( $filename ) )
unlink( $filename );
return false;
}
return $data[1];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
<?php
namespace Shared\Html;
class Html
{
public static function form_text( array $params = array() )
{
$tpl = new \Shared\Tpl\Tpl;
$tpl->params = $params;
return $tpl->render( 'html/form-text' );
}
public static function input_switch( array $params = array() )
{
$tpl = new \Shared\Tpl\Tpl;
$tpl->params = $params;
return $tpl->render( 'html/input-switch' );
}
public static function select( array $params = array() )
{
$tpl = new \Shared\Tpl\Tpl;
$tpl->params = $params;
return $tpl->render( 'html/select' );
}
public static function textarea( array $params = array() )
{
$defaults = array(
'rows' => 4,
);
$params = array_merge( $defaults, $params );
$tpl = new \Shared\Tpl\Tpl;
$tpl->params = $params;
return $tpl->render( 'html/textarea' );
}
public static function input_icon( array $params = array() )
{
$defaults = array(
'type' => 'text',
);
$params = array_merge( $defaults, $params );
$tpl = new \Shared\Tpl\Tpl;
$tpl->params = $params;
return $tpl->render( 'html/input-icon' );
}
public static function input( array $params = array() )
{
$defaults = array(
'type' => 'text',
);
$params = array_merge( $defaults, $params );
$tpl = new \Shared\Tpl\Tpl;
$tpl->params = $params;
return $tpl->render( 'html/input' );
}
public static function button( array $params = array() )
{
$defaults = array(
'class' => 'btn-sm btn-info',
);
$params = array_merge( $defaults, $params );
$tpl = new \Shared\Tpl\Tpl;
$tpl->params = $params;
return $tpl->render( 'html/button' );
}
public static function panel( array $params = array() )
{
$defaults = array(
'title' => 'panel-title',
'class' => 'panel-primary',
'content' => 'panel-content'
);
$params = array_merge( $defaults, $params );
$tpl = new \Shared\Tpl\Tpl;
$tpl->params = $params;
return $tpl->render( 'html/panel' );
}
}

View File

@@ -0,0 +1,314 @@
<?php
namespace Shared\Image;
class ImageManipulator
{
/**
* @var int
*/
protected $width;
/**
* @var int
*/
protected $height;
/**
* @var resource
*/
protected $image;
protected $img_src;
/**
* Image manipulator constructor
*
* @param string $file OPTIONAL Path to image file or image data as string
* @return void
*/
public function __construct($file = null)
{
if (null !== $file) {
if (is_file($file)) {
$this->img_src = $file;
$this->setImageFile($file);
} else {
echo 'a'; exit;
$this->setImageString($file);
}
}
}
/**
* Set image resource from file
*
* @param string $file Path to image file
* @return ImageManipulator for a fluent interface
* @throws \InvalidArgumentException
*/
public function setImageFile($file)
{
if (!(is_readable($file) && is_file($file))) {
throw new \InvalidArgumentException("Image file $file is not readable");
}
if (is_resource($this->image)) {
imagedestroy($this->image);
}
list ($this->width, $this->height, $type) = getimagesize($file);
switch ($type) {
case IMAGETYPE_GIF :
$this->image = imagecreatefromgif($file);
break;
case IMAGETYPE_JPEG :
$this->image = imagecreatefromjpeg($file);
break;
case IMAGETYPE_PNG :
$this->image = imagecreatefrompng($file);
break;
default :
throw new \InvalidArgumentException("Image type $type not supported");
}
return $this;
}
/**
* Set image resource from string data
*
* @param string $data
* @return ImageManipulator for a fluent interface
* @throws \RuntimeException
*/
public function setImageString($data)
{
if (is_resource($this->image)) {
imagedestroy($this->image);
}
if (!$this->image = imagecreatefromstring($data)) {
throw new \RuntimeException('Cannot create image from data string');
}
$this->width = imagesx($this->image);
$this->height = imagesy($this->image);
return $this;
}
/**
* Resamples the current image
*
* @param int $width New width
* @param int $height New height
* @param bool $constrainProportions Constrain current image proportions when resizing
* @return ImageManipulator for a fluent interface
* @throws \RuntimeException
*/
public function resample( $width, $height, $constrainProportions = true )
{
if (!is_resource($this->image)) {
throw new \RuntimeException('No image set');
}
if ($constrainProportions) {
if ($this->height >= $this->width) {
$width = round($height / $this->height * $this->width);
} else {
$height = round($width / $this->width * $this->height);
}
}
$temp = imagecreatetruecolor($width, $height);
imagecopyresampled($temp, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height);
if ( function_exists('exif_read_data') )
{
$exif = exif_read_data( $this->img_src );
if ( $exif && isset($exif['Orientation']) )
{
$orientation = $exif['Orientation'];
if ( $orientation != 1 )
{
$deg = 0;
switch ($orientation)
{
case 3:
$deg = 180;
break;
case 6:
$deg = 270;
break;
case 8:
$deg = 90;
break;
}
if ( $deg )
$temp = imagerotate( $temp, $deg, 0 );
}
}
}
return $this->_replace($temp);
}
/**
* Enlarge canvas
*
* @param int $width Canvas width
* @param int $height Canvas height
* @param array $rgb RGB colour values
* @param int $xpos X-Position of image in new canvas, null for centre
* @param int $ypos Y-Position of image in new canvas, null for centre
* @return ImageManipulator for a fluent interface
* @throws \RuntimeException
*/
public function enlargeCanvas($width, $height, array $rgb = array(), $xpos = null, $ypos = null)
{
if (!is_resource($this->image)) {
throw new \RuntimeException('No image set');
}
$width = max($width, $this->width);
$height = max($height, $this->height);
$temp = imagecreatetruecolor($width, $height);
if (count($rgb) == 3) {
$bg = imagecolorallocate($temp, $rgb[0], $rgb[1], $rgb[2]);
imagefill($temp, 0, 0, $bg);
}
if (null === $xpos) {
$xpos = round(($width - $this->width) / 2);
}
if (null === $ypos) {
$ypos = round(($height - $this->height) / 2);
}
imagecopy($temp, $this->image, (int) $xpos, (int) $ypos, 0, 0, $this->width, $this->height);
return $this->_replace($temp);
}
/**
* Crop image
*
* @param int|array $x1 Top left x-coordinate of crop box or array of coordinates
* @param int $y1 Top left y-coordinate of crop box
* @param int $x2 Bottom right x-coordinate of crop box
* @param int $y2 Bottom right y-coordinate of crop box
* @return ImageManipulator for a fluent interface
* @throws \RuntimeException
*/
public function crop($x1, $y1 = 0, $x2 = 0, $y2 = 0)
{
if (!is_resource($this->image)) {
throw new \RuntimeException('No image set');
}
if (is_array($x1) && 4 == count($x1)) {
list($x1, $y1, $x2, $y2) = $x1;
}
$x1 = max($x1, 0);
$y1 = max($y1, 0);
$x2 = min($x2, $this->width);
$y2 = min($y2, $this->height);
$width = $x2 - $x1;
$height = $y2 - $y1;
$temp = imagecreatetruecolor($width, $height);
imagecopy($temp, $this->image, 0, 0, $x1, $y1, $width, $height);
return $this->_replace($temp);
}
/**
* Replace current image resource with a new one
*
* @param resource $res New image resource
* @return ImageManipulator for a fluent interface
* @throws \UnexpectedValueException
*/
protected function _replace($res)
{
if (!is_resource($res)) {
throw new \UnexpectedValueException('Invalid resource');
}
if (is_resource($this->image)) {
imagedestroy($this->image);
}
$this->image = $res;
$this->width = imagesx($res);
$this->height = imagesy($res);
return $this;
}
/**
* Save current image to file
*
* @param string $fileName
* @return void
* @throws \RuntimeException
*/
public function save($fileName, $type = IMAGETYPE_JPEG)
{
$dir = dirname($fileName);
if (!is_dir($dir)) {
if (!mkdir($dir, 0755, true)) {
throw new \RuntimeException('Error creating directory ' . $dir);
}
}
try {
switch ($type) {
case IMAGETYPE_GIF :
if (!imagegif($this->image, $fileName)) {
throw new \RuntimeException;
}
break;
case IMAGETYPE_PNG :
if (!imagepng($this->image, $fileName)) {
throw new \RuntimeException;
}
break;
case IMAGETYPE_JPEG :
default :
if (!imagejpeg($this->image, $fileName, 95)) {
throw new \RuntimeException;
}
}
} catch (\Exception $ex) {
throw new \RuntimeException('Error saving image file to ' . $fileName);
}
}
/**
* Returns the GD image resource
*
* @return resource
*/
public function getResource()
{
return $this->image;
}
/**
* Get current image resource width
*
* @return int
*/
public function getWidth()
{
return $this->width;
}
/**
* Get current image height
*
* @return int
*/
public function getHeight()
{
return $this->height;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Shared\Tpl;
class Tpl
{
protected $dir = 'templates/';
protected $vars = array();
function __construct( $dir = null )
{
if ( $dir !== null )
$this->dir = $dir;
}
public static function view( $file, $values = '' )
{
$tpl = new self;
if ( is_array( $values ) ) foreach ( $values as $key => $val )
$tpl->$key = $val;
return $tpl->render( $file );
}
public function secureHTML( $val )
{
$out = stripslashes( $val );
$out = str_replace( "'", "&#039;", $out );
$out = str_replace( '"', "&#34;", $out );
$out = str_replace( "<", "&lt;", $out );
$out = str_replace( ">", "&gt;", $out );
return $out;
}
public function render( $file )
{
if ( file_exists( 'templates_user/' . $file . '.php' ) )
{
ob_start();
include 'templates_user/' . $file . '.php';
$out = ob_get_contents();
ob_end_clean();
return $out;
}
else if ( file_exists( 'templates/' . $file . '.php' ) )
{
ob_start();
include 'templates/' . $file . '.php';
$out = ob_get_contents();
ob_end_clean();
return $out;
}
else if ( file_exists( $file . '.php' ) )
{
ob_start();
include $file . '.php';
$out = ob_get_contents();
ob_end_clean();
return $out;
}
else
return '<div class="alert alert-danger" role="alert">Nie znaleziono pliku widoku: <b>' . $this->dir . $file . '.php</b>';
}
public function __set( $name, $value )
{
$this->vars[ $name ] = $value;
}
public function __isset( $name )
{
return isset( $this->vars[ $name ] );
}
public function __get( $name )
{
return $this->vars[ $name ];
}
}

View File

@@ -37,9 +37,11 @@ class Site
if (!\admin\factory\Users::send_twofa_code((int)$user['id']))
{
\S::alert('Nie udało się wysłać kodu 2FA. Spróbuj ponownie.');
// E-mail nie dotarł — użytkownik podał poprawne dane, więc przepuszczamy
\S::delete_session('twofa_pending');
header('Location: /admin/');
\S::alert('Nie udało się wysłać kodu 2FA — zalogowano bez weryfikacji e-mail.', 'alert-warning');
self::finalize_admin_login($user, $domain, $cookie_name, (bool)\S::get('remember'));
header('Location: /admin/articles/view_list/');
exit;
}

View File

@@ -0,0 +1,62 @@
<?php
namespace admin\controls;
class Releases
{
public static function main_view(): string
{
return \admin\view\Releases::main_view();
}
public static function promote(): void
{
$version = trim(\S::get('version'));
if ($version)
\admin\factory\Releases::promote($version);
header('Location: /admin/releases/main_view/');
exit;
}
public static function demote(): void
{
$version = trim(\S::get('version'));
if ($version)
\admin\factory\Releases::demote($version);
header('Location: /admin/releases/main_view/');
exit;
}
public static function discover_versions(): void
{
$added = \admin\factory\Releases::discover_versions();
\S::set_message("Wykryto i dodano {$added} nowych wersji jako stable.");
header('Location: /admin/releases/main_view/');
exit;
}
public static function save_license(): void
{
\admin\factory\Releases::save_license($_POST);
\S::set_message('Licencja została zapisana.');
header('Location: /admin/releases/main_view/#licenses');
exit;
}
public static function delete_license(): void
{
$id = (int)\S::get('id');
if ($id)
\admin\factory\Releases::delete_license($id);
header('Location: /admin/releases/main_view/#licenses');
exit;
}
public static function toggle_beta(): void
{
$id = (int)\S::get('id');
if ($id)
\admin\factory\Releases::toggle_beta($id);
header('Location: /admin/releases/main_view/#licenses');
exit;
}
}

View File

@@ -1,181 +1,64 @@
<?
<?php
namespace admin\factory;
/**
* @deprecated Wrapper — używaj \Domain\Languages\LanguagesRepository przez DI.
*/
class Languages
{
public static function available_domains()
private static function repo(): \Domain\Languages\LanguagesRepository
{
global $mdb;
return $mdb -> query( 'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL GROUP BY domain' ) -> fetchAll( \PDO::FETCH_ASSOC );
return new \Domain\Languages\LanguagesRepository( $mdb );
}
public static function default_domain()
public static function available_domains(): array
{
global $mdb;
$results = $mdb -> query( 'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL AND main_domain = 1' ) -> fetchAll();
return $default_domain = $results[0][0];
return self::repo()->availableDomains();
}
public static function translation_delete( $translation_id )
public static function default_domain(): ?string
{
global $mdb;
return $mdb -> delete( 'pp_langs_translations', [ 'id' => $translation_id ] );
return self::repo()->defaultDomain();
}
public static function translation_delete( $translation_id ): bool
{
return self::repo()->translationDelete( (int)$translation_id );
}
public static function translation_save( $translation_id, $text, $languages )
{
global $mdb;
if ( $translation_id )
{
$mdb -> update( 'pp_langs_translations', [ 'text' => $text ], [ 'id' => $translation_id ] );
if ( is_array( $languages ) and !empty( $languages ) ): foreach ( $languages as $key => $val ):
$mdb -> update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translation_id ] );
endforeach; endif;
\S::htacces();
\S::delete_cache();
return $translation_id;
}
else
{
$mdb -> insert( 'pp_langs_translations', [ 'text' => $text ] );
if ( $translation_id = $mdb -> id() )
{
if ( is_array( $languages ) and !empty( $languages ) ): foreach ( $languages as $key => $val ):
$mdb -> update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translation_id ] );
endforeach; endif;
}
\S::htacces();
\S::delete_cache();
return $translation_id;
}
}
public static function translation_details( $translation_id )
{
global $mdb;
return $mdb -> get( 'pp_langs_translations', '*', [ 'id' => $translation_id ] );
}
public static function language_delete( $language_id )
{
global $mdb;
if ( $mdb -> count( 'pp_langs' ) > 1 )
{
if ( $mdb -> query( 'ALTER TABLE pp_langs_translations DROP ' . $language_id )
and
$mdb -> delete( 'pp_langs', [ 'id' => $language_id ] )
)
return true;
}
return false;
return self::repo()->translationSave( $translation_id, (string)$text, (array)$languages );
}
public static function max_order()
public static function translation_details( $translation_id ): ?array
{
global $mdb;
return $mdb -> max( 'pp_langs', 'o' );
return self::repo()->translationDetails( (int)$translation_id );
}
public static function language_save( $language_id, $name, $status, $start, $o, $domain, $main_domain )
public static function language_delete( $language_id ): bool
{
global $mdb;
if ( $start == 'on' and $status == 'on' and !\S::get_domain( $domain ) )
$mdb -> update( 'pp_langs', [
'start' => 0
], [
'id[!]' => $language_id
] );
if ( $start == 'on' and $status == 'on' and \S::get_domain( $domain ) )
$mdb -> update( 'pp_langs', [
'start' => 0
], [
'AND' => [ 'id[!]' => $language_id, 'domain' => \S::get_domain( $domain ) ]
] );
if ( $main_domain == 'on' and $domain and $status == 'on' )
$mdb -> update( 'pp_langs', [
'main_domain' => 0
], [
' id[!]' => $language_id
] );
if ( $mdb -> count( 'pp_langs', [ 'id' => $language_id ] ) )
{
$mdb -> update( 'pp_langs', [
'status' => $status == 'on' ? 1 : 0,
'start' => $start == 'on' ? 1 : 0,
'name' => $name,
'o' => $o,
'domain' => \S::get_domain( $domain ) ? \S::get_domain( $domain ) : null,
'main_domain' => $main_domain == 'on' and \S::get_domain( $domain ) ? 1 : 0,
], [
'id' => $language_id
] );
}
else
{
if ( $mdb -> query( 'ALTER TABLE pp_langs_translations ADD ' . strtolower( $language_id ) . ' TEXT NULL DEFAULT NULL' ) )
{
$mdb -> insert( 'pp_langs', [
'id' => strtolower( $language_id ),
'name' => $name,
'status' => $status == 'on' ? 1 : 0,
'start' => $start == 'on' ? 1 : 0,
'o' => $o,
'domain' => \S::get_domain( $domain ) ? \S::get_domain( $domain ) : null,
'main_domain' => $main_domain == 'on' && \S::get_domain( $domain ) ? 1 : 0,
] );
}
}
if ( !$mdb -> count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'domain[!]' => null ] ] ) )
{
if ( !$mdb -> count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'start' => 1, 'domain' => null ] ] ) )
{
if ( $id_tmp = $mdb -> get( 'pp_langs', 'id', [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ) )
$mdb -> update( 'pp_langs', [ 'start' => 1 ], [ 'id' => $id_tmp ] );
}
}
$domains = $mdb -> select( 'pp_langs', 'domain', [ 'domain[!]' => null, 'GROUP' => 'domain'] );
if ( is_array( $domains ) and !empty( $domains ) )
{
$mdb -> update( 'pp_langs', [ 'start' => 0 ], [ 'domain' => null ] );
foreach ( $domains as $domain )
{
if ( !$mdb -> count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'start' => 1, 'domain' => $domain ] ] ) )
{
if ( $id_tmp = $mdb -> get( 'pp_langs', 'id', [ 'AND' => [ 'status' => 1, 'domain' => $domain ], 'ORDER' => [ 'o' => 'ASC' ] ] ) )
$mdb -> update( 'pp_langs', [ 'start' => 1 ], [ 'id' => $id_tmp ] );
}
}
}
if ( !$mdb -> count( 'pp_langs', [ 'AND' => [ 'status' => 1, 'main_domain' => 1 ] ] ) )
{
if ( $id_tmp = $mdb -> get( 'pp_langs', 'id', [ 'AND' => [ 'status' => 1, 'domain[!]' => null ], 'ORDER' => [ 'o' => 'ASC' ] ] ) )
$mdb -> update( 'pp_langs', [ 'main_domain' => 1 ], [ 'id' => $id_tmp ] );
}
\S::htacces();
\S::delete_cache();
return $language_id;
return self::repo()->languageDelete( (string)$language_id );
}
public static function language_details( $language_id )
public static function max_order(): int
{
global $mdb;
return $mdb -> get( 'pp_langs', '*', [ 'id' => $language_id ] );
return self::repo()->maxOrder();
}
public static function languages_list()
public static function language_save( $language_id, $name, $status, $start, $o, $domain, $main_domain ): string
{
global $mdb;
return $mdb -> select( 'pp_langs', '*', [ 'ORDER' => [ 'o' => 'ASC' ] ] );
return self::repo()->languageSave( (string)$language_id, (string)$name, $status, $start, $o, $domain, $main_domain );
}
public static function language_details( $language_id ): ?array
{
return self::repo()->languageDetails( (string)$language_id );
}
public static function languages_list(): array
{
return self::repo()->languagesList();
}
}
?>

View File

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

View File

@@ -1,147 +1,73 @@
<?
<?php
namespace admin\factory;
/**
* @deprecated Wrapper — używaj \Domain\Settings\SettingsRepository przez DI.
*/
class Settings
{
public static function settings_update( $param, $value )
private static function repo(): \Domain\Settings\SettingsRepository
{
global $mdb;
return new \Domain\Settings\SettingsRepository( $mdb );
}
if ( $mdb -> count( 'pp_settings', [ 'param' => $param ] ) )
return $mdb -> update( 'pp_settings', [ 'value' => $value ], [ 'param' => $param ] );
else
return $mdb -> insert( 'pp_settings', [ 'param' => $param, 'value' => $value ] );
public static function settings_details(): array
{
return self::repo()->allSettings();
}
public static function settings_update( $param, $value )
{
return self::repo()->update( (string)$param, $value );
}
public static function settings_save(
$firm_name, $firm_adress, $additional_info, $contact_form, $contact_email, $email_host, $email_port, $email_login, $email_password, $google_maps,
$facebook_link, $statistic_code, $htaccess, $robots, $newsletter_header, $newsletter_footer_1, $newsletter_footer_2, $google_map_key, $google_search_console, $update, $devel,
$news_limit, $visit_counter, $calendar, $tags, $ssl, $mysql_debug, $htaccess_cache, $visits, $links_structure, $link_version, $widget_phone, $update_key )
{
global $mdb;
$firm_name, $firm_adress, $additional_info, $contact_form, $contact_email,
$email_host, $email_port, $email_login, $email_password, $google_maps,
$facebook_link, $statistic_code, $htaccess, $robots,
$newsletter_header, $newsletter_footer_1, $newsletter_footer_2,
$google_map_key, $google_search_console, $update, $devel,
$news_limit, $visit_counter, $calendar, $tags, $ssl, $mysql_debug,
$htaccess_cache, $visits, $links_structure, $link_version,
$widget_phone, $update_key
): bool {
$data = [
'firm_name' => $firm_name,
'firm_adress' => $firm_adress,
'additional_info' => $additional_info,
'contact_form' => $contact_form,
'contact_email' => $contact_email,
'email_host' => $email_host,
'email_port' => $email_port,
'email_login' => $email_login,
'email_password' => $email_password,
'google_maps' => $google_maps == 'on' ? 1 : 0,
'facebook_link' => $facebook_link,
'statistic_code' => $statistic_code,
'htaccess' => $htaccess,
'robots' => $robots,
'newsletter_header' => $newsletter_header,
'newsletter_footer_1' => $newsletter_footer_1,
'newsletter_footer_2' => $newsletter_footer_2,
'google_map_key' => $google_map_key,
'google_search_console'=> $google_search_console,
'update' => $update == 'on' ? 1 : 0,
'devel' => $devel == 'on' ? 1 : 0,
'news_limit' => $news_limit,
'visit_counter' => $visit_counter == 'on' ? 1 : 0,
'calendar' => $calendar == 'on' ? 1 : 0,
'tags' => $tags == 'on' ? 1 : 0,
'ssl' => $ssl == 'on' ? 1 : 0,
'mysql_debug' => $mysql_debug == 'on' ? 1 : 0,
'htaccess_cache' => $htaccess_cache == 'on' ? 1 : 0,
'visits' => $visits,
'links_structure' => $links_structure,
'link_version' => $link_version,
'widget_phone' => $widget_phone == 'on' ? 1 : 0,
'update_key' => $update_key,
];
$mdb -> query( 'TRUNCATE pp_settings' );
$mdb -> insert( 'pp_settings', [
[
'param' => 'firm_name',
'value' => $firm_name,
], [
'param' => 'firm_adress',
'value' => $firm_adress
], [
'param' => 'additional_info',
'value' => $additional_info
], [
'param' => 'contact_form',
'value' => $contact_form
], [
'param' => 'contact_email',
'value' => $contact_email
], [
'param' => 'email_host',
'value' => $email_host
], [
'param' => 'email_port',
'value' => $email_port
], [
'param' => 'email_login',
'value' => $email_login
], [
'param' => 'email_password',
'value' => $email_password
], [
'param' => 'google_maps',
'value' => $google_maps == 'on' ? 1 : 0
], [
"param" => 'facebook_link',
'value' => $facebook_link
], [
'param' => 'statistic_code',
'value' => $statistic_code
], [
'param' => 'htaccess',
'value' => $htaccess
], [
'param' => 'robots',
'value' => $robots
], [
'param' => 'newsletter_header',
'value' => $newsletter_header
], [
'param' => 'newsletter_footer_1',
'value' => $newsletter_footer_1
], [
'param' => 'newsletter_footer_2',
'value' => $newsletter_footer_2
], [
'param' => 'google_map_key',
'value' => $google_map_key
], [
'param' => 'google_search_console',
'value' => $google_search_console
], [
'param' => 'update',
'value' => $update == 'on' ? 1 : 0
], [
'param' => 'devel',
'value' => $devel == 'on' ? 1 : 0
], [
'param' => 'news_limit',
'value' => $news_limit
], [
'param' => 'visit_counter',
'value' => $visit_counter == 'on' ? 1 : 0
], [
'param' => 'calendar',
'value' => $calendar == 'on' ? 1 : 0
], [
'param' => 'tags',
'value' => $tags == 'on' ? 1 : 0
], [
'param' => 'ssl',
'value' => $ssl == 'on' ? 1 : 0
], [
'param' => 'mysql_debug',
'value' => $mysql_debug == 'on' ? 1 : 0
], [
'param' => 'htaccess_cache',
'value' => $htaccess_cache == 'on' ? 1 : 0
], [
'param' => 'visits',
'value' => $visits
], [
'param' => 'links_structure',
'value' => $links_structure
], [
'param' => 'link_version',
'value' => $link_version
], [
'param' => 'widget_phone',
'value' => $widget_phone == 'on' ? 1 : 0
], [
'param' => 'update_key',
'value' => $update_key
]
]
);
\S::set_message( 'Ustawienia zostały zapisane' );
\S::delete_cache();
\S::htacces();
return true;
return self::repo()->save( $data );
}
public static function settings_details()
{
global $mdb;
$results = $mdb -> select( 'pp_settings', '*', [ 'ORDER' => [ 'id' => 'ASC' ] ] );
if ( is_array( $results ) ) foreach ( $results as $row )
$settings[$row['param']] = $row['value'];
return $settings;
}
}
?>

View File

@@ -1,306 +1,83 @@
<?php
namespace admin\factory;
/**
* @deprecated Wrapper — używaj \Domain\User\UserRepository przez DI.
*/
class Users
{
public static function user_delete($user_id)
private static function repo(): \Domain\User\UserRepository
{
global $mdb;
return $mdb->delete('pp_users', ['id' => (int)$user_id]);
return new \Domain\User\UserRepository( $mdb );
}
public static function user_details($user_id)
public static function user_delete( $user_id ): bool
{
global $mdb;
return $mdb->get('pp_users', '*', ['id' => (int)$user_id]);
return self::repo()->delete( (int)$user_id );
}
public static function user_privileges($user_id)
public static function user_details( $user_id ): ?array
{
global $mdb;
return $mdb->select('pp_users_privileges', '*', ['id_user' => (int)$user_id]);
return self::repo()->find( (int)$user_id );
}
public static function user_save($user_id, $login, $status, $active_to, $password, $password_re, $admin, $privileges, $twofa_enabled = 0, $twofa_email = '' )
public static function user_privileges( $user_id ): array
{
global $mdb, $lang;
$mdb->delete('pp_users_privileges', ['id_user' => (int) $user_id]);
if (!$user_id)
{
if (strlen($password) < 5)
return $response = ['status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.'];
if ($password != $password_re)
return $response = ['status' => 'error', 'msg' => 'Podane hasła są różne'];
if ($mdb->insert(
'pp_users',
[
'login' => $login,
'status' => $status == 'on' ? 1 : 0,
'active_to' => $active_to == '' ? NULL : $active_to,
'admin' => $admin,
'password' => md5($password),
'twofa_enabled' => $twofa_enabled == 'on' ? 1 : 0,
'twofa_email' => $twofa_email
]
))
$id_user = $mdb->get('pp_users', 'id', ['ORDER' => ['id' => 'DESC']]);
if (is_array($privileges))
{
foreach ($privileges as $pri)
{
$mdb->insert(
'pp_users_privileges',
[
'name' => $pri,
'id_user' => $id_user
]
);
}
}
else
{
$mdb->insert(
'pp_users_privileges',
[
'name' => $privileges,
'id_user' => $id_user
]
);
}
return $response = ['status' => 'ok', 'msg' => 'Użytkownik został zapisany.'];
}
else
{
if ($password and strlen($password) < 5)
return $response = ['status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.'];
if ($password and $password != $password_re)
return $response = ['status' => 'error', 'msg' => 'Podane hasła są różne'];
if ($password)
$mdb->update('pp_users', [
'password' => md5($password)
], [
'id' => (int) $user_id
]);
$mdb->update('pp_users', [
'login' => $login,
'admin' => $admin,
'status' => $status == 'on' ? 1 : 0,
'active_to' => $active_to == '' ? NULL : $active_to,
'error_logged_count' => 0,
'twofa_enabled' => $twofa_enabled == 'on' ? 1 : 0,
'twofa_email' => $twofa_email
], [
'id' => (int) $user_id
]);
if (is_array($privileges))
{
foreach ($privileges as $pri)
{
$mdb->insert('pp_users_privileges', [
'name' => $pri,
'id_user' => $user_id
]);
}
}
else
{
$mdb->insert('pp_users_privileges', [
'name' => $privileges,
'id_user' => $user_id
]);
}
return $response = ['status' => 'ok', 'msg' => 'Uzytkownik został zapisany.'];
}
\S::delete_cache();
return self::repo()->privileges( (int)$user_id );
}
public static function check_login($login, $user_id)
{
global $mdb;
if ($mdb->get('pp_users', 'login', ['AND' => ['login' => $login, 'id[!]' => (int)$user_id]]))
return $response = ['status' => 'error', 'msg' => 'Podany login jest już zajęty.'];
return $response = ['status' => 'ok'];
public static function user_save(
$user_id, $login, $status, $active_to, $password, $password_re,
$admin, $privileges, $twofa_enabled = 0, $twofa_email = ''
): array {
return self::repo()->save(
$user_id, (string)$login, $status, $active_to,
(string)$password, (string)$password_re,
$admin, $privileges, $twofa_enabled, (string)$twofa_email
);
}
public static function logon($login, $password)
public static function check_login( $login, $user_id ): array
{
global $mdb;
if ( self::repo()->isLoginTaken( (string)$login, (int)$user_id ) )
return [ 'status' => 'error', 'msg' => 'Podany login jest już zajęty.' ];
if (!$mdb->get('pp_users', '*', ['login' => $login]))
return 0;
if (!$mdb->get('pp_users', '*', ['AND' => ['login' => $login, 'status' => 1, 'error_logged_count[<]' => 5]]))
return -1;
if ($mdb->get('pp_users', '*', [
'AND' => [
'login' => $login,
'status' => 1,
'password' => md5($password),
'OR' => ['active_to[>=]' => date('Y-m-d'), 'active_to' => null]
]
]))
{
$mdb->update('pp_users', ['last_logged' => date('Y-m-d H:i:s'), 'error_logged_count' => 0], ['login' => $login]);
return 1;
}
else
{
$mdb->update('pp_users', ['last_error_logged' => date('Y-m-d H:i:s'), 'error_logged_count[+]' => 1], ['login' => $login]);
if ($mdb->get('pp_users', 'error_logged_count', ['login' => $login]) >= 5)
{
$mdb->update('pp_users', ['status' => 0], ['login' => $login]);
return -1;
}
}
return 0;
return [ 'status' => 'ok' ];
}
public static function details($login)
public static function logon( $login, $password ): int
{
global $mdb;
return $mdb->get('pp_users', '*', ['login' => $login]);
return self::repo()->logon( (string)$login, (string)$password );
}
public static function check_privileges($name, $user_id)
public static function details( $login ): ?array
{
global $mdb;
if ($user_id == 1)
return true;
else
{
if (!$privilages = \Cache::fetch("check_privileges:$user_id:$name-tmp"))
{
$privilages = $mdb->count('pp_users_privileges', ['AND' => ['name' => $name, 'id_user' => (int)$user_id]]);
\Cache::store("check_privileges:$user_id:$name", $privilages);
}
return $privilages;
}
return self::repo()->findByLogin( (string)$login );
}
static public function get_by_id(int $userId): ?array
public static function check_privileges( $name, $user_id ): bool
{
global $mdb;
return $mdb->get('pp_users', '*', ['id' => $userId]) ?: null;
return self::repo()->hasPrivilege( (string)$name, (int)$user_id );
}
static public function send_twofa_code(int $userId, bool $resend = false): bool
public static function get_by_id( int $userId ): ?array
{
$user = self::get_by_id($userId);
if (!$user)
return false;
if ((int)$user['twofa_enabled'] !== 1)
{
return false;
}
$to = $user['twofa_email'] ?: $user['login'];
if (!filter_var($to, FILTER_VALIDATE_EMAIL))
{
return false;
}
if ($resend && !empty($user['twofa_sent_at']))
{
$last = strtotime($user['twofa_sent_at']);
if ($last && (time() - $last) < 30)
{
return false;
}
}
$code = random_int(100000, 999999);
$hash = password_hash((string)$code, PASSWORD_DEFAULT);
self::update_by_id($userId, [
'twofa_code_hash' => $hash,
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 10 * 60), // 10 minut
'twofa_sent_at' => date('Y-m-d H:i:s'),
'twofa_failed_attempts' => 0,
]);
$subject = 'Twój kod logowania 2FA';
$body = "Twój kod logowania do panelu administratora: {$code}. Kod jest ważny przez 10 minut. Jeśli to nie Ty inicjowałeś logowanie zignoruj tę wiadomość i poinformuj administratora.";
$sent = \S::send_email($to, $subject, $body);
if (!$sent) {
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-type: text/plain; charset=UTF-8\r\n";
$headers .= "From: no-reply@" . ($_SERVER['HTTP_HOST'] ?? 'localhost') . "\r\n";
$encodedSubject = mb_encode_mimeheader($subject, 'UTF-8');
$sent = mail($to, $encodedSubject, $body, $headers);
}
return $sent;
return self::repo()->find( $userId );
}
static public function update_by_id(int $userId, array $data): bool
public static function send_twofa_code( int $userId, bool $resend = false ): bool
{
global $mdb;
return (bool)$mdb->update('pp_users', $data, ['id' => $userId]);
return self::repo()->sendTwofaCode( $userId, $resend );
}
static public function verify_twofa_code(int $userId, string $code): bool
public static function update_by_id( int $userId, array $data ): bool
{
$user = self::get_by_id( $userId );
if (!$user) return false;
return self::repo()->update( $userId, $data );
}
if ((int)$user['twofa_failed_attempts'] >= 5)
{
return false; // zbyt wiele prób
}
// sprawdź ważność
if (empty($user['twofa_expires_at']) || time() > strtotime($user['twofa_expires_at']))
{
// wyczyść po wygaśnięciu
self::update_by_id($userId, [
'twofa_code_hash' => null,
'twofa_expires_at' => null,
]);
return false;
}
$ok = (!empty($user['twofa_code_hash']) && password_verify($code, $user['twofa_code_hash']));
if ($ok)
{
// sukces: czyścimy wszystko
self::update_by_id($userId, [
'twofa_code_hash' => null,
'twofa_expires_at' => null,
'twofa_sent_at' => null,
'twofa_failed_attempts' => 0,
'last_logged' => date('Y-m-d H:i:s'),
]);
return true;
}
// zła próba — inkrementacja
self::update_by_id($userId, [
'twofa_failed_attempts' => (int)$user['twofa_failed_attempts'] + 1,
'last_error_logged' => date('Y-m-d H:i:s'),
]);
return false;
public static function verify_twofa_code( int $userId, string $code ): bool
{
return self::repo()->verifyTwofaCode( $userId, $code );
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace admin\view;
class Releases
{
public static function main_view(): string
{
$tpl = new \Tpl;
$tpl->versions = \admin\factory\Releases::get_versions();
$tpl->licenses = \admin\factory\Releases::get_licenses();
return $tpl->render('releases/main-view');
}
}

View File

@@ -1,46 +1,8 @@
<?php
class Cache
/**
* Wrapper delegujący do \Shared\Cache\CacheHandler.
* Zachowany dla wstecznej kompatybilności — stopniowo zastępować \Cache na \Shared\Cache\CacheHandler.
*/
class Cache extends \Shared\Cache\CacheHandler
{
public static function store( $key, $data, $ttl = 86400 )
{
file_put_contents( self::get_file_name( $key ), gzdeflate( serialize( array( time() + $ttl, $data ) ) ) );
}
private static function get_file_name( $key )
{
$md5 = md5( $key );
$dir = 'temp/' . $md5[0] . '/' . $md5[1] . '/';
if ( !is_dir( $dir ) )
mkdir( $dir , 0755 , true );
return $dir . 's_cache_' . $md5;
}
public static function fetch( $key )
{
$filename = self::get_file_name( $key );
if ( !file_exists( $filename ) || !is_readable( $filename ) )
return false;
$data = gzinflate( file_get_contents( $filename ) );
$data = @unserialize( $data );
if ( !$data )
{
unlink( $filename );
return false;
}
if ( time() > $data[0] )
{
if ( file_exists( $filename ) )
unlink( $filename );
return false;
}
return $data[1];
}
}
?>

View File

@@ -1,91 +1,8 @@
<?php
class Html
/**
* Wrapper delegujący do \Shared\Html\Html.
* Zachowany dla wstecznej kompatybilności — stopniowo zastępować \Html na \Shared\Html\Html.
*/
class Html extends \Shared\Html\Html
{
public static function form_text( array $params = array() )
{
$tpl = new \Tpl;
$tpl -> params = $params;
return $tpl -> render( 'html/form-text' );
}
public static function input_switch( array $params = array() )
{
$tpl = new \Tpl;
$tpl -> params = $params;
return $tpl -> render( 'html/input-switch' );
}
public static function select( array $params = array() )
{
$tpl = new \Tpl;
$tpl -> params = $params;
return $tpl -> render( 'html/select' );
}
public static function textarea( array $params = array() )
{
$defaults = array(
'rows' => 4,
);
$params = array_merge( $defaults, $params );
$tpl = new \Tpl;
$tpl -> params = $params;
return $tpl -> render( 'html/textarea' );
}
public static function input_icon( array $params = array() )
{
$defaults = array(
'type' => 'text',
);
$params = array_merge( $defaults, $params );
$tpl = new \Tpl;
$tpl -> params = $params;
return $tpl -> render( 'html/input-icon' );
}
public static function input( array $params = array() )
{
$defaults = array(
'type' => 'text',
);
$params = array_merge( $defaults, $params );
$tpl = new \Tpl;
$tpl -> params = $params;
return $tpl -> render( 'html/input' );
}
public static function button( array $params = array() )
{
$defaults = array(
'class' => 'btn-sm btn-info',
);
$params = array_merge( $defaults, $params );
$tpl = new \Tpl;
$tpl -> params = $params;
return $tpl -> render( 'html/button' );
}
public static function panel( array $params = array() )
{
$defaults = array(
'title' => 'panel-title',
'class' => 'panel-primary',
'content' => 'panel-content'
);
$params = array_merge( $defaults, $params );
$tpl = new \Tpl;
$tpl -> params = $params;
return $tpl -> render( 'html/panel' );
}
}

View File

@@ -1,312 +1,8 @@
<?php
class ImageManipulator
/**
* Wrapper delegujący do \Shared\Image\ImageManipulator.
* Zachowany dla wstecznej kompatybilności — stopniowo zastępować new ImageManipulator na new \Shared\Image\ImageManipulator.
*/
class ImageManipulator extends \Shared\Image\ImageManipulator
{
/**
* @var int
*/
protected $width;
/**
* @var int
*/
protected $height;
/**
* @var resource
*/
protected $image;
protected $img_src;
/**
* Image manipulator constructor
*
* @param string $file OPTIONAL Path to image file or image data as string
* @return void
*/
public function __construct($file = null)
{
if (null !== $file) {
if (is_file($file)) {
$this -> img_src = $file;
$this->setImageFile($file);
} else {
echo 'a'; exit;
$this->setImageString($file);
}
}
}
/**
* Set image resource from file
*
* @param string $file Path to image file
* @return ImageManipulator for a fluent interface
* @throws InvalidArgumentException
*/
public function setImageFile($file)
{
if (!(is_readable($file) && is_file($file))) {
throw new InvalidArgumentException("Image file $file is not readable");
}
if (is_resource($this->image)) {
imagedestroy($this->image);
}
list ($this->width, $this->height, $type) = getimagesize($file);
switch ($type) {
case IMAGETYPE_GIF :
$this->image = imagecreatefromgif($file);
break;
case IMAGETYPE_JPEG :
$this->image = imagecreatefromjpeg($file);
break;
case IMAGETYPE_PNG :
$this->image = imagecreatefrompng($file);
break;
default :
throw new InvalidArgumentException("Image type $type not supported");
}
return $this;
}
/**
* Set image resource from string data
*
* @param string $data
* @return ImageManipulator for a fluent interface
* @throws RuntimeException
*/
public function setImageString($data)
{
if (is_resource($this->image)) {
imagedestroy($this->image);
}
if (!$this->image = imagecreatefromstring($data)) {
throw new RuntimeException('Cannot create image from data string');
}
$this->width = imagesx($this->image);
$this->height = imagesy($this->image);
return $this;
}
/**
* Resamples the current image
*
* @param int $width New width
* @param int $height New height
* @param bool $constrainProportions Constrain current image proportions when resizing
* @return ImageManipulator for a fluent interface
* @throws RuntimeException
*/
public function resample( $width, $height, $constrainProportions = true )
{
if (!is_resource($this->image)) {
throw new RuntimeException('No image set');
}
if ($constrainProportions) {
if ($this->height >= $this->width) {
$width = round($height / $this->height * $this->width);
} else {
$height = round($width / $this->width * $this->height);
}
}
$temp = imagecreatetruecolor($width, $height);
imagecopyresampled($temp, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height);
if ( function_exists('exif_read_data') )
{
$exif = exif_read_data( $this -> img_src );
if ( $exif && isset($exif['Orientation']) )
{
$orientation = $exif['Orientation'];
if ( $orientation != 1 )
{
$deg = 0;
switch ($orientation)
{
case 3:
$deg = 180;
break;
case 6:
$deg = 270;
break;
case 8:
$deg = 90;
break;
}
if ( $deg )
$temp = imagerotate( $temp, $deg, 0 );
}
}
}
return $this->_replace($temp);
}
/**
* Enlarge canvas
*
* @param int $width Canvas width
* @param int $height Canvas height
* @param array $rgb RGB colour values
* @param int $xpos X-Position of image in new canvas, null for centre
* @param int $ypos Y-Position of image in new canvas, null for centre
* @return ImageManipulator for a fluent interface
* @throws RuntimeException
*/
public function enlargeCanvas($width, $height, array $rgb = array(), $xpos = null, $ypos = null)
{
if (!is_resource($this->image)) {
throw new RuntimeException('No image set');
}
$width = max($width, $this->width);
$height = max($height, $this->height);
$temp = imagecreatetruecolor($width, $height);
if (count($rgb) == 3) {
$bg = imagecolorallocate($temp, $rgb[0], $rgb[1], $rgb[2]);
imagefill($temp, 0, 0, $bg);
}
if (null === $xpos) {
$xpos = round(($width - $this->width) / 2);
}
if (null === $ypos) {
$ypos = round(($height - $this->height) / 2);
}
imagecopy($temp, $this->image, (int) $xpos, (int) $ypos, 0, 0, $this->width, $this->height);
return $this->_replace($temp);
}
/**
* Crop image
*
* @param int|array $x1 Top left x-coordinate of crop box or array of coordinates
* @param int $y1 Top left y-coordinate of crop box
* @param int $x2 Bottom right x-coordinate of crop box
* @param int $y2 Bottom right y-coordinate of crop box
* @return ImageManipulator for a fluent interface
* @throws RuntimeException
*/
public function crop($x1, $y1 = 0, $x2 = 0, $y2 = 0)
{
if (!is_resource($this->image)) {
throw new RuntimeException('No image set');
}
if (is_array($x1) && 4 == count($x1)) {
list($x1, $y1, $x2, $y2) = $x1;
}
$x1 = max($x1, 0);
$y1 = max($y1, 0);
$x2 = min($x2, $this->width);
$y2 = min($y2, $this->height);
$width = $x2 - $x1;
$height = $y2 - $y1;
$temp = imagecreatetruecolor($width, $height);
imagecopy($temp, $this->image, 0, 0, $x1, $y1, $width, $height);
return $this->_replace($temp);
}
/**
* Replace current image resource with a new one
*
* @param resource $res New image resource
* @return ImageManipulator for a fluent interface
* @throws UnexpectedValueException
*/
protected function _replace($res)
{
if (!is_resource($res)) {
throw new UnexpectedValueException('Invalid resource');
}
if (is_resource($this->image)) {
imagedestroy($this->image);
}
$this->image = $res;
$this->width = imagesx($res);
$this->height = imagesy($res);
return $this;
}
/**
* Save current image to file
*
* @param string $fileName
* @return void
* @throws RuntimeException
*/
public function save($fileName, $type = IMAGETYPE_JPEG)
{
$dir = dirname($fileName);
if (!is_dir($dir)) {
if (!mkdir($dir, 0755, true)) {
throw new RuntimeException('Error creating directory ' . $dir);
}
}
try {
switch ($type) {
case IMAGETYPE_GIF :
if (!imagegif($this->image, $fileName)) {
throw new RuntimeException;
}
break;
case IMAGETYPE_PNG :
if (!imagepng($this->image, $fileName)) {
throw new RuntimeException;
}
break;
case IMAGETYPE_JPEG :
default :
if (!imagejpeg($this->image, $fileName, 95)) {
throw new RuntimeException;
}
}
} catch (Exception $ex) {
throw new RuntimeException('Error saving image file to ' . $fileName);
}
}
/**
* Returns the GD image resource
*
* @return resource
*/
public function getResource()
{
return $this->image;
}
/**
* Get current image resource width
*
* @return int
*/
public function getWidth()
{
return $this->width;
}
/**
* Get current image height
*
* @return int
*/
public function getHeight()
{
return $this->height;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,73 +1,8 @@
<?php
class Tpl
/**
* Wrapper delegujący do \Shared\Tpl\Tpl.
* Zachowany dla wstecznej kompatybilności — stopniowo zastępować \Tpl na \Shared\Tpl\Tpl.
*/
class Tpl extends \Shared\Tpl\Tpl
{
protected $dir = 'templates/';
protected $vars = array();
function __construct( $dir = null )
{
if ( $dir !== null )
$this -> dir = $dir;
}
public static function view( $file, $values = '' )
{
$tpl = new \Tpl;
if ( is_array( $values ) ) foreach ( $values as $key => $val )
$tpl -> $key = $val;
return $tpl -> render( $file );
}
public function secureHTML( $val )
{
$out = stripslashes( $val );
$out = str_replace( "'", "&#039;", $out );
$out = str_replace( '"', "&#34;", $out );
$out = str_replace( "<", "&lt;", $out );
$out = str_replace( ">", "&gt;", $out );
return $out;
}
public function render( $file )
{
if ( file_exists( 'templates_user/' . $file . '.php' ) )
{
ob_start();
include 'templates_user/' . $file . '.php';
$out = ob_get_contents();
ob_end_clean();
return $out;
}
else if ( file_exists( 'templates/' . $file . '.php' ) )
{
ob_start();
include 'templates/' . $file . '.php';
$out = ob_get_contents();
ob_end_clean();
return $out;
}
else if ( file_exists( $file . '.php' ) )
{
ob_start();
include $file . '.php';
$out = ob_get_contents();
ob_end_clean();
return $out;
}
else
return '<div class="alert alert-danger" role="alert">Nie znaleziono pliku widoku: <b>' . $this -> dir . $file . '.php</b>';
}
public function __set( $name, $value )
{
$this -> vars[ $name ] = $value;
}
public function __get( $name )
{
return $this -> vars[ $name ];
}
}

View File

@@ -1,58 +1,34 @@
<?php
namespace front\factory;
/**
* @deprecated Wrapper — używaj \Domain\Languages\LanguagesRepository przez DI.
*/
class Languages
{
public static function default_domain()
private static function repo(): \Domain\Languages\LanguagesRepository
{
global $mdb;
$results = $mdb -> query( 'SELECT domain FROM pp_langs WHERE status = 1 AND domain IS NOT NULL AND main_domain = 1' ) -> fetchAll();
return $default_domain = $results[0][0];
return new \Domain\Languages\LanguagesRepository( $mdb );
}
public static function default_language( $domain = '' )
public static function default_domain(): ?string
{
global $mdb;
if ( !$default_language = \Cache::fetch( "default_language:$domain" ) )
{
if ( $domain )
$results = $mdb -> query( 'SELECT id FROM pp_langs WHERE status = 1 AND domain = \'' . $domain . '\' ORDER BY start DESC, o ASC LIMIT 1' ) -> fetchAll();
if ( !$domain or !\front\factory\Languages::default_domain() )
$results = $mdb -> query( 'SELECT id FROM pp_langs WHERE status = 1 AND domain IS NULL ORDER BY start DESC, o ASC LIMIT 1' ) -> fetchAll();
$default_language = $results[0][0];
\Cache::store( "default_language:$domain", $default_language );
}
return $default_language;
return self::repo()->defaultDomain();
}
public static function active_languages()
public static function default_language( $domain = '' ): ?string
{
global $mdb;
if ( !$active_languages = \Cache::fetch( 'active_languages' ) )
{
$active_languages = $mdb -> select( 'pp_langs', [ 'id', 'name', 'domain' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
\Cache::store( 'active_languages', $active_languages );
}
return $active_languages;
return self::repo()->defaultLanguage( (string)$domain );
}
public static function lang_translations( $language = 'pl' )
public static function active_languages(): array
{
global $mdb;
if ( !$translations = \Cache::fetch( "lang_translations:$language" ) )
{
$translations[ '0' ] = $language;
$results = $mdb -> select( 'pp_langs_translations', [ 'text', $language ] );
if ( is_array( $results ) ) foreach ( $results as $row )
$translations[ $row['text'] ] = $row[ $language ];
\Cache::store( "lang_translations:$language", $translations );
}
return $translations;
return self::repo()->activeLanguages();
}
public static function lang_translations( $language = 'pl' ): array
{
return self::repo()->langTranslations( (string)$language );
}
}

View File

@@ -1,27 +1,24 @@
<?php
namespace front\factory;
/**
* @deprecated Wrapper — używaj \Domain\Settings\SettingsRepository przez DI.
*/
class Settings
{
public static function settings_details()
private static function repo(): \Domain\Settings\SettingsRepository
{
global $mdb;
if ( !$settings = \Cache::fetch( 'settings_details' ) )
{
$results = $mdb -> select( 'pp_settings', '*' );
if ( is_array( $results ) ) foreach ( $results as $row )
$settings[ $row['param'] ] = $row['value'];
\Cache::store( 'settings_details', $settings );
}
return $settings;
return new \Domain\Settings\SettingsRepository( $mdb );
}
public static function visit_counter()
public static function settings_details(): array
{
global $mdb;
return $mdb -> get( 'pp_settings', 'value', [ 'param' => 'visits'] );
return self::repo()->allSettings();
}
public static function visit_counter(): ?string
{
return self::repo()->visitCounter();
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

10
composer.json Normal file
View File

@@ -0,0 +1,10 @@
{
"require-dev": {
"phpunit/phpunit": "^10.5"
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}

1688
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
<?php
$database['host'] = 'localhost';
$database['host_remote'] = 'host117523.hostido.net.pl';
$database['user'] = 'host117523_cmspro';
$database['password'] = '3sJADeqKHLqHddfavDeR';
$database['name'] = 'host117523_cmspro';

View File

@@ -4,10 +4,14 @@ function __autoload_my_classes( $classname )
{
$q = explode( '\\' , $classname );
$c = array_pop( $q );
// 1. Legacy: class.ClassName.php
$f = 'autoload/' . implode( '/' , $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) )
require_once( $f );
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = 'autoload/' . implode( '/' , $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );
date_default_timezone_set( 'Europe/Warsaw' );

178
docs/FORM_EDIT_SYSTEM.md Normal file
View File

@@ -0,0 +1,178 @@
# Form Edit System - Dokumentacja użycia
## Architektura
```
┌─────────────────────────────────────────────────────────────┐
│ Controller │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ edit() │ │ save() │ │
│ │ - buduje VM │ │ - walidacja │ │
│ │ - renderuje │ │ - zapis │ │
│ └────────┬────────┘ └─────────────────┘ │
└───────────┼─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ FormEditViewModel │
│ - title, formId, data, fields, tabs, actions │
│ - validationErrors, persist, languages │
└───────────┬─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ components/form-edit.php (szablon) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ FormFieldRenderer - renderuje każde pole │ │
│ │ ├─ input, select, textarea, switch │ │
│ │ ├─ date, datetime, editor, image │ │
│ │ └─ lang_section (zagnieżdżone pola) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Pliki systemu
| Plik | Opis |
|------|------|
| `autoload/admin/ViewModels/Forms/FormFieldType.php` | Stale typow pol |
| `autoload/admin/ViewModels/Forms/FormField.php` | Factory methods per typ |
| `autoload/admin/ViewModels/Forms/FormTab.php` | Zakladki |
| `autoload/admin/ViewModels/Forms/FormAction.php` | Akcje (zapisz, anuluj) |
| `autoload/admin/ViewModels/Forms/FormEditViewModel.php` | ViewModel formularza |
| `autoload/admin/Support/Forms/FormValidator.php` | Walidacja pol |
| `autoload/admin/Support/Forms/FormRequestHandler.php` | Obsluga POST + persist |
| `autoload/admin/Support/Forms/FormFieldRenderer.php` | Renderowanie HTML |
| `admin/templates/components/form-edit.php` | Uniwersalny szablon |
## Przykład użycia w kontrolerze
```php
use admin\ViewModels\Forms\FormEditViewModel;
use admin\ViewModels\Forms\FormField;
use admin\ViewModels\Forms\FormTab;
use admin\ViewModels\Forms\FormAction;
use admin\Support\Forms\FormRequestHandler;
class BannerController
{
public function edit(): string
{
$banner = $this->repository->find($id);
$languages = \admin\factory\Languages::languages_list();
$viewModel = new FormEditViewModel(
formId: 'banner-edit',
title: 'Edycja banera',
data: $banner,
tabs: [
new FormTab('settings', 'Ustawienia', 'fa-wrench'),
new FormTab('content', 'Zawartość', 'fa-file'),
],
fields: [
// Zakładka Ustawienia
FormField::text('name', [
'label' => 'Nazwa',
'tab' => 'settings',
'required' => true,
]),
FormField::switch('status', [
'label' => 'Aktywny',
'tab' => 'settings',
]),
FormField::date('date_start', [
'label' => 'Data rozpoczęcia',
'tab' => 'settings',
]),
// Sekcja językowa w zakładce Zawartość
FormField::langSection('translations', 'content', [
FormField::image('src', ['label' => 'Obraz']),
FormField::text('url', ['label' => 'Url']),
FormField::editor('text', ['label' => 'Treść']),
]),
],
actions: [
FormAction::save('/admin/banners/save', '/admin/banners'),
FormAction::cancel('/admin/banners'),
],
languages: $languages,
persist: true,
);
return \Tpl::view('components/form-edit', ['form' => $viewModel]);
}
public function save(): void
{
$formHandler = new FormRequestHandler();
$viewModel = $this->buildFormViewModel(); // jak w edit()
$result = $formHandler->handleSubmit($viewModel, $_POST);
if (!$result['success']) {
// Błędy walidacji - zapisane automatycznie do sesji
echo json_encode(['success' => false, 'errors' => $result['errors']]);
exit;
}
// Sukces - persist wyczyszczony automatycznie
$this->repository->save($result['data']);
echo json_encode(['success' => true]);
exit;
}
}
```
## Dostępne typy pól
| Typ | Metoda | Opcje |
|-----|--------|-------|
| `text` | `FormField::text(name, ['label' => '...', 'required' => true])` | placeholder, help |
| `number` | `FormField::number(name, [...])` | - |
| `email` | `FormField::email(name, [...])` | walidacja formatu |
| `password` | `FormField::password(name, [...])` | - |
| `date` | `FormField::date(name, [...])` | datetimepicker |
| `datetime` | `FormField::datetime(name, [...])` | datetimepicker z czasem |
| `switch` | `FormField::switch(name, [...])` | checked (bool) |
| `select` | `FormField::select(name, ['options' => [...]])` | options: [key => label] |
| `textarea` | `FormField::textarea(name, ['rows' => 4])` | rows |
| `editor` | `FormField::editor(name, ['toolbar' => 'MyTool'])` | CKEditor |
| `image` | `FormField::image(name, ['filemanager' => true])` | filemanager URL |
| `file` | `FormField::file(name, [...])` | filemanager |
| `hidden` | `FormField::hidden(name, value)` | - |
| `color` | `FormField::color(name, ['label' => '...'])` | HTML5 color picker + text input |
| `lang_section` | `FormField::langSection(name, 'tab', [fields])` | pola per język |
## Walidacja
Walidacja jest automatyczna na podstawie właściwości pól:
- `required` - pole wymagane
- `type` = `email` - walidacja formatu e-mail
- `type` = `number` - walidacja liczby
- `type` = `date` - walidacja formatu YYYY-MM-DD
Dla sekcji językowych walidacja jest powtarzana dla każdego aktywnego języka.
## Persist (zapamiętywanie danych)
Gdy `persist = true`:
1. Przy błędzie walidacji dane są zapisywane w `$_SESSION['form_persist'][$formId]`
2. Formularz automatycznie przywraca dane z sesji przy ponownym wyświetleniu
3. Po udanym zapisie sesja jest czyszczona automatycznie przez `FormRequestHandler`
## Przerabianie istniejących formularzy
1. **Kontroler** - zamień `view\Xxx::edit()` na `FormEditViewModel`
2. **Repository** - dostosuj `save()` do formatu z `FormRequestHandler` (lub dodaj wsparcie dla obu formatów)
3. **Szablon** - usuń stary szablon lub zostaw jako fallback
4. **Testy** - zaktualizuj testy jeśli zmienił się format danych
## Aktualizacja 2026-02-15 (ver. 0.275)
- Modul `ShopCategory` zostal zmigrowany do warstwy Domain + DI, ale formularz kategorii nadal korzysta z legacy `gridEdit`.
- W ramach migracji wydzielono skrypty UI do osobnych partiali `*-custom-script.php` (lista, browse, edycja, produkty), co upraszcza dalsze przepiecie formularza na `components/form-edit`.
- Po migracji `ShopCategory` kolejnym kandydatem do pelnej migracji formularza na Form Edit System pozostaje modul `Order` (zgodnie z `REFACTORING_PLAN.md`).
---
*Dokument aktualizowany: 2026-02-15*

24
docs/MEMORY.md Normal file
View File

@@ -0,0 +1,24 @@
# Pamięć projektu cnsPRO
Notatki i wnioski zebrane podczas pracy z kodem. Aktualizowane na bieżąco.
---
## Serwer produkcyjny
- PHP < 8.0 — unikać `match`, named arguments, union types, `str_contains()` itp.
- Zamiast `match` używać operatorów trójargumentowych (ternary) lub `if/else`
## Redis cache — konwencje
- TTL domyślnie 86400 (24h)
- Klucze produktów: `shop\product:{id}:{lang}:{permutation_hash}`
- Wzorzec czyszczenia: `CacheHandler::deletePattern("shop\\product:{$id}:*")`
- Dane w cache są serializowane — wymagają `unserialize()` po `get()`
## Aktualizacje klienckie
- Pliki `*.md` NIGDY nie trafiają do ZIP aktualizacji
- `updates/changelog.php` to plik serwisowy repozytorium, nie runtime klienta
- Główny `.htaccess` wdrażany osobno, poza ZIP aktualizacji
- W archiwum ZIP NIE powinno być folderu z nazwą wersji — struktura zaczyna się od katalogów projektu

120
docs/PROJECT_STRUCTURE.md Normal file
View File

@@ -0,0 +1,120 @@
# Struktura projektu cmsPRO
## Punkty wejścia
| Plik | Opis |
|------|------|
| `index.php` | Router frontendu |
| `admin/index.php` | Router panelu admina |
| `ajax.php` | AJAX frontend |
| `admin/ajax.php` | AJAX admin |
| `api.php` | Publiczne API |
| `cron.php` | Zadania cykliczne (newsletter) |
| `download.php` | Chronione pobieranie plików |
Każdy punkt wejścia ładuje dwa autoloadery (PSR-4 + legacy):
```php
spl_autoload_register(function($class) { /* PSR-4: src/ → autoload/ */ });
spl_autoload_register(function($class) { /* legacy: class.{Name}.php */ });
```
---
## Wzorzec architektoniczny — Static Factory (MVCish)
```
autoload/{admin|front}/
├── controls/class.{Module}.php ← obsługa requestów
├── factory/class.{Module}.php ← logika biznesowa + DB
└── view/class.{Module}.php ← generowanie HTML
```
Przestrzenie nazw: `\admin\controls`, `\admin\factory`, `\admin\view`,
`\front\controls`, `\front\factory`, `\front\view`
---
## Refaktoryzacja DDD — stan aktualny
Projekt migruje stopniowo do architektury DDD. Stare klasy stają się
cienkimi wrapperami delegującymi do nowych klas w `Shared\` i `Domain\`.
### Faza 0 ✓ — Autoloader PSR-4
Dodany do wszystkich 6 punktów wejścia. Mapowanie: namespace → `autoload/`.
### Faza 1 ✓ — Shared utilities (`autoload/Shared/`)
```
autoload/Shared/
├── Cache/CacheHandler.php ← \Shared\Cache\CacheHandler
├── Email/ ← \Shared\Email\*
├── Helpers/Helpers.php ← \Shared\Helpers\Helpers
├── Html/Html.php ← \Shared\Html\Html
├── Image/ImageManipulator.php ← \Shared\Image\ImageManipulator
└── Tpl/Tpl.php ← \Shared\Tpl\Tpl
```
Stare klasy (`class.S.php`, `class.Cache.php`, itd.) są teraz cienkimi
wrapperami — zachowana pełna kompatybilność wsteczna.
### Faza 2 (w toku) — Domain Repositories (`autoload/Domain/`)
```
autoload/Domain/
├── Languages/LanguagesRepository.php ← \Domain\Languages\LanguagesRepository ✓
├── Settings/SettingsRepository.php ← \Domain\Settings\SettingsRepository ✓
└── User/UserRepository.php ← \Domain\User\UserRepository ✓
```
Następne: `Domain\Pages`, `Domain\Layouts`, `Domain\Articles`, ...
---
## Katalogi
| Katalog | Zawartość |
|---------|-----------|
| `autoload/` | Klasy PHP (modele, kontrolery, fabryki, widoki, Shared, Domain) |
| `admin/templates/` | Szablony panelu admina (17 modułów) |
| `templates/` | Szablony frontendu (systemowe, tylko do odczytu) |
| `templates_user/` | Szablony frontendu (nadpisywalne przez użytkownika) |
| `layout/` | SCSS → CSS (style.scss → style.css) |
| `upload/` | Pliki użytkownika (article_images/, article_files/, filemanager/) |
| `libraries/` | Zewnętrzne biblioteki (Medoo, CKEditor, Bootstrap, jQuery…) |
| `plugins/` | Hooki (special-actions.php, -middle.php, -end.php) |
| `migrations/` | Pliki SQL per wersja (np. `0.304.sql`) |
| `updates/` | Paczki ZIP aktualizacji |
| `temp/` | Cache plikowy (gzip, 24h, generowany automatycznie) |
| `docs/` | Dokumentacja techniczna |
---
## Kluczowe klasy
| Klasa | Opis |
|-------|------|
| `\Shared\Helpers\Helpers` (`class.S.php`) | Megautylita: sesja, cookie, email, SEO, detekcja botów |
| `\Shared\Tpl\Tpl` (`class.Tpl.php`) | Silnik szablonów |
| `\Shared\Cache\CacheHandler` (`class.Cache.php`) | Cache plikowy |
| `\Shared\Html\Html` (`class.Html.php`) | Builder komponentów formularzy |
| `\Shared\Image\ImageManipulator` (`class.Image.php`) | Manipulacja obrazami + WebP |
| `class.Article.php` | Model artykułu (ArrayAccess, lazy multilang) |
---
## Baza danych
Prefiks tabel: `pp_`. ORM: Medoo (globalny `$mdb`). Konfiguracja: `config.php`.
Główne tabele: `pp_users`, `pp_articles`, `pp_articles_langs`, `pp_pages`,
`pp_pages_langs`, `pp_languages`, `pp_settings`, `pp_newsletter`,
`pp_newsletter_users`, `pp_tags`, `pp_banners`, `pp_layouts`, `pp_backups`.
---
## System wielojęzyczny
- Sesja: `$_SESSION['current-lang']`
- Tabela: `pp_languages`
- Składnia w treści: `[LANG:klucz]`
- Cache tłumaczeń: `$_SESSION['lang-{lang_id}']`

66
docs/TESTING.md Normal file
View File

@@ -0,0 +1,66 @@
# Testowanie cmsPRO
## Szybki start
```bash
# Instalacja PHPUnit (jednorazowo)
composer install
# Uruchomienie testów
./vendor/bin/phpunit
# Konkretny plik
./vendor/bin/phpunit tests/Unit/Domain/Settings/SettingsRepositoryTest.php
# Konkretny test
./vendor/bin/phpunit --filter testAllSettingsReturnsMappedArray
```
## Aktualny stan
```text
Testy jednostkowe dla Domain\ (Faza 2 DDD)
```
## Konfiguracja
- **PHPUnit 10** via `composer`
- **Bootstrap:** `tests/bootstrap.php`
- **Config:** `phpunit.xml`
## Struktura testów
```
tests/
├── bootstrap.php ← autoloader + stuby (CacheHandler, S)
└── Unit/
└── Domain/
├── Languages/LanguagesRepositoryTest.php
├── Settings/SettingsRepositoryTest.php
└── User/UserRepositoryTest.php
```
## Stuby (bootstrap.php)
- `\Shared\Cache\CacheHandler` — in-memory stub z `fetch()`/`store()`/`delete()`/`reset()`
- `\S` — stub z `delete_cache()`, `htacces()`, `get_domain()`, `send_email()`
- `medoo` — mockowany przez PHPUnit (`$this->createMock(\medoo::class)`)
## Dodawanie nowych testów
1. Plik w `tests/Unit/Domain/<Modul>/<Klasa>Test.php`.
2. Rozszerz `PHPUnit\Framework\TestCase`.
3. Nazwy metod zaczynaj od `test`.
4. Wzorzec AAA: Arrange, Act, Assert.
## Mockowanie Medoo
```php
$db = $this->createMock(\medoo::class);
$db->method('get')->willReturn(['id' => 1]);
$repo = new SettingsRepository($db);
$value = $repo->visitCounter();
$this->assertSame('1', $value);
```

View File

@@ -0,0 +1,73 @@
# Instrukcja tworzenia aktualizacji shopPRO
## Nowy sposób (od v0.301) — automatyczny build script
### Wymagania
- Git z tagami wersji (np. `v0.299`, `v0.300`)
- PowerShell
### Workflow
```
1. Pracuj normalnie: commit, push, commit, push...
2. Gdy wersja gotowa:
→ git tag v0.XXX
→ ./build-update.ps1 -ToTag v0.XXX -ChangelogEntry "NEW - opis"
3. Upload plików z updates/0.XX/ na serwer aktualizacji
```
### Użycie build-update.ps1
```powershell
# Podgląd zmian (bez tworzenia plików)
./build-update.ps1 -ToTag v0.301 -DryRun
# Budowanie paczki (auto-detect poprzedniego tagu)
./build-update.ps1 -ToTag v0.301 -ChangelogEntry "NEW - opis zmiany"
# Z jawnym tagiem źródłowym
./build-update.ps1 -FromTag v0.300 -ToTag v0.301 -ChangelogEntry "NEW - opis"
```
### Co robi skrypt automatycznie
1. `git diff --name-status` między tagami → listy dodanych/zmodyfikowanych/usuniętych plików
2. Filtrowanie przez `.updateignore` (pliki deweloperskie, konfiguracyjne itp.)
3. Kopiowanie plików do temp, tworzenie ZIP
4. SHA256 checksum ZIP-a
5. Generowanie `ver_X.XXX_manifest.json`
6. Generowanie legacy `_sql.txt` i `_files.txt` (okres przejściowy)
7. Aktualizacja `versions.php` i `changelog.php`
8. Cleanup
### Pliki wynikowe
- `updates/0.XX/ver_X.XXX.zip` — paczka z plikami
- `updates/0.XX/ver_X.XXX_manifest.json` — manifest z checksumem, listą zmian, SQL
- `updates/0.XX/ver_X.XXX_sql.txt` — legacy SQL (okres przejściowy)
- `updates/0.XX/ver_X.XXX_files.txt` — legacy lista plików do usunięcia (okres przejściowy)
### Migracje SQL
Pliki SQL umieszczaj w `migrations/{version}.sql` (np. `migrations/0.301.sql`).
Build script automatycznie je wczyta i umieści w manifeście + legacy `_sql.txt`.
### Format manifestu
```json
{
"version": "0.301",
"date": "2026-02-22",
"checksum_zip": "sha256:abc123...",
"files": {
"modified": ["autoload/Domain/Order/OrderRepository.php"],
"added": ["autoload/Domain/Order/NewHelper.php"],
"deleted": ["autoload/shop/OldClass.php"]
},
"directories_deleted": [],
"sql": ["ALTER TABLE pp_x ADD COLUMN y INT DEFAULT 0"],
"changelog": "NEW - opis zmiany"
}
```
### .updateignore
Plik w katalogu głównym projektu, wzorce plików wykluczonych z paczek (jak `.gitignore`).
### INFO
pamiętaj że push czasem zwraca błąd autoryzacji, wtedy spróbuj ponownie

View File

@@ -0,0 +1,113 @@
# Design: Dwukanałowy system aktualizacji + zarządzanie licencjami
**Data:** 2026-02-28
**Status:** Zatwierdzony
## Kontekst
cmsPRO jest używany przez wielu klientów. Celem jest wprowadzenie dwustopniowej dystrybucji aktualizacji:
- Kanał **beta** — tylko wybrane testowe instalacje (12 strony dewelopera)
- Kanał **stable** — wszyscy pozostali klienci
Zarządzanie odbywa się z panelu admina na `cmspro.project-dc.pl` (ten sam serwer i DB co serwer aktualizacji).
**Kluczowe ograniczenie:** nowy moduł admina oraz nowe tabele DB nigdy nie trafiają do paczek aktualizacyjnych wysyłanych klientom.
---
## Sekcja 1: Baza danych
Dwie nowe tabele tworzone **ręcznie tylko na serwerze dewelopera**. Nie mogą pojawić się w żadnym SQL migracyjnym dla klientów.
```sql
CREATE TABLE pp_update_licenses (
id INT AUTO_INCREMENT PRIMARY KEY,
`key` VARCHAR(64) NOT NULL UNIQUE,
domain VARCHAR(255) NOT NULL,
valid_to_date DATE NULL,
valid_to_version VARCHAR(10) NULL,
beta TINYINT(1) NOT NULL DEFAULT 0,
note TEXT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE pp_update_versions (
id INT AUTO_INCREMENT PRIMARY KEY,
version VARCHAR(10) NOT NULL UNIQUE,
channel ENUM('beta','stable') NOT NULL DEFAULT 'beta',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
promoted_at DATETIME NULL
);
```
Dane z `updates/versions.php` (tablica `$license`) migrujemy jednorazowo do `pp_update_licenses`.
---
## Sekcja 2: Plik `updates/versions.php`
Plik zostaje w tym samym miejscu, zmienia się tylko logika (hardkodowane tablice zastępowane odczytem z DB).
### Nowy przepływ:
1. `require_once '../config.php'` → dostęp do `$mdb` (Medoo)
2. Skan filesystem — lista istniejących ZIPów (jak dotychczas)
3. **Auto-discovery:** każdy ZIP nieobecny w `pp_update_versions``INSERT IGNORE` z `channel='beta'`. Wrzucenie nowego ZIPa na serwer automatycznie rejestruje go jako beta — bez zmian w `build-update.ps1`.
4. Walidacja klucza z `pp_update_licenses` (zamiast `$license[...]`)
5. Filtrowanie wersji według kanału:
- klient z `beta=1` → wersje gdzie `channel IN ('beta','stable')`
- klient z `beta=0` → tylko `channel='stable'`
6. Dalsze filtrowanie po `valid_to_date` i `valid_to_version` (bez zmian)
7. Wypisanie listy wersji (jak dotychczas)
---
## Sekcja 3: Nowy moduł admina `Releases`
Nowa nazwa `Releases` (odróżnienie od istniejącego modułu `Update` używanego przez klientów do aktualizacji własnej instalacji).
### Zakładka "Wersje"
- Tabela z `pp_update_versions` + status istnienia ZIPa na dysku
- Kolumny: wersja, kanał, data dodania, data promocji
- Akcje: **Promuj do stable** / **Cofnij do beta**
### Zakładka "Licencje"
- Tabela z `pp_update_licenses`
- Kolumny: domena, klucz (skrócony hash), ważna do daty, ważna do wersji, beta (toggle), notatka
- Pełny CRUD: dodaj / edytuj / usuń
- Szybki toggle flagi `beta` bezpośrednio z listy
### Pliki modułu
Wszystkie wykluczone z paczek ZIP dla klientów:
```
autoload/admin/controls/class.Releases.php
autoload/admin/factory/class.Releases.php
autoload/admin/view/class.Releases.php
admin/templates/releases/main-view.php
admin/templates/releases/licenses-view.php
```
---
## Sekcja 4: Wykluczenie z aktualizacji klientów
### Pliki PHP
W `build-update.ps1` — dodanie powyższych ścieżek do listy wykluczeń.
### Tabele DB
`pp_update_licenses` i `pp_update_versions` **nigdy** nie pojawiają się w:
- plikach `_sql.txt` dołączanych do paczek
- sekcji `sql` w plikach `_manifest.json`
Tabele tworzone jednorazowo ręcznie na serwerze dewelopera.
---
## Migracja danych
Jednorazowy skrypt PHP (uruchamiany ręcznie na serwerze) przenosi dane z `$license[...]` z `versions.php` do `pp_update_licenses`. Po migracji tablica `$license` w `versions.php` jest usuwana.

View File

@@ -0,0 +1,766 @@
# Releases Module Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Wdrożyć dwukanałowy system aktualizacji (beta/stable) z zarządzaniem licencjami w panelu admina.
**Architecture:** Dwie nowe tabele MySQL (`pp_update_licenses`, `pp_update_versions`) dostępne tylko na serwerze dewelopera. `updates/versions.php` czyta z DB przez Medoo. Nowy moduł `Releases` w panelu admina zarządza wersjami i licencjami. Całość wykluczona z paczek klientów przez `.updateignore`.
**Tech Stack:** PHP 8.x, Medoo 1.x (`$mdb` global), jQuery UI (dialog), Bootstrap 3 (klasy CSS), `\Tpl` (admin templates z `admin/templates/`), `\S::get()` (POST→GET params).
---
### Task 1: Dodaj wykluczenia do `.updateignore`
**Files:**
- Modify: `.updateignore`
**Step 1: Dopisz nowe wykluczenia**
Otwórz `.updateignore` i dopisz na końcu:
```
# Moduł zarządzania releaseami (tylko serwer dewelopera)
autoload/admin/controls/class.Releases.php
autoload/admin/factory/class.Releases.php
autoload/admin/view/class.Releases.php
admin/templates/releases/
# Menu dewelopera
templates/additional-menu.php
```
**Step 2: Zweryfikuj manualnie**
Uruchom build w trybie dry-run i sprawdź, że powyższe pliki NIE pojawiają się na liście:
```powershell
./build-update.ps1 -ToTag v9.999 -DryRun
```
**Step 3: Commit**
```bash
git add .updateignore
git commit -m "chore: wyklucz modul Releases i menu dewelopera z paczek klientow"
```
---
### Task 2: Utwórz tabele DB (jednorazowo na serwerze)
**Files:**
- Create: `_db_releases_setup.sql` (uruchom raz w phpMyAdmin, nie commituj)
**Step 1: Utwórz plik SQL**
```sql
CREATE TABLE pp_update_licenses (
id INT AUTO_INCREMENT PRIMARY KEY,
`key` VARCHAR(64) NOT NULL UNIQUE,
domain VARCHAR(255) NOT NULL,
valid_to_date DATE NULL,
valid_to_version VARCHAR(10) NULL,
beta TINYINT(1) NOT NULL DEFAULT 0,
note TEXT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE pp_update_versions (
id INT AUTO_INCREMENT PRIMARY KEY,
version VARCHAR(10) NOT NULL UNIQUE,
channel ENUM('beta','stable') NOT NULL DEFAULT 'beta',
created_at DATETIME NULL,
promoted_at DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
**Step 2: Uruchom w phpMyAdmin na serwerze `cmspro.project-dc.pl`**
Wklej SQL do phpMyAdmin → Execute. Obie tabele muszą być widoczne.
**Step 3: Nie commituj** — ten plik jest tylko pomocniczy, możesz go usunąć po wykonaniu.
---
### Task 3: Utwórz `admin\factory\Releases`
**Files:**
- Create: `autoload/admin/factory/class.Releases.php`
**Step 1: Napisz klasę**
```php
<?php
namespace admin\factory;
class Releases
{
public static function get_versions(): array
{
global $mdb;
$rows = $mdb->select('pp_update_versions', '*', ['ORDER' => ['version' => 'DESC']]);
if (!$rows) return [];
foreach ($rows as &$row)
$row['zip_exists'] = file_exists('../updates/' . self::zip_dir($row['version']) . '/ver_' . $row['version'] . '.zip');
return $rows;
}
public static function promote(string $version): void
{
global $mdb;
$mdb->update('pp_update_versions',
['channel' => 'stable', 'promoted_at' => date('Y-m-d H:i:s')],
['version' => $version]
);
}
public static function demote(string $version): void
{
global $mdb;
$mdb->update('pp_update_versions',
['channel' => 'beta', 'promoted_at' => null],
['version' => $version]
);
}
public static function get_licenses(): array
{
global $mdb;
return $mdb->select('pp_update_licenses', '*', ['ORDER' => ['domain' => 'ASC']]) ?: [];
}
public static function get_license(int $id): array
{
global $mdb;
return $mdb->get('pp_update_licenses', '*', ['id' => $id]) ?: [];
}
public static function save_license(array $data): void
{
global $mdb;
$row = [
'key' => trim($data['key'] ?? ''),
'domain' => trim($data['domain'] ?? ''),
'valid_to_date' => $data['valid_to_date'] ?: null,
'valid_to_version' => $data['valid_to_version'] ?: null,
'beta' => (int)(bool)($data['beta'] ?? 0),
'note' => trim($data['note'] ?? ''),
];
if (!empty($data['id']))
$mdb->update('pp_update_licenses', $row, ['id' => (int)$data['id']]);
else
$mdb->insert('pp_update_licenses', $row);
}
public static function delete_license(int $id): void
{
global $mdb;
$mdb->delete('pp_update_licenses', ['id' => $id]);
}
public static function toggle_beta(int $id): void
{
global $mdb;
$license = $mdb->get('pp_update_licenses', ['id', 'beta'], ['id' => $id]);
if ($license)
$mdb->update('pp_update_licenses', ['beta' => $license['beta'] ? 0 : 1], ['id' => $id]);
}
private static function zip_dir(string $version): string
{
return substr($version, 0, strlen($version) - (strlen($version) == 5 ? 2 : 1)) . '0';
}
}
```
**Step 2: Weryfikacja składni**
```bash
php -l autoload/admin/factory/class.Releases.php
```
Oczekiwane: `No syntax errors detected`
---
### Task 4: Utwórz `admin\controls\Releases`
**Files:**
- Create: `autoload/admin/controls/class.Releases.php`
**Step 1: Napisz klasę**
```php
<?php
namespace admin\controls;
class Releases
{
public static function main_view(): string
{
return \admin\view\Releases::main_view();
}
public static function promote(): void
{
$version = trim(\S::get('version'));
if ($version)
\admin\factory\Releases::promote($version);
header('Location: /admin/releases/main_view/');
exit;
}
public static function demote(): void
{
$version = trim(\S::get('version'));
if ($version)
\admin\factory\Releases::demote($version);
header('Location: /admin/releases/main_view/');
exit;
}
public static function save_license(): void
{
\admin\factory\Releases::save_license($_POST);
\S::set_message('Licencja została zapisana.');
header('Location: /admin/releases/main_view/');
exit;
}
public static function delete_license(): void
{
$id = (int)\S::get('id');
if ($id)
\admin\factory\Releases::delete_license($id);
header('Location: /admin/releases/main_view/');
exit;
}
public static function toggle_beta(): void
{
$id = (int)\S::get('id');
if ($id)
\admin\factory\Releases::toggle_beta($id);
header('Location: /admin/releases/main_view/');
exit;
}
}
```
**Step 2: Weryfikacja składni**
```bash
php -l autoload/admin/controls/class.Releases.php
```
---
### Task 5: Utwórz `admin\view\Releases`
**Files:**
- Create: `autoload/admin/view/class.Releases.php`
**Step 1: Napisz klasę**
```php
<?php
namespace admin\view;
class Releases
{
public static function main_view(): string
{
$tpl = new \Tpl;
$tpl->versions = \admin\factory\Releases::get_versions();
$tpl->licenses = \admin\factory\Releases::get_licenses();
return $tpl->render('releases/main-view');
}
}
```
**Step 2: Weryfikacja składni**
```bash
php -l autoload/admin/view/class.Releases.php
```
---
### Task 6: Utwórz szablon `admin/templates/releases/main-view.php`
**Files:**
- Create: `admin/templates/releases/main-view.php`
**Step 1: Utwórz katalog i plik**
```php
<?php global $user; ?>
<div class="row">
<div class="col-lg-12">
<!-- TABS NAV -->
<ul class="nav nav-tabs releases-tabs" style="margin-bottom:20px">
<li class="active"><a href="#" data-tab="versions">Wersje</a></li>
<li><a href="#" data-tab="licenses">Licencje</a></li>
</ul>
<!-- TAB: WERSJE -->
<div id="tab-versions">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">Wersje</h3></div>
<div class="panel-body">
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Wersja</th><th>Kanał</th><th>Dodana</th>
<th>Promocja do stable</th><th>ZIP</th><th>Akcje</th>
</tr>
</thead>
<tbody>
<? foreach ($this->versions as $v): ?>
<tr>
<td><strong><?= htmlspecialchars($v['version']) ?></strong></td>
<td>
<? if ($v['channel'] == 'stable'): ?>
<span class="label label-success">stable</span>
<? else: ?>
<span class="label label-warning">beta</span>
<? endif; ?>
</td>
<td><?= $v['created_at'] ? substr($v['created_at'], 0, 10) : '-' ?></td>
<td><?= $v['promoted_at'] ? substr($v['promoted_at'], 0, 10) : '-' ?></td>
<td><?= $v['zip_exists'] ? '<span class="text-success">✓</span>' : '<span class="text-danger">✗</span>' ?></td>
<td>
<? if ($v['channel'] == 'beta'): ?>
<a href="/admin/releases/promote/?version=<?= urlencode($v['version']) ?>"
class="btn btn-xs btn-success"
onclick="return confirm('Promować <?= $v['version'] ?> do stable?')">
Promuj →stable
</a>
<? else: ?>
<a href="/admin/releases/demote/?version=<?= urlencode($v['version']) ?>"
class="btn btn-xs btn-default"
onclick="return confirm('Cofnąć <?= $v['version'] ?> do beta?')">
Cofnij →beta
</a>
<? endif; ?>
</td>
</tr>
<? endforeach; ?>
<? if (!$this->versions): ?>
<tr><td colspan="6" class="text-center text-muted">Brak wersji w bazie. Wersje będą rejestrowane automatycznie przy pierwszym odpytaniu versions.php.</td></tr>
<? endif; ?>
</tbody>
</table>
</div>
</div>
</div><!-- /tab-versions -->
<!-- TAB: LICENCJE -->
<div id="tab-licenses" style="display:none">
<div class="panel panel-default">
<div class="panel-heading" style="display:flex;justify-content:space-between;align-items:center">
<h3 class="panel-title">Licencje</h3>
<button class="btn btn-sm btn-system" id="btn-add-license">+ Dodaj licencję</button>
</div>
<div class="panel-body">
<!-- Formularz dodawania/edycji (ukryty) -->
<div id="license-form-box" style="display:none;border:1px solid #ddd;padding:15px;margin-bottom:20px;background:#f9f9f9">
<h4 id="license-form-title">Nowa licencja</h4>
<form method="post" action="/admin/releases/save_license/">
<input type="hidden" name="id" id="f-id" value="">
<div class="row">
<div class="form-group col-lg-3">
<label>Klucz (hash)</label>
<input type="text" name="key" id="f-key" class="form-control" placeholder="md5/hash lub pusty dla domyślnego">
</div>
<div class="form-group col-lg-3">
<label>Domena</label>
<input type="text" name="domain" id="f-domain" class="form-control" required>
</div>
<div class="form-group col-lg-2">
<label>Ważna do daty</label>
<input type="date" name="valid_to_date" id="f-valid-date" class="form-control">
</div>
<div class="form-group col-lg-2">
<label>Ważna do wersji</label>
<input type="text" name="valid_to_version" id="f-valid-ver" class="form-control" placeholder="np. 1.618">
</div>
<div class="form-group col-lg-1">
<label>Beta</label><br>
<input type="checkbox" name="beta" id="f-beta" value="1">
</div>
<div class="form-group col-lg-2">
<label>Notatka</label>
<input type="text" name="note" id="f-note" class="form-control">
</div>
</div>
<button type="submit" class="btn btn-system btn-sm">Zapisz</button>
<button type="button" class="btn btn-default btn-sm" id="btn-cancel-license">Anuluj</button>
</form>
</div><!-- /license-form-box -->
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Domena</th><th>Klucz</th><th>Do daty</th>
<th>Do wersji</th><th>Beta</th><th>Notatka</th><th>Akcje</th>
</tr>
</thead>
<tbody>
<? foreach ($this->licenses as $lic): ?>
<tr>
<td><?= htmlspecialchars($lic['domain']) ?></td>
<td><code title="<?= htmlspecialchars($lic['key']) ?>">
<?= $lic['key'] === '' ? '<em>(domyślny)</em>' : htmlspecialchars(substr($lic['key'], 0, 8)) . '…' ?>
</code></td>
<td><?= $lic['valid_to_date'] ?: '<em>∞</em>' ?></td>
<td><?= $lic['valid_to_version'] ?: '<em>∞</em>' ?></td>
<td>
<a href="/admin/releases/toggle_beta/?id=<?= $lic['id'] ?>"
class="label <?= $lic['beta'] ? 'label-info' : 'label-default' ?>"
title="Kliknij aby przełączyć">
<?= $lic['beta'] ? 'beta' : 'stable' ?>
</a>
</td>
<td><?= htmlspecialchars($lic['note'] ?? '') ?></td>
<td>
<button class="btn btn-xs btn-default btn-edit-license"
data-id="<?= $lic['id'] ?>"
data-key="<?= htmlspecialchars($lic['key']) ?>"
data-domain="<?= htmlspecialchars($lic['domain']) ?>"
data-valid-date="<?= $lic['valid_to_date'] ?? '' ?>"
data-valid-ver="<?= $lic['valid_to_version'] ?? '' ?>"
data-beta="<?= $lic['beta'] ?>"
data-note="<?= htmlspecialchars($lic['note'] ?? '') ?>">Edytuj</button>
<a href="/admin/releases/delete_license/?id=<?= $lic['id'] ?>"
class="btn btn-xs btn-danger"
onclick="return confirm('Usunąć licencję <?= htmlspecialchars($lic['domain']) ?>?')">Usuń</a>
</td>
</tr>
<? endforeach; ?>
<? if (!$this->licenses): ?>
<tr><td colspan="7" class="text-center text-muted">Brak licencji. Dodaj pierwszą lub uruchom skrypt migracji.</td></tr>
<? endif; ?>
</tbody>
</table>
</div>
</div>
</div><!-- /tab-licenses -->
</div>
</div>
<script>
// Przełączanie tabów
$('.releases-tabs a').on('click', function(e) {
e.preventDefault();
$('.releases-tabs li').removeClass('active');
$(this).parent().addClass('active');
$('[id^="tab-"]').hide();
$('#tab-' + $(this).data('tab')).show();
});
// Formularz: Dodaj nową licencję
$('#btn-add-license').on('click', function() {
$('#license-form-title').text('Nowa licencja');
$('#f-id').val('');
$('#f-key, #f-domain, #f-valid-date, #f-valid-ver, #f-note').val('');
$('#f-beta').prop('checked', false);
$('#license-form-box').slideDown();
});
// Formularz: Edytuj istniejącą licencję
$('.btn-edit-license').on('click', function() {
var d = $(this).data();
$('#license-form-title').text('Edytuj licencję: ' + d.domain);
$('#f-id').val(d.id);
$('#f-key').val(d.key);
$('#f-domain').val(d.domain);
$('#f-valid-date').val(d.validDate);
$('#f-valid-ver').val(d.validVer);
$('#f-beta').prop('checked', d.beta == 1);
$('#f-note').val(d.note);
$('#license-form-box').slideDown();
$('html, body').animate({ scrollTop: $('#license-form-box').offset().top - 20 }, 300);
});
// Anuluj formularz
$('#btn-cancel-license').on('click', function() {
$('#license-form-box').slideUp();
});
</script>
```
**Step 2: Zweryfikuj, że szablon jest poprawny**
Otwórz w przeglądarce: `https://cmspro.project-dc.pl/admin/releases/main_view/`
Oczekiwane: strona ładuje się bez błędów PHP, widoczne dwa taby, tabela wersji pusta lub z danymi.
---
### Task 7: Dodaj pozycję menu dla dewelopera
**Files:**
- Create: `templates/additional-menu.php` (ten plik nie trafia do klientów — dodany do `.updateignore` w Task 1)
**Step 1: Utwórz plik**
```php
<?php
// Menu tylko na serwerze dewelopera — wykluczone z .updateignore
?>
<div class="title">Developer</div>
<ul>
<li>
<a href="/admin/releases/main_view/">
<img src="/admin/css/icons/settings-20-filled.svg">Releases &amp; Licencje
</a>
</li>
</ul>
```
**Step 2: Sprawdź w przeglądarce**
Po odświeżeniu panelu admina powinna pojawić się sekcja "Developer" z linkiem "Releases & Licencje" w menu bocznym.
---
### Task 8: Migracja danych licencji do DB
Licencje z hardkodowanej tablicy `$license` w `updates/versions.php` muszą trafić do `pp_update_licenses` **przed** przejściem na nową wersję `versions.php`.
**Files:**
- Create: `_migrate_licenses.php` (tymczasowy, uruchom raz przez przeglądarkę, potem usuń)
**Step 1: Utwórz skrypt migracji**
```php
<?php
// UWAGA: Uruchom JEDEN raz przez przeglądarkę, potem natychmiast usuń!
// URL: https://cmspro.project-dc.pl/_migrate_licenses.php
require_once 'config.php';
require_once 'libraries/medoo/medoo.php';
$mdb = new medoo([
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
]);
// Wyodrębnij $license z pliku versions.php bez uruchamiania die()
$source = file_get_contents('updates/versions.php');
// Wyciągnij wszystkie wpisy $license['key']['domain'] itd.
preg_match_all('/\$license\[\'([^\']*)\'\]\[\'domain\'\]\s*=\s*\'([^\']*)\';/', $source, $m_domain);
preg_match_all('/\$license\[\'([^\']*)\'\]\[\'valid_to_date\'\]\s*=\s*\'([^\']*)\';/', $source, $m_date);
preg_match_all('/\$license\[\'([^\']*)\'\]\[\'valid_to_version\'\]\s*=\s*\'([^\']*)\';/', $source, $m_ver);
// Zbuduj mapę po kluczu
$dates = array_combine($m_date[1], $m_date[2]);
$vers = array_combine($m_ver[1], $m_ver[2]);
$count = 0;
foreach ($m_domain[1] as $i => $key) {
$domain = $m_domain[2][$i];
$row = [
'key' => $key,
'domain' => $domain,
'valid_to_date' => ($dates[$key] ?? '') ?: null,
'valid_to_version' => ($vers[$key] ?? '') ?: null,
'beta' => 0,
];
// Pomiń jeśli już istnieje
if ($mdb->has('pp_update_licenses', ['key' => $key])) {
echo "SKIP (już istnieje): $domain ($key)<br>";
continue;
}
$mdb->insert('pp_update_licenses', $row);
echo "OK: $domain ($key)<br>";
$count++;
}
echo "<hr><strong>Zmigrowano $count licencji.</strong>";
echo "<br><strong style='color:red'>USUŃ ten plik z serwera!</strong>";
```
**Step 2: Wgraj na serwer i uruchom**
```
https://cmspro.project-dc.pl/_migrate_licenses.php
```
Oczekiwane: lista "OK: domena (klucz)" dla każdej licencji, na końcu podsumowanie.
**Step 3: Usuń skrypt z serwera**
Skasuj `_migrate_licenses.php` — nie commituj go do repozytorium.
**Step 4: Ustaw flagę beta dla swoich testowych stron**
W panelu admina `/admin/releases/main_view/` → zakładka Licencje → przy swoich testowych domenach kliknij "stable" aby przełączyć na "beta".
---
### Task 9: Przebuduj `updates/versions.php`
**Files:**
- Modify: `updates/versions.php`
> **WAŻNE:** Wykonaj ten krok dopiero po Task 8 (migracja danych do DB).
**Step 1: Zastąp całą zawartość pliku**
```php
<?
require_once '../config.php';
require_once '../libraries/medoo/medoo.php';
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );
$current_ver = 1691; // aktualizowane automatycznie przez build-update.ps1
// 1. Skan filesystem — lista istniejących ZIPów
$versions = [];
for ( $i = 1; $i <= $current_ver; $i++ )
{
$dir = substr( number_format( $i / 1000, 3 ), 0, strlen( number_format( $i / 1000, 3 ) ) - 2 ) . '0';
$version_old = number_format( $i / 1000, 2 );
$version_new = number_format( $i / 1000, 3 );
if ( file_exists( '../updates/' . $dir . '/ver_' . $version_old . '.zip' ) )
$versions[] = $version_old;
if ( file_exists( '../updates/' . $dir . '/ver_' . $version_new . '.zip' ) )
$versions[] = $version_new;
}
$versions = array_unique( $versions );
// 2. Walidacja klucza licencji
$license = $mdb->get( 'pp_update_licenses', '*', [ 'key' => ( $_GET['key'] ?? '' ) ] );
if ( !$license )
die();
// 3. Sprawdź ważność daty
if ( $license['valid_to_date'] && $license['valid_to_date'] < date( 'Y-m-d' ) )
die();
// 4. Auto-discovery: rejestruj nowe ZIPy jako beta
$known = array_flip( $mdb->select( 'pp_update_versions', 'version', [] ) ?: [] );
foreach ( $versions as $ver )
{
if ( !isset( $known[$ver] ) )
{
@$mdb->insert( 'pp_update_versions', [
'version' => $ver,
'channel' => 'beta',
'created_at' => date( 'Y-m-d H:i:s' )
] );
$known[$ver] = true;
}
}
// 5. Filtruj wersje wg kanału (beta widzi beta+stable, reszta tylko stable)
$channels = $license['beta'] ? [ 'beta', 'stable' ] : [ 'stable' ];
$allowed = array_flip( $mdb->select( 'pp_update_versions', 'version', [ 'channel' => $channels ] ) ?: [] );
// 6. Wypisz dostępne wersje
$valid_to_version = $license['valid_to_version'];
foreach ( $versions as $ver )
{
if ( !isset( $allowed[$ver] ) )
continue;
if ( $valid_to_version && $ver > $valid_to_version )
continue;
echo $ver . PHP_EOL;
}
```
**Step 2: Weryfikacja składni**
```bash
php -l updates/versions.php
```
**Step 3: Test ręczny**
Otwórz w przeglądarce (podaj klucz jednej z migrowanych licencji):
```
https://cmspro.project-dc.pl/updates/versions.php?key=TWOJ_KLUCZ
```
Oczekiwane:
- Dla klucza z `beta=0`: zwraca TYLKO wersje `channel='stable'` (pusta lista jeśli żadna jeszcze niepromowana)
- Dla klucza z `beta=1`: zwraca wersje `channel='beta'` i `'stable'`
- Dla nieprawidłowego klucza: brak odpowiedzi (die())
**Step 4: Sprawdź panel aktualizacji u klienta**
Na swojej testowej stronie (beta=1) otwórz `/admin/update/main_view/` — powinien widzieć dostępne wersje.
---
### Task 10: Commit końcowy
**Step 1: Sprawdź status**
```bash
git status
git diff updates/versions.php
```
**Step 2: Commit**
```bash
git add \
.updateignore \
autoload/admin/factory/class.Releases.php \
autoload/admin/controls/class.Releases.php \
autoload/admin/view/class.Releases.php \
admin/templates/releases/main-view.php \
templates/additional-menu.php \
updates/versions.php
git commit -m "$(cat <<'EOF'
feat: dwukanalowy system aktualizacji (beta/stable) + zarzadzanie licencjami
- Nowy modul admin\Releases: lista wersji z promocja beta→stable,
CRUD licencji z flaga beta
- versions.php czyta z DB (pp_update_licenses, pp_update_versions)
zamiast hardkodowanej tablicy $license
- Auto-discovery: nowe ZIPy automatycznie rejestrowane jako 'beta'
- Calosc wykluczona z paczek klientow przez .updateignore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)"
```
---
## Weryfikacja end-to-end
Po wdrożeniu sprawdź ręcznie:
1. **Nowy ZIP (beta):** Wrzuć nowy ZIP na serwer → odpytaj `versions.php` kluczem beta → pojawia się nowa wersja → w panelu admina widoczna jako `beta`
2. **Promocja:** Kliknij "Promuj →stable" → odpytaj kluczem `beta=0` → wersja pojawia się na liście
3. **Cofnięcie:** Kliknij "Cofnij →beta" → klient `beta=0` nie widzi wersji
4. **Licencja:** Dodaj nową licencję przez formularz → weryfikuj w phpMyAdmin
5. **Toggle beta:** Kliknij "stable" przy licencji → zmienia się na "beta"
6. **Zabezpieczenie:** Wgraj nową wersję CMS do klienta → sprawdź czy `admin/templates/releases/` i `autoload/admin/*/class.Releases.php` NIE znalazły się w ZIPie (build dry-run)

View File

@@ -4,10 +4,14 @@ function __autoload_my_classes( $classname )
{
$q = explode( '\\', $classname );
$c = array_pop( $q );
$f = 'autoload/' . implode( '/', $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) )
require_once( $f );
// 1. Legacy: class.ClassName.php
$f = 'autoload/' . implode( '/', $q ) . '/class.' . $c . '.php';
if ( file_exists( $f ) ) { require_once( $f ); return; }
// 2. PSR-4: ClassName.php
$f = 'autoload/' . implode( '/', $q ) . '/' . $c . '.php';
if ( file_exists( $f ) ) require_once( $f );
}
spl_autoload_register( '__autoload_my_classes' );
date_default_timezone_set( 'Europe/Warsaw' );

96
log.txt
View File

@@ -1,96 +0,0 @@
Array
(
)
Array
(
[title] => "Udar mózgu: Kluczowe znaczenie wczesnej diagnostyki i interwencji medycznej"
[entry] => Udar mózgu stanowi jedno z najpoważniejszych zagrożeń dla zdrowia, dotykając każdego roku wiele osób w Polsce. Wczesne rozpoznanie jego objawów oraz niezwłoczna interwencja medyczna mogą uratować życie i zminimalizować ryzyko trwałych uszkodzeń neurologicznych.
[text] => <p><span style="font-weight:bold;">Udar mózgu</span> to jedno z najgroźniejszych zagrożeń dla zdrowia człowieka. Każdego roku dotyka on tysiące Polaków, a prawidłowa i szybka reakcja może uratować życie oraz zapobiec trwałym uszkodzeniom neurologicznym. Niestety, wiele osób nie rozpoznaje wczesnych objawów, które wysyła nasz organizm przed udarem. <span style="font-style:italic;">Neurologowie</span> ostrzegają, że pierwsze symptomy mogą być bardzo subtelne, lecz ich zignorowanie niesie poważne konsekwencje.</p>
<p><span style="font-weight:bold;">Czym jest udar mózgu?</span> Udar mózgu to nagłe zaburzenie krążenia krwi w obrębie mózgu. Może on mieć charakter niedokrwienny (najczęstszy, spowodowany zatkaniem naczynia krwionośnego) lub krwotoczny (wynikający z pęknięcia naczynia i krwawienia do mózgu). Obie formy prowadzą do niedotlenienia obszaru mózgu i szybkiego obumierania komórek nerwowych.</p>
<p><span style="font-weight:bold;">Najczęściej występujące czynniki ryzyka udaru:</span></p>
<ul>
<li>nadciśnienie tętnicze</li>
<li>cukrzyca</li>
<li>choroby serca, w szczególności migotanie przedsionków</li>
<li>palenie papierosów</li>
<li>otyłość</li>
<li>niezdrowa dieta i brak aktywności fizycznej</li>
<li>przewlekły stres</li>
<li>nadużywanie alkoholu</li>
<li>podeszły wiek</li>
<li>czynniki genetyczne</li>
</ul>
<p><span style="font-weight:bold;">Organizm ostrzega przed udarem na jakie symptomy uważać?</span> Wiele osób sądzi, że udar pojawia się nagle i bez zapowiedzi. Tymczasem ciało często wysyła sygnały ostrzegawcze. Wczesne rozpoznanie pozwala szybko zareagować i zapobiec powikłaniom.</p>
<p><span style="text-decoration:underline;">Najczęściej występujące symptomy zwiastujące udar:</span></p>
<ol>
<li>
<span style="font-weight:bold;">Nagła, jednostronna słabość lub drętwienie:</span>
<span>Dochodzi do niej najczęściej w obrębie twarzy, ramienia bądź nogi. Uczucie osłabienia lub brak czucia po jednej stronie ciała to klasyczny znak ostrzegawczy. Zdarza się też opadanie kącika ust.</span>
</li>
<li>
<span style="font-weight:bold;">Problemy z mową i rozumieniem:</span>
<span>Chory ma trudności z wypowiadaniem słów, zrozumieniem tego, co się do niego mówi, lub doświadcza bełkotliwej mowy. Może mieć również trudności ze znalezieniem właściwych słów.</span>
</li>
<li>
<span style="font-weight:bold;">Nagłe zaburzenia widzenia:</span>
<span>Pojawia się pogorszenie ostrości wzroku, podwójne widzenie lub częściowa utrata wzroku najczęściej w jednym oku lub po jednej stronie pola widzenia.</span>
</li>
<li>
<span style="font-weight:bold;">Silny, nagły ból głowy:</span>
<span>Zwłaszcza jeśli towarzyszą mu nudności, wymioty, zaburzenia świadomości lub sztywność karku. Takie bóle głowy mogą wskazywać na udar krwotoczny.</span>
</li>
<li>
<span style="font-weight:bold;">Zaburzenia równowagi, zawroty głowy, trudności w chodzeniu:</span>
<span>Osoba może mieć uczucie wirowania, trudności z koordynacją, traci równowagę oraz przewraca się bez wyraźnej przyczyny.</span>
</li>
<li>
<span style="font-weight:bold;">Nagła utrata przytomności lub splątanie:</span>
<span>Osoba staje się zdezorientowana, trudna do nawiązania kontaktu, a nawet może zemdleć.</span>
</li>
</ol>
<p>Nie należy lekceważyć nawet przejściowych objawów, które same ustępują. Mogą one oznaczać tzw. przejściowy atak niedokrwienny (TIA), będący <span style="font-weight:bold;">ostrzegawczym sygnałem</span> przed pełnoobjawowym udarem.</p>
<p><span style="font-weight:bold;">Jak zachować się, gdy pojawią się objawy udaru?</span></p>
<ul>
<li>Niezwłocznie wezwij pogotowie ratunkowe (numer 112 lub 999).</li>
<li>Nie próbuj samodzielnie dojechać do szpitala leczenie musi rozpocząć się jak najszybciej, najlepiej w ambulansie, gdzie podejmowane są pierwsze działania ratujące życie.</li>
<li>Osoba z podejrzeniem udaru powinna być ułożona w bezpiecznej pozycji, nie podawać jej jedzenia ani picia.</li>
<li>Poinformuj ratowników o wszystkich zaobserwowanych objawach i ich czasie wystąpienia.</li>
</ul>
<p><span style="font-weight:bold;">Leczenie i rehabilitacja</span></p>
<p>Najważniejsze jest rozpoczęcie terapii jak najszybciej po wystąpieniu objawów istnieje tzw. „<span style="font-style:italic;">złota godzina</span>”, w której wdrożenie leczenia trombolitycznego (rozpuszczającego zakrzepy) istotnie poprawia rokowanie pacjenta. Rehabilitacja neurologiczna powinna rozpocząć się jak najszybciej, co sprzyja powrotowi sprawności i minimalizuje skutki udaru.</p>
<p><span style="font-weight:bold;">Profilaktyka udaru co możemy zrobić?</span></p>
<ul>
<li>Kontroluj ciśnienie tętnicze krwi i regularnie przyjmuj leki przepisane przez lekarza.</li>
<li>Wyeliminuj czynniki ryzyka: rzuć palenie, unikaj nadmiernego spożycia alkoholu, dbaj o prawidłową masę ciała.</li>
<li>Zadbaj o zbilansowaną dietę bogatą w warzywa, owoce, pełnoziarniste produkty i zdrowe tłuszcze.</li>
<li>Systematycznie uprawiaj aktywność fizyczną.</li>
<li>Regularnie badaj poziom cholesterolu i cukru we krwi.</li>
<li>Lecz choroby przewlekłe takie jak cukrzyca i schorzenia serca.</li>
<li>Unikaj przewlekłego stresu i zadbaj o higienę snu.</li>
</ul>
<p><span style="font-weight:bold;">Podsumowanie</span></p>
<p>Udar mózgu może dotknąć każdego, szczególnie osoby z grup ryzyka. Wczesne rozpoznanie objawów i szybkie udzielenie pomocy daje największą szansę na przeżycie i powrót do sprawności. Nie lekceważ nawet krótkotrwałych symptomów każdy z nich może być ostrzeżeniem wysyłanym przez Twój organizm. Edukacja i profilaktyka to najskuteczniejsze narzędzia w walce ze skutkami udaru.</p>
[page_id] => 41
[action] => add_article
)
Array
(
[main_image] => Array
(
[name] => FLUX.1-dev
[type] => image/jpeg
[tmp_name] => /tmp/phpafhT1D
[error] => 0
[size] => 115301
)
)

14
phpunit.xml Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>autoload/Domain</directory>
<directory>autoload/Shared</directory>
</include>
</source>
</phpunit>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://cmsen.project-dc.pl</loc>
<lastmod>2021-12-15</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://cmsen.project-dc.pl/home</loc>
<lastmod>2021-12-15</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
</urlset>

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://cmspro.project-dc.pl</loc>
<lastmod>2021-12-15</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://cmspro.project-dc.pl/home</loc>
<lastmod>2021-12-15</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://cmspro.project-dc.pl/s-49-test</loc>
<lastmod>2021-12-15</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://cmspro.project-dc.pl/strona-testowa</loc>
<lastmod>2021-12-15</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://cmspro.project-dc.pl/s-42-strona-druga-2</loc>
<lastmod>2021-12-15</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://cmspro.project-dc.pl/s-48-prowadzenie-spraw-przed-prezesem-urzedu-ochrony-danych-osobowych-prezesem-urzedu-konkurencji-i-konsumentow-oraz-krajowa-izba-odwolawcza</loc>
<lastmod>2021-12-15</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://cmspro.project-dc.pl/a-11-test</loc>
<lastmod>2021-12-15</lastmod>
<changefreq>daily</changefreq>
<priority>0.6</priority>
</url>
</urlset>

View File

@@ -1,4 +0,0 @@
]ŽÑ
Â0 Eÿ%_°ÎIÝíoø.QË,tE„±7Õ*Ó<<3C>œ$·e´X`¬5öз½-<2D>c˜÷„±Ç"ºGáJNTSK®L>,è@‰G_ñè%Ó+dÓ
zPä4Ì<x!<21>µ?±
¦+`9•ͪï±^«>sJ~¢ÍÓ;<3B>L—B

View File

@@ -1 +0,0 @@
Kエ2イェホエ2ーホエ2477エ07166qャ<71>ャk

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,4 +0,0 @@
ŤTËŽÓJÝ#ń¬ c;ďžÁ!BH<42>tĹEČŞt—ížŘݦ»3ĆFlŘđ
<EFBFBD>_ąKćż(;ńŤÍ‰HQuęqR}ętŘgÉKÉÂĺ2\®ÖŃzŮţ¸6]łĎ­™źHSÄ
ô/- <0B>ßš0<ůA´v™3<>Njy,U˘[w´bţ¦ÜľőôősÖ=˛ÎhUŘ\”Űű÷şŇ<C59F>ů\+Üʼn6Eçc~ŘŮé9†ČĽť1żŞŞ'Ľ°ĄŃú^#wŹź”]<¤žşä8ÓÖuÔ]{Ăĺ<šNÚŁz˘ĐÝ))µ90?š÷w>†rťJőW

View File

@@ -1 +0,0 @@
K<EFBFBD>2<EFBFBD><EFBFBD>δ2<EFBFBD>δ2477<EFBFBD>07166q<><71><EFBFBD> <0C><> <0C><>k

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1 +0,0 @@
e<EFBFBD><EFBFBD>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,123 @@
<?php
namespace Tests\Unit\Domain\Languages;
use Domain\Languages\LanguagesRepository;
use PHPUnit\Framework\TestCase;
class LanguagesRepositoryTest extends TestCase
{
private function mockDb(): object
{
return $this->createMock(\medoo::class);
}
protected function setUp(): void
{
\Shared\Cache\CacheHandler::reset();
}
// --- languagesList ---
public function testLanguagesListReturnsArray(): void
{
$db = $this->mockDb();
$db->method('select')->willReturn([['id' => 'pl', 'name' => 'Polski']]);
$repo = new LanguagesRepository($db);
$this->assertSame([['id' => 'pl', 'name' => 'Polski']], $repo->languagesList());
}
public function testLanguagesListReturnsEmptyWhenNull(): void
{
$db = $this->mockDb();
$db->method('select')->willReturn(null);
$repo = new LanguagesRepository($db);
$this->assertSame([], $repo->languagesList());
}
// --- languageDetails ---
public function testLanguageDetailsReturnsRowWhenFound(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn(['id' => 'pl', 'name' => 'Polski']);
$repo = new LanguagesRepository($db);
$this->assertSame('pl', $repo->languageDetails('pl')['id']);
}
public function testLanguageDetailsReturnsNullWhenNotFound(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn(null);
$repo = new LanguagesRepository($db);
$this->assertNull($repo->languageDetails('xx'));
}
// --- activeLanguages ---
public function testActiveLanguagesQueriesDbAndCaches(): void
{
$expected = [['id' => 'pl', 'name' => 'Polski', 'domain' => null]];
$db = $this->mockDb();
$db->expects($this->once())->method('select')->willReturn($expected);
$repo = new LanguagesRepository($db);
$this->assertSame($expected, $repo->activeLanguages());
// Drugi odczyt — z cache (mock select nie zostanie wywołany drugi raz)
$this->assertSame($expected, $repo->activeLanguages());
}
public function testActiveLanguagesReturnsEmptyWhenNull(): void
{
$db = $this->mockDb();
$db->method('select')->willReturn(null);
$repo = new LanguagesRepository($db);
$this->assertSame([], $repo->activeLanguages());
}
// --- maxOrder ---
public function testMaxOrderReturnsInteger(): void
{
$db = $this->mockDb();
$db->method('max')->willReturn('5');
$repo = new LanguagesRepository($db);
$this->assertSame(5, $repo->maxOrder());
}
// --- translationDelete ---
public function testTranslationDeleteReturnsTrueOnSuccess(): void
{
$db = $this->mockDb();
$db->method('delete')->willReturn(1);
$repo = new LanguagesRepository($db);
$this->assertTrue($repo->translationDelete(1));
}
public function testTranslationDeleteReturnsFalseOnFailure(): void
{
$db = $this->mockDb();
$db->method('delete')->willReturn(0);
$repo = new LanguagesRepository($db);
$this->assertFalse($repo->translationDelete(1));
}
// --- translationDetails ---
public function testTranslationDetailsReturnsRowOrNull(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn(['id' => 1, 'text' => 'hello']);
$repo = new LanguagesRepository($db);
$this->assertSame(['id' => 1, 'text' => 'hello'], $repo->translationDetails(1));
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Tests\Unit\Domain\Settings;
use Domain\Settings\SettingsRepository;
use PHPUnit\Framework\TestCase;
class SettingsRepositoryTest extends TestCase
{
private function mockDb(): object
{
return $this->createMock(\medoo::class);
}
protected function setUp(): void
{
\Shared\Cache\CacheHandler::reset();
}
// --- allSettings ---
public function testAllSettingsReturnsMappedArray(): void
{
$db = $this->mockDb();
$db->method('select')->willReturn([
['param' => 'site_name', 'value' => 'Test CMS'],
['param' => 'email', 'value' => 'admin@test.pl'],
]);
$repo = new SettingsRepository($db);
$result = $repo->allSettings();
$this->assertSame('Test CMS', $result['site_name']);
$this->assertSame('admin@test.pl', $result['email']);
}
public function testAllSettingsReturnsEmptyArrayWhenDbReturnsNull(): void
{
$db = $this->mockDb();
$db->method('select')->willReturn(null);
$repo = new SettingsRepository($db);
$this->assertSame([], $repo->allSettings());
}
public function testAllSettingsUsesCache(): void
{
$db = $this->mockDb();
$db->expects($this->never())->method('select');
\Shared\Cache\CacheHandler::store('settings_details', ['cached' => '1']);
$repo = new SettingsRepository($db);
$result = $repo->allSettings();
$this->assertSame('1', $result['cached']);
}
// --- update ---
public function testUpdateCallsDbUpdateWhenParamExists(): void
{
$db = $this->mockDb();
$db->method('count')->willReturn(1);
$db->expects($this->once())->method('update')->willReturn(true);
$db->expects($this->never())->method('insert');
$repo = new SettingsRepository($db);
$this->assertTrue($repo->update('site_name', 'Nowa Nazwa'));
}
public function testUpdateCallsDbInsertWhenParamMissing(): void
{
$db = $this->mockDb();
$db->method('count')->willReturn(0);
$db->expects($this->once())->method('insert')->willReturn(true);
$db->expects($this->never())->method('update');
$repo = new SettingsRepository($db);
$repo->update('new_param', 'value');
}
// --- visitCounter ---
public function testVisitCounterReturnsValue(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn('1234');
$repo = new SettingsRepository($db);
$this->assertSame('1234', $repo->visitCounter());
}
public function testVisitCounterReturnsNullWhenEmpty(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn(null);
$repo = new SettingsRepository($db);
$this->assertNull($repo->visitCounter());
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace Tests\Unit\Domain\User;
use Domain\User\UserRepository;
use PHPUnit\Framework\TestCase;
class UserRepositoryTest extends TestCase
{
private function mockDb(): object
{
return $this->createMock(\medoo::class);
}
protected function setUp(): void
{
\Shared\Cache\CacheHandler::reset();
}
// --- find ---
public function testFindReturnsUserArray(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn(['id' => 1, 'login' => 'admin']);
$repo = new UserRepository($db);
$this->assertSame('admin', $repo->find(1)['login']);
}
public function testFindReturnsNullWhenNotFound(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn(null);
$repo = new UserRepository($db);
$this->assertNull($repo->find(99));
}
// --- findByLogin ---
public function testFindByLoginReturnsUser(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn(['id' => 1, 'login' => 'admin']);
$repo = new UserRepository($db);
$this->assertNotNull($repo->findByLogin('admin'));
}
// --- all ---
public function testAllReturnsArray(): void
{
$db = $this->mockDb();
$db->method('select')->willReturn([['id' => 1], ['id' => 2]]);
$repo = new UserRepository($db);
$this->assertCount(2, $repo->all());
}
public function testAllReturnsEmptyArrayWhenNull(): void
{
$db = $this->mockDb();
$db->method('select')->willReturn(null);
$repo = new UserRepository($db);
$this->assertSame([], $repo->all());
}
// --- hasPrivilege ---
public function testHasPrivilegeReturnsTrueForAdminUser(): void
{
$db = $this->mockDb();
$repo = new UserRepository($db);
// userId === 1 zawsze ma uprawnienia, bez zapytania do DB
$db->expects($this->never())->method('count');
$this->assertTrue($repo->hasPrivilege('articles', 1));
}
public function testHasPrivilegeReturnsTrueWhenPrivilegeExists(): void
{
$db = $this->mockDb();
$db->method('count')->willReturn(1);
$repo = new UserRepository($db);
$this->assertTrue($repo->hasPrivilege('articles', 2));
}
public function testHasPrivilegeReturnsFalseWhenPrivilegeMissing(): void
{
$db = $this->mockDb();
$db->method('count')->willReturn(0);
$repo = new UserRepository($db);
$this->assertFalse($repo->hasPrivilege('articles', 2));
}
// --- logon ---
public function testLogonReturnsZeroWhenUserNotFound(): void
{
$db = $this->mockDb();
// Pierwsze get() (sprawdź czy login istnieje) → null
$db->method('get')->willReturn(null);
$repo = new UserRepository($db);
$this->assertSame(0, $repo->logon('unknown', 'pass'));
}
public function testLogonReturnsMinusOneWhenAccountBlocked(): void
{
$db = $this->mockDb();
// Pierwsze get() → użytkownik istnieje, drugie → konto zablokowane (null)
$db->method('get')->willReturnOnConsecutiveCalls(
['id' => 2, 'login' => 'user'],
null
);
$repo = new UserRepository($db);
$this->assertSame(-1, $repo->logon('user', 'pass'));
}
public function testLogonReturnsOneOnSuccess(): void
{
$db = $this->mockDb();
$db->method('get')->willReturnOnConsecutiveCalls(
['id' => 2, 'login' => 'user'], // login istnieje
['id' => 2, 'status' => 1, 'error_logged_count' => 0], // nie zablokowany
['id' => 2] // hasło poprawne
);
$db->method('update')->willReturn(true);
$repo = new UserRepository($db);
$this->assertSame(1, $repo->logon('user', 'pass'));
}
// --- isLoginTaken ---
public function testIsLoginTakenReturnsTrueWhenExists(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn('user');
$repo = new UserRepository($db);
$this->assertTrue($repo->isLoginTaken('user'));
}
public function testIsLoginTakenReturnsFalseWhenFree(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn(null);
$repo = new UserRepository($db);
$this->assertFalse($repo->isLoginTaken('newuser'));
}
// --- verifyTwofaCode ---
public function testVerifyTwofaCodeReturnsFalseWhenUserNotFound(): void
{
$db = $this->mockDb();
$db->method('get')->willReturn(null);
$repo = new UserRepository($db);
$this->assertFalse($repo->verifyTwofaCode(1, '123456'));
}
public function testVerifyTwofaCodeReturnsFalseWhenTooManyFailedAttempts(): void
{
$db = $this->mockDb();
$user = [
'id' => 2,
'twofa_failed_attempts' => 5,
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 600),
'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
];
$db->method('get')->willReturn($user);
$repo = new UserRepository($db);
$this->assertFalse($repo->verifyTwofaCode(2, '123456'));
}
public function testVerifyTwofaCodeReturnsFalseWhenExpired(): void
{
$db = $this->mockDb();
$user = [
'id' => 2,
'twofa_failed_attempts' => 0,
'twofa_expires_at' => date('Y-m-d H:i:s', time() - 1),
'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
];
$db->method('get')->willReturn($user);
$db->method('update')->willReturn(true);
$repo = new UserRepository($db);
$this->assertFalse($repo->verifyTwofaCode(2, '123456'));
}
public function testVerifyTwofaCodeReturnsTrueOnValidCode(): void
{
$code = '123456';
$db = $this->mockDb();
$user = [
'id' => 2,
'twofa_failed_attempts' => 0,
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 600),
'twofa_code_hash' => password_hash($code, PASSWORD_DEFAULT),
];
// find() wywołuje get() dwa razy (raz przez verifyTwofaCode, raz przez update)
$db->method('get')->willReturn($user);
$db->method('update')->willReturn(true);
$repo = new UserRepository($db);
$this->assertTrue($repo->verifyTwofaCode(2, $code));
}
// --- delete ---
public function testDeleteReturnsTrueOnSuccess(): void
{
$db = $this->mockDb();
$db->method('delete')->willReturn(1);
$repo = new UserRepository($db);
$this->assertTrue($repo->delete(2));
}
// --- save — walidacja ---
public function testSaveReturnsErrorWhenPasswordTooShort(): void
{
$db = $this->mockDb();
$db->method('delete')->willReturn(1);
$repo = new UserRepository($db);
$result = $repo->save(0, 'newuser', 'on', '', '123', '123', 0, []);
$this->assertSame('error', $result['status']);
}
public function testSaveReturnsErrorWhenPasswordsMismatch(): void
{
$db = $this->mockDb();
$db->method('delete')->willReturn(1);
$repo = new UserRepository($db);
$result = $repo->save(0, 'newuser', 'on', '', 'password1', 'password2', 0, []);
$this->assertSame('error', $result['status']);
}
}

21
tests/bootstrap.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
// Medoo ORM
require_once __DIR__ . '/../libraries/medoo/medoo.php';
// Stuby — muszą być załadowane PRZED autoloaderem PSR-4,
// żeby nie zostały nadpisane przez prawdziwe klasy
require_once __DIR__ . '/stubs/CacheHandler.php';
require_once __DIR__ . '/stubs/S.php';
// PSR-4 autoloader dla Domain\
// Shared\ jest obsłużona przez stub powyżej — pomijamy w autoloaderze
spl_autoload_register(function (string $class): void {
if (strncmp($class, 'Domain\\', 7) === 0) {
$rel = substr($class, 7);
$file = __DIR__ . '/../autoload/Domain/' . str_replace('\\', '/', $rel) . '.php';
if (file_exists($file)) {
require $file;
}
}
});

View File

@@ -0,0 +1,24 @@
<?php
namespace Shared\Cache;
class CacheHandler
{
private static array $store = [];
public static function reset(): void { self::$store = []; }
public static function fetch(string $key): mixed
{
return self::$store[$key] ?? false;
}
public static function store(string $key, mixed $value, int $ttl = 0): void
{
self::$store[$key] = $value;
}
public static function delete(string $key): void
{
unset(self::$store[$key]);
}
}

8
tests/stubs/S.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
class S
{
public static function delete_cache(): void {}
public static function htacces(): void {}
public static function get_domain(string $domain = ''): ?string { return $domain ?: null; }
public static function send_email(string $to, string $subject, string $body): bool { return true; }
}

BIN
updates/1.60/ver_1.690.zip Normal file

Binary file not shown.

BIN
updates/1.60/ver_1.691.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,4 @@
F: ../backup_20250512_232458.zip
F: ../backup_tmp.json
F: ../sitemap_cmsenproject-dcpl.xml
F: ../sitemap_cmsproproject-dcpl.xml

View File

@@ -0,0 +1,49 @@
{
"changelog": "Refaktoryzacja DDD Faza 0+1: PSR-4 autoloader, Shared (CacheHandler, Helpers, Html, ImageManipulator, Tpl), Domain (LanguagesRepository, SettingsRepository, UserRepository), testy jednostkowe Domain\\, docs/",
"version": "1.691",
"files": {
"added": [
"autoload/Domain/Languages/LanguagesRepository.php",
"autoload/Domain/Settings/SettingsRepository.php",
"autoload/Domain/User/UserRepository.php",
"autoload/Shared/Cache/CacheHandler.php",
"autoload/Shared/Helpers/Helpers.php",
"autoload/Shared/Html/Html.php",
"autoload/Shared/Image/ImageManipulator.php",
"autoload/Shared/Tpl/Tpl.php"
],
"deleted": [
"backup_20250512_232458.zip",
"backup_tmp.json",
"sitemap_cmsenproject-dcpl.xml",
"sitemap_cmsproproject-dcpl.xml"
],
"modified": [
"admin/ajax.php",
"admin/index.php",
"ajax.php",
"api.php",
"autoload/admin/class.Site.php",
"autoload/admin/factory/class.Languages.php",
"autoload/admin/factory/class.Settings.php",
"autoload/admin/factory/class.Users.php",
"autoload/class.Cache.php",
"autoload/class.Html.php",
"autoload/class.Image.php",
"autoload/class.S.php",
"autoload/class.Tpl.php",
"autoload/front/factory/class.Languages.php",
"autoload/front/factory/class.Settings.php",
"cron.php",
"index.php"
]
},
"checksum_zip": "sha256:f53230f36d391828f4e368f3fc3420d8f9430ca507a1d4f57a0988823ac22192",
"sql": [
],
"date": "2026-02-27",
"directories_deleted": [
]
}

View File

@@ -1,454 +1,71 @@
<?
$current_ver = 1690;
require_once '../config.php';
require_once '../libraries/medoo/medoo.php';
for ($i = 1; $i <= $current_ver; $i++)
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );
$current_ver = 1691; // aktualizowane automatycznie przez build-update.ps1
// 1. Skan filesystem — lista istniejących ZIPów
$versions = [];
for ( $i = 1; $i <= $current_ver; $i++ )
{
$dir = substr(number_format($i / 1000, 3), 0, strlen(number_format($i / 1000, 3)) - 2) . '0';
$version_old = number_format($i / 1000, 2);
$version_new = number_format($i / 1000, 3);
$dir = substr( number_format( $i / 1000, 3 ), 0, strlen( number_format( $i / 1000, 3 ) ) - 2 ) . '0';
$version_old = number_format( $i / 1000, 2 );
$version_new = number_format( $i / 1000, 3 );
if (file_exists('../updates/' . $dir . '/ver_' . $version_old . '.zip'))
if ( file_exists( '../updates/' . $dir . '/ver_' . $version_old . '.zip' ) )
$versions[] = $version_old;
if (file_exists('../updates/' . $dir . '/ver_' . $version_new . '.zip'))
if ( file_exists( '../updates/' . $dir . '/ver_' . $version_new . '.zip' ) )
$versions[] = $version_new;
}
$versions = array_unique( $versions );
$license['']['domain'] = 'project-pro.pl';
$license['']['valid_to_date'] = '';
$license['']['valid_to_version'] = '1.519';
$license['cd9c8211255303fbacb4add2129caff9']['domain'] = 'template01';
$license['cd9c8211255303fbacb4add2129caff9']['valid_to_date'] = '';
$license['cd9c8211255303fbacb4add2129caff9']['valid_to_version'] = '';
$license['fc32d79ec43ac1a5bdf2de69b65773f2']['domain'] = 'szablony';
$license['fc32d79ec43ac1a5bdf2de69b65773f2']['valid_to_date'] = '';
$license['fc32d79ec43ac1a5bdf2de69b65773f2']['valid_to_version'] = '';
$license['E79DB4882828CCB767DFEA0AADF185BA']['domain'] = 'project-pro.pl';
$license['E79DB4882828CCB767DFEA0AADF185BA']['valid_to_date'] = '';
$license['E79DB4882828CCB767DFEA0AADF185BA']['valid_to_version'] = '';
$license['644DD48CEA53091AEE926A7EAE7B0653']['domain'] = 'tele-maco.com';
$license['644DD48CEA53091AEE926A7EAE7B0653']['valid_to_date'] = '';
$license['644DD48CEA53091AEE926A7EAE7B0653']['valid_to_version'] = '1.548';
$license['D1137E156E2196846D8FB10B71EB6B49']['domain'] = 'kangoor.pl';
$license['D1137E156E2196846D8FB10B71EB6B49']['valid_to_date'] = '';
$license['D1137E156E2196846D8FB10B71EB6B49']['valid_to_version'] = '1.548';
$license['093BB09C91A9E94D7C5336547748CDAF']['domain'] = 'projektowanieogrodow-kanczugowska.pl';
$license['093BB09C91A9E94D7C5336547748CDAF']['valid_to_date'] = '';
$license['093BB09C91A9E94D7C5336547748CDAF']['valid_to_version'] = '1.548';
$license['DFA5463207E86DD8D525340A7B63FD35']['domain'] = 'globelus.pl';
$license['DFA5463207E86DD8D525340A7B63FD35']['valid_to_date'] = '';
$license['DFA5463207E86DD8D525340A7B63FD35']['valid_to_version'] = '1.618';
$license['35C16BE1111EEB36355E1C5AE43F8373']['domain'] = 'domkidarbeskidu.pl';
$license['35C16BE1111EEB36355E1C5AE43F8373']['valid_to_date'] = '';
$license['35C16BE1111EEB36355E1C5AE43F8373']['valid_to_version'] = '1.548';
$license['E7750D7395576D424ABA08C81504AF65']['domain'] = 'wesolek-ropczyce.pl';
$license['E7750D7395576D424ABA08C81504AF65']['valid_to_date'] = '';
$license['E7750D7395576D424ABA08C81504AF65']['valid_to_version'] = '1.548';
$license['410ed13e1c99b03b0ef7067a96cddd2e']['domain'] = 'nexdiag.com';
$license['410ed13e1c99b03b0ef7067a96cddd2e']['valid_to_date'] = '';
$license['410ed13e1c99b03b0ef7067a96cddd2e']['valid_to_version'] = '';
$license['7448547CE8DB5FB02594D60C828282AF']['domain'] = 'sdprog.com';
$license['7448547CE8DB5FB02594D60C828282AF']['valid_to_date'] = '';
$license['7448547CE8DB5FB02594D60C828282AF']['valid_to_version'] = '1.618';
$license['8A75B8079231AF2C164B87A9945D9DA0']['domain'] = 'olihouse.pl';
$license['8A75B8079231AF2C164B87A9945D9DA0']['valid_to_date'] = '';
$license['8A75B8079231AF2C164B87A9945D9DA0']['valid_to_version'] = '1.618';
$license['D8C47C064A3B117512873DE29346397D']['domain'] = 'zaufany-ksiegowy.com.pl';
$license['D8C47C064A3B117512873DE29346397D']['valid_to_date'] = '';
$license['D8C47C064A3B117512873DE29346397D']['valid_to_version'] = '';
$license['B29764D197531EF8C8D39867034433DB']['domain'] = 'bziuk.pl';
$license['B29764D197531EF8C8D39867034433DB']['valid_to_date'] = '';
$license['B29764D197531EF8C8D39867034433DB']['valid_to_version'] = '1.618';
$license['0FD74F515F6C294BAEC97226C60FF173']['domain'] = 'feb.net.pl';
$license['0FD74F515F6C294BAEC97226C60FF173']['valid_to_date'] = '';
$license['0FD74F515F6C294BAEC97226C60FF173']['valid_to_version'] = '';
$license['9EE3E126BBC6E001F0F0184BDB36ECB3']['domain'] = 'lekkopijani.pl';
$license['9EE3E126BBC6E001F0F0184BDB36ECB3']['valid_to_date'] = '';
$license['9EE3E126BBC6E001F0F0184BDB36ECB3']['valid_to_version'] = '1.618';
$license['B7DCF327FDA377C13E0DDC267A3CAAC2']['domain'] = 'studniazyczen.pl';
$license['B7DCF327FDA377C13E0DDC267A3CAAC2']['valid_to_date'] = '';
$license['B7DCF327FDA377C13E0DDC267A3CAAC2']['valid_to_version'] = '1.607';
$license['387DD19D13F0AB85B20B4D1F16D0C0DF']['domain'] = 'kubak.com.pl';
$license['387DD19D13F0AB85B20B4D1F16D0C0DF']['valid_to_date'] = '';
$license['387DD19D13F0AB85B20B4D1F16D0C0DF']['valid_to_version'] = '1.618';
$license['81e85da9375bf2ad6594a5300f859c71']['domain'] = 'ctpoland.com.pl';
$license['81e85da9375bf2ad6594a5300f859c71']['valid_to_date'] = '';
$license['81e85da9375bf2ad6594a5300f859c71']['valid_to_version'] = '1.618';
$license['677A5C20CC99261A41D30C112A0C3441']['domain'] = 'bmk-ck.pl';
$license['677A5C20CC99261A41D30C112A0C3441']['valid_to_date'] = '';
$license['677A5C20CC99261A41D30C112A0C3441']['valid_to_version'] = '1.618';
$license['7835eb439a571df1ce63c56dd464a90a']['domain'] = 'demo.pro24.com.pl';
$license['7835eb439a571df1ce63c56dd464a90a']['valid_to_date'] = '';
$license['7835eb439a571df1ce63c56dd464a90a']['valid_to_version'] = '1.618';
$license['4e87d09851053a1d35dfb3dd0653504d']['domain'] = 'lipinskipawel.pl';
$license['4e87d09851053a1d35dfb3dd0653504d']['valid_to_date'] = '';
$license['4e87d09851053a1d35dfb3dd0653504d']['valid_to_version'] = '1.618';
$license['0918AB95878AD6EEE2936A77278B5C4E']['domain'] = 'iweda.pl';
$license['0918AB95878AD6EEE2936A77278B5C4E']['valid_to_date'] = '';
$license['0918AB95878AD6EEE2936A77278B5C4E']['valid_to_version'] = '1.618';
$license['7d16b320128c1eafd43637a28272ce5a']['domain'] = 'gpb-group.pl';
$license['7d16b320128c1eafd43637a28272ce5a']['valid_to_date'] = '';
$license['7d16b320128c1eafd43637a28272ce5a']['valid_to_version'] = '1.618';
$license['cd4aee2bce3827f446de7a60874fcc78']['domain'] = 'proxima.waw.pl';
$license['cd4aee2bce3827f446de7a60874fcc78']['valid_to_date'] = '';
$license['cd4aee2bce3827f446de7a60874fcc78']['valid_to_version'] = '';
$license['ca12bdf8718169341e2a2bd60b9b6718']['domain'] = 'kopalniarakszawa.pl';
$license['ca12bdf8718169341e2a2bd60b9b6718']['valid_to_date'] = '';
$license['ca12bdf8718169341e2a2bd60b9b6718']['valid_to_version'] = '1.618';
$license['caeeb6ff0580a30f23dbf5d74c50021f']['domain'] = 'konfiguratorgbp.pl';
$license['caeeb6ff0580a30f23dbf5d74c50021f']['valid_to_date'] = '';
$license['caeeb6ff0580a30f23dbf5d74c50021f']['valid_to_version'] = '1.618';
$license['f775aa50ba64a63a8e4fa6d27a75f7b5']['domain'] = 'agrofurdyna.pl';
$license['f775aa50ba64a63a8e4fa6d27a75f7b5']['valid_to_date'] = '';
$license['f775aa50ba64a63a8e4fa6d27a75f7b5']['valid_to_version'] = '1.618';
$license['118cf28359bb9a4e9892a5fe22f934b7']['domain'] = 'ankiety.pl';
$license['118cf28359bb9a4e9892a5fe22f934b7']['valid_to_date'] = '';
$license['118cf28359bb9a4e9892a5fe22f934b7']['valid_to_version'] = '1.618';
$license['2d9a32bb73802ebf5a5ff03cbad2fee5']['domain'] = 'dendron.net.pl';
$license['2d9a32bb73802ebf5a5ff03cbad2fee5']['valid_to_date'] = '';
$license['2d9a32bb73802ebf5a5ff03cbad2fee5']['valid_to_version'] = '';
$license['e8ddc0cc8a4292fde817506bbceea25e']['domain'] = 'orange.pl';
$license['e8ddc0cc8a4292fde817506bbceea25e']['valid_to_date'] = '';
$license['e8ddc0cc8a4292fde817506bbceea25e']['valid_to_version'] = '1.618';
$license['e7b112bcf7da625993537913205aeb90']['domain'] = 'kobcrane-montaze.pl';
$license['e7b112bcf7da625993537913205aeb90']['valid_to_date'] = '';
$license['e7b112bcf7da625993537913205aeb90']['valid_to_version'] = '';
$license['6582e3ac1215189e745cffa6f14242b7']['domain'] = 'landeo.pl';
$license['6582e3ac1215189e745cffa6f14242b7']['valid_to_date'] = '';
$license['6582e3ac1215189e745cffa6f14242b7']['valid_to_version'] = '1.618';
$license['8df0e9b3455b5de1d7bfbfd4924cf16a']['domain'] = '4est.pl';
$license['8df0e9b3455b5de1d7bfbfd4924cf16a']['valid_to_date'] = '';
$license['8df0e9b3455b5de1d7bfbfd4924cf16a']['valid_to_version'] = '1.618';
$license['bfd57185b0202b23d5a2cae1ddcfbcf2']['domain'] = 'pbawers.pl';
$license['bfd57185b0202b23d5a2cae1ddcfbcf2']['valid_to_date'] = '';
$license['bfd57185b0202b23d5a2cae1ddcfbcf2']['valid_to_version'] = '1.618';
$license['e82f945eeea9647022ab04821a8ca47d']['domain'] = 'uczciwawycena.pl';
$license['e82f945eeea9647022ab04821a8ca47d']['valid_to_date'] = '';
$license['e82f945eeea9647022ab04821a8ca47d']['valid_to_version'] = '1.618';
$license['3936b399e94b91829e1851b2fecb5977']['domain'] = 'm3rent.pl';
$license['3936b399e94b91829e1851b2fecb5977']['valid_to_date'] = '';
$license['3936b399e94b91829e1851b2fecb5977']['valid_to_version'] = '1.618';
$license['cbc2a0890d9974e03206e836e245c311']['domain'] = 'dhtpolska.pl';
$license['cbc2a0890d9974e03206e836e245c311']['valid_to_date'] = '';
$license['cbc2a0890d9974e03206e836e245c311']['valid_to_version'] = '1.618';
$license['aa1177a9715b5663ba4b251082679065']['domain'] = 'recosolar.pl';
$license['aa1177a9715b5663ba4b251082679065']['valid_to_date'] = '';
$license['aa1177a9715b5663ba4b251082679065']['valid_to_version'] = '';
$license['edd8ce704a8c04940d333b0b53266610']['domain'] = 'wonderloft.pl';
$license['edd8ce704a8c04940d333b0b53266610']['valid_to_date'] = '';
$license['edd8ce704a8c04940d333b0b53266610']['valid_to_version'] = '1.618';
$license['9adf305a9439e8f9fd78f7618e652978']['domain'] = 'kancelariaskoczek.pl';
$license['9adf305a9439e8f9fd78f7618e652978']['valid_to_date'] = '';
$license['9adf305a9439e8f9fd78f7618e652978']['valid_to_version'] = '';
$license['72b050effb57632584204046b6a298de']['domain'] = 'imperialogistyka.pl';
$license['72b050effb57632584204046b6a298de']['valid_to_date'] = '';
$license['72b050effb57632584204046b6a298de']['valid_to_version'] = '';
$license['6674cbb6c4d1edc9277b3c924a9a5005']['domain'] = 'dobrodomski.pl';
$license['6674cbb6c4d1edc9277b3c924a9a5005']['valid_to_date'] = '';
$license['6674cbb6c4d1edc9277b3c924a9a5005']['valid_to_version'] = '';
$license['fcb769550dae95039c25439e42b97077']['domain'] = 'mikro-domki.pl';
$license['fcb769550dae95039c25439e42b97077']['valid_to_date'] = '';
$license['fcb769550dae95039c25439e42b97077']['valid_to_version'] = '';
$license['d15c0512b8511c5a2bad7e02d8a74110']['domain'] = 'innovationprzeworsk.net';
$license['d15c0512b8511c5a2bad7e02d8a74110']['valid_to_date'] = '';
$license['d15c0512b8511c5a2bad7e02d8a74110']['valid_to_version'] = '';
$license['9158a44e52e5b24bd84419deebee051a']['domain'] = 'mpmkotly.pl';
$license['9158a44e52e5b24bd84419deebee051a']['valid_to_date'] = '';
$license['9158a44e52e5b24bd84419deebee051a']['valid_to_version'] = '';
$license['0bbc77cf87cde41d4761d57b0c7fe923']['domain'] = 'medologic.com';
$license['0bbc77cf87cde41d4761d57b0c7fe923']['valid_to_date'] = '';
$license['0bbc77cf87cde41d4761d57b0c7fe923']['valid_to_version'] = '';
$license['e428a539d475907921cd526ae88c9f47']['domain'] = 'dzieciusiowo.pl';
$license['e428a539d475907921cd526ae88c9f47']['valid_to_date'] = '';
$license['e428a539d475907921cd526ae88c9f47']['valid_to_version'] = '';
$license['2d0479b8cdb00fdb6a58105bc55e6cfa']['domain'] = 'nataliaroch.pl';
$license['2d0479b8cdb00fdb6a58105bc55e6cfa']['valid_to_date'] = '';
$license['2d0479b8cdb00fdb6a58105bc55e6cfa']['valid_to_version'] = '';
$license['454e89c0922aa91ce79ec4c1a5ac6054']['domain'] = 'softimi.pl';
$license['454e89c0922aa91ce79ec4c1a5ac6054']['valid_to_date'] = '';
$license['454e89c0922aa91ce79ec4c1a5ac6054']['valid_to_version'] = '';
$license['8c85f7f6aa5442e5f42cef1903f25f0b']['domain'] = 'nosmoke.pl';
$license['8c85f7f6aa5442e5f42cef1903f25f0b']['valid_to_date'] = '';
$license['8c85f7f6aa5442e5f42cef1903f25f0b']['valid_to_version'] = '';
$license['e688622c70a439ab650eacd0add35032']['domain'] = 'kudlatypies.pl';
$license['e688622c70a439ab650eacd0add35032']['valid_to_date'] = '';
$license['e688622c70a439ab650eacd0add35032']['valid_to_version'] = '';
$license['99593574ef188006af7f53715431623d']['domain'] = 'magdapara.pl';
$license['99593574ef188006af7f53715431623d']['valid_to_date'] = '';
$license['99593574ef188006af7f53715431623d']['valid_to_version'] = '';
$license['07c9408bae288ce6a7bf2b4189760960']['domain'] = 'verdavivo.pl';
$license['07c9408bae288ce6a7bf2b4189760960']['valid_to_date'] = '';
$license['07c9408bae288ce6a7bf2b4189760960']['valid_to_version'] = '';
$license['1cb534040cb2649a9b15b0becfc693da']['domain'] = 'swiatpaneli.rzeszow.pl';
$license['1cb534040cb2649a9b15b0becfc693da']['valid_to_date'] = '';
$license['1cb534040cb2649a9b15b0becfc693da']['valid_to_version'] = '';
$license['2cfdb0f74dbb7748dd06a67f076a2b70']['domain'] = 'fibnet.com.pl';
$license['2cfdb0f74dbb7748dd06a67f076a2b70']['valid_to_date'] = '';
$license['2cfdb0f74dbb7748dd06a67f076a2b70']['valid_to_version'] = '';
$license['1b1a48cdfc259963b1687d1ac88195e3']['domain'] = 'safarikamper.pl';
$license['1b1a48cdfc259963b1687d1ac88195e3']['valid_to_date'] = '';
$license['1b1a48cdfc259963b1687d1ac88195e3']['valid_to_version'] = '';
$license['8e148b983ff683837783f4ea4ccf2f71']['domain'] = 'rampol.net';
$license['8e148b983ff683837783f4ea4ccf2f71']['valid_to_date'] = '';
$license['8e148b983ff683837783f4ea4ccf2f71']['valid_to_version'] = '';
$license['40976930ccc19f6c0f08c903d68d2f85']['domain'] = 'vidok.pl';
$license['40976930ccc19f6c0f08c903d68d2f85']['valid_to_date'] = '';
$license['40976930ccc19f6c0f08c903d68d2f85']['valid_to_version'] = '';
$license['7f2e9bae197a7ca68b8ffa19fe116d14']['domain'] = 'pubmed.pl';
$license['7f2e9bae197a7ca68b8ffa19fe116d14']['valid_to_date'] = '';
$license['7f2e9bae197a7ca68b8ffa19fe116d14']['valid_to_version'] = '';
$license['a31397f5c0f290533a5418b3a7cd68f7']['domain'] = 'skoczek.pro24.link';
$license['a31397f5c0f290533a5418b3a7cd68f7']['valid_to_date'] = '';
$license['a31397f5c0f290533a5418b3a7cd68f7']['valid_to_version'] = '';
$license['599191bf2871cbf4bbf663a071c33cc1']['domain'] = 'marina-pallatium.eu';
$license['599191bf2871cbf4bbf663a071c33cc1']['valid_to_date'] = '';
$license['599191bf2871cbf4bbf663a071c33cc1']['valid_to_version'] = '';
$license['07117d04a12904e25dfe0b37e52176c7']['domain'] = 'zaufane.pl';
$license['07117d04a12904e25dfe0b37e52176c7']['valid_to_date'] = '';
$license['07117d04a12904e25dfe0b37e52176c7']['valid_to_version'] = '';
$license['3038ab96e469229478bd4d4266484a59']['domain'] = 'conflo.pl';
$license['3038ab96e469229478bd4d4266484a59']['valid_to_date'] = '';
$license['3038ab96e469229478bd4d4266484a59']['valid_to_version'] = '';
$license['25ce54d28231faad3a5854df9651e61f']['domain'] = 'inwestprofil.pl';
$license['25ce54d28231faad3a5854df9651e61f']['valid_to_date'] = '';
$license['25ce54d28231faad3a5854df9651e61f']['valid_to_version'] = '';
$license['79080dc0f7b03b50d74f00ba1f87825b']['domain'] = 'grzesiek.pro24.link';
$license['79080dc0f7b03b50d74f00ba1f87825b']['valid_to_date'] = '';
$license['79080dc0f7b03b50d74f00ba1f87825b']['valid_to_version'] = '';
$license['dec5859d7bd0eeccb8cf7016da184d5a']['domain'] = 'emerch.pl';
$license['dec5859d7bd0eeccb8cf7016da184d5a']['valid_to_date'] = '';
$license['dec5859d7bd0eeccb8cf7016da184d5a']['valid_to_version'] = '';
$license['7b1782067d011cc2d991b18e26f3e82b']['domain'] = 'enology.pl';
$license['7b1782067d011cc2d991b18e26f3e82b']['valid_to_date'] = '';
$license['7b1782067d011cc2d991b18e26f3e82b']['valid_to_version'] = '';
$license['8928ad9031cef96302c5c2f2e7686ed3']['domain'] = 'choinki-lancut.eu';
$license['8928ad9031cef96302c5c2f2e7686ed3']['valid_to_date'] = '';
$license['8928ad9031cef96302c5c2f2e7686ed3']['valid_to_version'] = '';
$license['64c01e72bff323a6dc5984f57074e393']['domain'] = 'choinki-lancut.eu';
$license['64c01e72bff323a6dc5984f57074e393']['valid_to_date'] = '';
$license['64c01e72bff323a6dc5984f57074e393']['valid_to_version'] = '';
$license['5f91caf697dfbb174bb4b7e80f57578e']['domain'] = 'wobistal.pl';
$license['5f91caf697dfbb174bb4b7e80f57578e']['valid_to_date'] = '';
$license['5f91caf697dfbb174bb4b7e80f57578e']['valid_to_version'] = '';
$license['4818078c90ac4e88c687c531c730f398']['domain'] = 'monika.pro24.link';
$license['4818078c90ac4e88c687c531c730f398']['valid_to_date'] = '';
$license['4818078c90ac4e88c687c531c730f398']['valid_to_version'] = '';
$license['fd51610b1f93c8ba96cd83c42c11777a']['domain'] = 'rodzic.eu';
$license['fd51610b1f93c8ba96cd83c42c11777a']['valid_to_date'] = '';
$license['fd51610b1f93c8ba96cd83c42c11777a']['valid_to_version'] = '';
$license['c957408692c36f05e1d12f8197aa54ae']['domain'] = 'emifloor.pl';
$license['c957408692c36f05e1d12f8197aa54ae']['valid_to_date'] = '';
$license['c957408692c36f05e1d12f8197aa54ae']['valid_to_version'] = '';
$license['cacae1bd64fe16d22a17c31941ba58a2']['domain'] = 'inwestor-zastepczy.rzeszow.pl';
$license['cacae1bd64fe16d22a17c31941ba58a2']['valid_to_date'] = '';
$license['cacae1bd64fe16d22a17c31941ba58a2']['valid_to_version'] = '';
$license['e3a8caa70a968f25bf097ad8623727c1']['domain'] = 'klimawent-lancut.pl';
$license['e3a8caa70a968f25bf097ad8623727c1']['valid_to_date'] = '';
$license['e3a8caa70a968f25bf097ad8623727c1']['valid_to_version'] = '';
$license['d41d8cd98f00b204e9800998ecf8427e']['domain'] = 'kurierolkuski.pl';
$license['d41d8cd98f00b204e9800998ecf8427e']['valid_to_date'] = '';
$license['d41d8cd98f00b204e9800998ecf8427e']['valid_to_version'] = '';
$license['a6ab4706022bd3000e48ce33d25d5081']['domain'] = 'pipir.pl';
$license['a6ab4706022bd3000e48ce33d25d5081']['valid_to_date'] = '';
$license['a6ab4706022bd3000e48ce33d25d5081']['valid_to_version'] = '';
$license['ecd142a4b8aab63383d762d4929341c8']['domain'] = 'hat-bud.pl';
$license['ecd142a4b8aab63383d762d4929341c8']['valid_to_date'] = '';
$license['ecd142a4b8aab63383d762d4929341c8']['valid_to_version'] = '';
$license['6badbcb418d51fc3d3b4f3121890c862']['domain'] = 'bip.rops.rzeszow.pl';
$license['6badbcb418d51fc3d3b4f3121890c862']['valid_to_date'] = '';
$license['6badbcb418d51fc3d3b4f3121890c862']['valid_to_version'] = '';
$license['8df2b842d2421be883c17b98b3c62eb8']['domain'] = 'haft.ertim.pl';
$license['8df2b842d2421be883c17b98b3c62eb8']['valid_to_date'] = '';
$license['8df2b842d2421be883c17b98b3c62eb8']['valid_to_version'] = '';
$license['c26efc849339d628d9d8cdc33158a9af']['domain'] = 'roximplast.pl';
$license['c26efc849339d628d9d8cdc33158a9af']['valid_to_date'] = '';
$license['c26efc849339d628d9d8cdc33158a9af']['valid_to_version'] = '';
$license['cf1fd02b4a287e235bc6ab5c2891f14d']['domain'] = 'tomaszpasko.pl';
$license['cf1fd02b4a287e235bc6ab5c2891f14d']['valid_to_date'] = '';
$license['cf1fd02b4a287e235bc6ab5c2891f14d']['valid_to_version'] = '';
$license['deb9fa5ac0c1ead8ba2a4224a4e3bd8f']['domain'] = 'symbiana.com.pl';
$license['deb9fa5ac0c1ead8ba2a4224a4e3bd8f']['valid_to_date'] = '';
$license['deb9fa5ac0c1ead8ba2a4224a4e3bd8f']['valid_to_version'] = '';
$license['549895ba2d09af4b938e41de51814ebb']['domain'] = 'pianfol.pl';
$license['549895ba2d09af4b938e41de51814ebb']['valid_to_date'] = '';
$license['549895ba2d09af4b938e41de51814ebb']['valid_to_version'] = '';
$license['076f0718e06bac1bcff90867eb0accf2']['domain'] = 'zsjasionka.szkola.pl';
$license['076f0718e06bac1bcff90867eb0accf2']['valid_to_date'] = '';
$license['076f0718e06bac1bcff90867eb0accf2']['valid_to_version'] = '';
$license['c377620734fa98b2418797f06326c718']['domain'] = 'de.vidok.com';
$license['c377620734fa98b2418797f06326c718']['valid_to_date'] = '';
$license['c377620734fa98b2418797f06326c718']['valid_to_version'] = '';
$license['2a5d8f9c8cb1423be0c366433252ddd1']['domain'] = 'kingstorage.pl';
$license['2a5d8f9c8cb1423be0c366433252ddd1']['valid_to_date'] = '';
$license['2a5d8f9c8cb1423be0c366433252ddd1']['valid_to_version'] = '';
$license['6d7813ea72268dd3a062e92be33adc59']['domain'] = 'dtf.centrumdruku24.pl';
$license['6d7813ea72268dd3a062e92be33adc59']['valid_to_date'] = '';
$license['6d7813ea72268dd3a062e92be33adc59']['valid_to_version'] = '';
$license['9ee5d052380f7b78a30656948e773705']['domain'] = 'dtf.centrumdruku24.pl';
$license['9ee5d052380f7b78a30656948e773705']['valid_to_date'] = '';
$license['9ee5d052380f7b78a30656948e773705']['valid_to_version'] = '';
$license['5ef6db1855e3d585fbbbb545034bbaf3']['domain'] = 'oknoland.mielec.pl';
$license['5ef6db1855e3d585fbbbb545034bbaf3']['valid_to_date'] = '';
$license['5ef6db1855e3d585fbbbb545034bbaf3']['valid_to_version'] = '';
$license['7fb7c4f62c94d24ff75f7931540ec75a']['domain'] = 'oknoland.mielec.pl';
$license['7fb7c4f62c94d24ff75f7931540ec75a']['valid_to_date'] = '';
$license['7fb7c4f62c94d24ff75f7931540ec75a']['valid_to_version'] = '';
$license['e2b0df429491afda870f30d4bdb313a0']['domain'] = 'przedszkole.pro24.com.pl';
$license['e2b0df429491afda870f30d4bdb313a0']['valid_to_date'] = '';
$license['e2b0df429491afda870f30d4bdb313a0']['valid_to_version'] = '';
$license['fb50f1b15be6a8af1178a2aab9a4dbdf']['domain'] = 'chmielowiec.eu';
$license['fb50f1b15be6a8af1178a2aab9a4dbdf']['valid_to_date'] = '';
$license['fb50f1b15be6a8af1178a2aab9a4dbdf']['valid_to_version'] = '';
$license['333f4c17de255dfec941fcdc60ea273a']['domain'] = 'kampery.brzezovka.pl';
$license['333f4c17de255dfec941fcdc60ea273a']['valid_to_date'] = '';
$license['333f4c17de255dfec941fcdc60ea273a']['valid_to_version'] = '';
$license['2f564150024940551b31a19b32ae9734']['domain'] = 'mogegarden.pl';
$license['2f564150024940551b31a19b32ae9734']['valid_to_date'] = '';
$license['2f564150024940551b31a19b32ae9734']['valid_to_version'] = '';
$license['78c454de6a7c40c55fbcd21440ef75a6']['domain'] = 'mogegarden.pl';
$license['78c454de6a7c40c55fbcd21440ef75a6']['valid_to_date'] = '';
$license['78c454de6a7c40c55fbcd21440ef75a6']['valid_to_version'] = '';
$license['2b353dfc72ad989727296019078a5939']['domain'] = 'manver.pl';
$license['2b353dfc72ad989727296019078a5939']['valid_to_date'] = '';
$license['2b353dfc72ad989727296019078a5939']['valid_to_version'] = '';
$license['7965db1a2cc68663077aa335aaf00d6e']['domain'] = 'dpsbabica.pl';
$license['7965db1a2cc68663077aa335aaf00d6e']['valid_to_date'] = '';
$license['7965db1a2cc68663077aa335aaf00d6e']['valid_to_version'] = '';
$license['1ac91c8663f2e371b9d3acbaeb45f184']['domain'] = 'eng.vidok.com';
$license['1ac91c8663f2e371b9d3acbaeb45f184']['valid_to_date'] = '';
$license['1ac91c8663f2e371b9d3acbaeb45f184']['valid_to_version'] = '';
$license['32967a10e80e258873376c9650206ff1']['domain'] = 'ertim.pl';
$license['32967a10e80e258873376c9650206ff1']['valid_to_date'] = '';
$license['32967a10e80e258873376c9650206ff1']['valid_to_version'] = '';
$license['797d5ede8228fff47151ec2555a71b29']['domain'] = 'patekaparments.pl';
$license['797d5ede8228fff47151ec2555a71b29']['valid_to_date'] = '';
$license['797d5ede8228fff47151ec2555a71b29']['valid_to_version'] = '';
$license['32764e943176bdd2563547046e2745cf']['domain'] = 'se.min-pan.krakow.pl';
$license['32764e943176bdd2563547046e2745cf']['valid_to_date'] = '';
$license['32764e943176bdd2563547046e2745cf']['valid_to_version'] = '';
$update_key = $_GET['key'];
if (!isset($license[$_GET['key']]))
// 2. Walidacja klucza licencji
$license = $mdb->get( 'pp_update_licenses', '*', [ 'key' => ( $_GET['key'] ?? '' ) ] );
if ( !$license )
die();
$valid_to_date = $license[$_GET['key']]['valid_to'];
if ($valid_to && $valid_to < date('Y-m-d'))
// 3. Sprawdź ważność daty
if ( $license['valid_to_date'] && $license['valid_to_date'] < date( 'Y-m-d' ) )
die();
$versions = array_unique($versions);
$valid_to_version = $license[$_GET['key']]['valid_to_version'];
if ($valid_to_version)
// 4. Auto-discovery: rejestruj nowe ZIPy jako beta
$known = array_flip( $mdb->select( 'pp_update_versions', 'version', [] ) ?: [] );
foreach ( $versions as $ver )
{
foreach ($versions as $ver)
if ($ver <= $valid_to_version)
echo $ver . PHP_EOL;
if ( !isset( $known[$ver] ) )
{
@$mdb->insert( 'pp_update_versions', [
'version' => $ver,
'channel' => 'beta',
'created_at' => date( 'Y-m-d H:i:s' )
] );
$known[$ver] = true;
}
}
else
// 5. Filtruj wersje wg kanału (beta widzi beta+stable, reszta tylko stable)
$channels = $license['beta'] ? [ 'beta', 'stable' ] : [ 'stable' ];
$allowed = array_flip( $mdb->select( 'pp_update_versions', 'version', [ 'channel' => $channels ] ) ?: [] );
// 6. Wypisz dostępne wersje
$valid_to_version = $license['valid_to_version'];
foreach ( $versions as $ver )
{
foreach ($versions as $ver)
echo $ver . PHP_EOL;
}
if ( !isset( $allowed[$ver] ) )
continue;
if ( $valid_to_version && $ver > $valid_to_version )
continue;
echo $ver . PHP_EOL;
}