Replace file-based JSON cron queue with DB-backed job queue (pp_cron_jobs, pp_cron_schedules). New Domain\CronJob module: CronJobType (constants), CronJobRepository (CRUD, atomic fetch, retry/backoff), CronJobProcessor (orchestration with handler registration). Priority ordering guarantees apilo_send_order (40) runs before sync tasks (50). Includes cron.php auth protection, race condition fix in fetchNext, API response validation, and DI wiring across all entry points. 41 new tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
451 lines
14 KiB
PHP
451 lines
14 KiB
PHP
<?php
|
|
namespace admin;
|
|
|
|
class App
|
|
{
|
|
const APP_SECRET_KEY = 'c3cb2537d25c0efc9e573d059d79c3b8';
|
|
|
|
/**
|
|
* Mapa nowych kontrolerów: module => fabryka kontrolera (DI)
|
|
*/
|
|
private static $newControllers = [];
|
|
|
|
public static function finalize_admin_login( array $user, string $domain, string $cookie_name, bool $remember = false )
|
|
{
|
|
\Shared\Helpers\Helpers::set_session( 'user', $user );
|
|
\Shared\Helpers\Helpers::delete_session( 'twofa_pending' );
|
|
|
|
if ( $remember ) {
|
|
$payloadArr = [
|
|
'login' => $user['login'],
|
|
'ts' => time()
|
|
];
|
|
|
|
$json = json_encode( $payloadArr, JSON_UNESCAPED_SLASHES );
|
|
$sig = hash_hmac( 'sha256', $json, self::APP_SECRET_KEY );
|
|
$payload = base64_encode( $json . '.' . $sig );
|
|
|
|
setcookie( $cookie_name, $payload, [
|
|
'expires' => time() + ( 86400 * 14 ),
|
|
'path' => '/',
|
|
'domain' => $domain,
|
|
'secure' => true,
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
] );
|
|
}
|
|
}
|
|
|
|
public static function special_actions()
|
|
{
|
|
global $mdb;
|
|
|
|
$sa = \Shared\Helpers\Helpers::get( 's-action' );
|
|
if ( !$sa ) return;
|
|
|
|
$domain = preg_replace( '/^www\./', '', $_SERVER['SERVER_NAME'] );
|
|
$cookie_name = 'admin_remember_' . str_replace( '.', '-', $domain );
|
|
$users = new \Domain\User\UserRepository( $mdb );
|
|
|
|
switch ( $sa )
|
|
{
|
|
case 'user-logon':
|
|
$login = \Shared\Helpers\Helpers::get( 'login' );
|
|
$pass = \Shared\Helpers\Helpers::get( 'password' );
|
|
$result = $users->logon( $login, $pass );
|
|
|
|
if ( $result == 1 )
|
|
{
|
|
$user = $users->details( $login );
|
|
if ( !$user ) {
|
|
\Shared\Helpers\Helpers::alert( 'Błąd logowania.' );
|
|
header( 'Location: /admin/' );
|
|
exit;
|
|
}
|
|
|
|
if ( $user['twofa_enabled'] == 1 )
|
|
{
|
|
\Shared\Helpers\Helpers::set_session( 'twofa_pending', [
|
|
'uid' => (int) $user['id'],
|
|
'login' => $login,
|
|
'remember' => (bool) \Shared\Helpers\Helpers::get( 'remember' ),
|
|
'started' => time(),
|
|
] );
|
|
|
|
if ( !$users->sendTwofaCode( (int) $user['id'] ) )
|
|
{
|
|
\Shared\Helpers\Helpers::alert( 'Nie udało się wysłać kodu 2FA. Spróbuj ponownie.' );
|
|
\Shared\Helpers\Helpers::delete_session( 'twofa_pending' );
|
|
header( 'Location: /admin/' );
|
|
exit;
|
|
}
|
|
|
|
header( 'Location: /admin/user/twofa/' );
|
|
exit;
|
|
}
|
|
|
|
self::finalize_admin_login( $user, $domain, $cookie_name, (bool) \Shared\Helpers\Helpers::get( 'remember' ) );
|
|
header( 'Location: /admin/articles/list/' );
|
|
exit;
|
|
}
|
|
|
|
if ( $result == -1 )
|
|
\Shared\Helpers\Helpers::alert( 'Z powodu 5 nieudanych prób Twoje konto zostało zablokowane.' );
|
|
else
|
|
\Shared\Helpers\Helpers::alert( 'Podane hasło jest nieprawidłowe lub użytkownik nie istnieje.' );
|
|
|
|
header( 'Location: /admin/' );
|
|
exit;
|
|
|
|
case 'user-2fa-verify':
|
|
$pending = \Shared\Helpers\Helpers::get_session( 'twofa_pending' );
|
|
if ( !$pending || empty( $pending['uid'] ) ) {
|
|
\Shared\Helpers\Helpers::alert( 'Sesja 2FA wygasła. Zaloguj się ponownie.' );
|
|
header( 'Location: /admin/' );
|
|
exit;
|
|
}
|
|
|
|
$code = trim( (string) \Shared\Helpers\Helpers::get( 'twofa' ) );
|
|
if ( !preg_match( '/^\d{6}$/', $code ) )
|
|
{
|
|
\Shared\Helpers\Helpers::alert( 'Nieprawidłowy format kodu.' );
|
|
header( 'Location: /admin/user/twofa/' );
|
|
exit;
|
|
}
|
|
|
|
if ( !$users->verifyTwofaCode( (int) $pending['uid'], $code ) )
|
|
{
|
|
\Shared\Helpers\Helpers::alert( 'Błędny lub wygasły kod.' );
|
|
header( 'Location: /admin/user/twofa/' );
|
|
exit;
|
|
}
|
|
|
|
$user = $users->details( $pending['login'] );
|
|
if ( !$user ) {
|
|
\Shared\Helpers\Helpers::delete_session( 'twofa_pending' );
|
|
\Shared\Helpers\Helpers::alert( 'Sesja wygasła. Zaloguj się ponownie.' );
|
|
header( 'Location: /admin/' );
|
|
exit;
|
|
}
|
|
self::finalize_admin_login( $user, $domain, $cookie_name, !empty( $pending['remember'] ) );
|
|
header( 'Location: /admin/articles/list/' );
|
|
exit;
|
|
|
|
case 'user-2fa-resend':
|
|
$pending = \Shared\Helpers\Helpers::get_session( 'twofa_pending' );
|
|
if ( !$pending || empty( $pending['uid'] ) )
|
|
{
|
|
\Shared\Helpers\Helpers::alert( 'Sesja 2FA wygasła. Zaloguj się ponownie.' );
|
|
header( 'Location: /admin/' );
|
|
exit;
|
|
}
|
|
|
|
if ( !$users->sendTwofaCode( (int) $pending['uid'], true ) )
|
|
\Shared\Helpers\Helpers::alert( 'Kod można wysłać ponownie po krótkiej przerwie.' );
|
|
else
|
|
\Shared\Helpers\Helpers::alert( 'Nowy kod został wysłany.' );
|
|
|
|
header( 'Location: /admin/user/twofa/' );
|
|
exit;
|
|
|
|
case 'user-logout':
|
|
setcookie( $cookie_name, '', [
|
|
'expires' => time() - 86400,
|
|
'path' => '/',
|
|
'domain' => $domain,
|
|
'secure' => true,
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
] );
|
|
\Shared\Helpers\Helpers::delete_session( 'twofa_pending' );
|
|
session_destroy();
|
|
header( 'Location: /admin/' );
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Entry point — auth check + layout rendering.
|
|
*/
|
|
public static function render(): string
|
|
{
|
|
global $user;
|
|
|
|
if ( \Shared\Helpers\Helpers::get( 'module' ) === 'user' && \Shared\Helpers\Helpers::get( 'action' ) === 'twofa' ) {
|
|
$controller = self::createController( 'Users' );
|
|
return $controller->twofa();
|
|
}
|
|
|
|
if ( !$user || !$user['admin'] )
|
|
{
|
|
$controller = self::createController( 'Users' );
|
|
return $controller->login_form();
|
|
}
|
|
|
|
$tpl = new \Shared\Tpl\Tpl;
|
|
$tpl->content = self::route();
|
|
return $tpl->render( 'site/main-layout' );
|
|
}
|
|
|
|
/**
|
|
* Routing — buduje nazwę modułu z URL i wywołuje akcję kontrolera.
|
|
*/
|
|
public static function route()
|
|
{
|
|
$_SESSION['admin'] = true;
|
|
|
|
if ( \Shared\Helpers\Helpers::get( 'p' ) )
|
|
\Shared\Helpers\Helpers::set_session( 'p', \Shared\Helpers\Helpers::get( 'p' ) );
|
|
|
|
// Budowanie nazwy modułu: shop_product → ShopProduct
|
|
$moduleName = '';
|
|
$parts = explode( '_', (string) \Shared\Helpers\Helpers::get( 'module' ) );
|
|
foreach ( $parts as $part )
|
|
$moduleName .= ucfirst( $part );
|
|
|
|
$action = \Shared\Helpers\Helpers::get( 'action' );
|
|
|
|
$controller = self::createController( $moduleName );
|
|
if ( $controller && method_exists( $controller, $action ) )
|
|
return $controller->$action();
|
|
|
|
\Shared\Helpers\Helpers::alert( 'Nieprawidłowy adres url.' );
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Tworzy instancję kontrolera z Dependency Injection.
|
|
*/
|
|
private static function createController( string $moduleName )
|
|
{
|
|
$factories = self::getControllerFactories();
|
|
if ( !isset( $factories[$moduleName] ) )
|
|
return null;
|
|
|
|
$factory = $factories[$moduleName];
|
|
return is_callable( $factory ) ? $factory() : null;
|
|
}
|
|
|
|
/**
|
|
* Zwraca mapę fabryk kontrolerów (lazy init).
|
|
*/
|
|
private static function getControllerFactories(): array
|
|
{
|
|
if ( !empty( self::$newControllers ) )
|
|
return self::$newControllers;
|
|
|
|
self::$newControllers = [
|
|
'Dashboard' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\DashboardController(
|
|
new \Domain\Dashboard\DashboardRepository( $mdb ),
|
|
new \Domain\ShopStatus\ShopStatusRepository( $mdb )
|
|
);
|
|
},
|
|
'Articles' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ArticlesController(
|
|
new \Domain\Article\ArticleRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb ),
|
|
new \Domain\Layouts\LayoutsRepository( $mdb ),
|
|
new \Domain\Pages\PagesRepository( $mdb )
|
|
);
|
|
},
|
|
'ArticlesArchive' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ArticlesArchiveController(
|
|
new \Domain\Article\ArticleRepository( $mdb )
|
|
);
|
|
},
|
|
'Banners' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\BannerController(
|
|
new \Domain\Banner\BannerRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'Settings' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\SettingsController(
|
|
new \Domain\Settings\SettingsRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'ProductArchive' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ProductArchiveController(
|
|
new \Domain\Product\ProductRepository( $mdb )
|
|
);
|
|
},
|
|
'Archive' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ProductArchiveController(
|
|
new \Domain\Product\ProductRepository( $mdb )
|
|
);
|
|
},
|
|
'Dictionaries' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\DictionariesController(
|
|
new \Domain\Dictionaries\DictionariesRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'Filemanager' => function() {
|
|
return new \admin\Controllers\FilemanagerController();
|
|
},
|
|
'Users' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\UsersController(
|
|
new \Domain\User\UserRepository( $mdb )
|
|
);
|
|
},
|
|
'Languages' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\LanguagesController(
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'Layouts' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\LayoutsController(
|
|
new \Domain\Layouts\LayoutsRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'Newsletter' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\NewsletterController(
|
|
new \Domain\Newsletter\NewsletterRepository(
|
|
$mdb,
|
|
new \Domain\Settings\SettingsRepository( $mdb )
|
|
),
|
|
new \Domain\Newsletter\NewsletterPreviewRenderer()
|
|
);
|
|
},
|
|
'Scontainers' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ScontainersController(
|
|
new \Domain\Scontainers\ScontainersRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopPromotion' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopPromotionController(
|
|
new \Domain\Promotion\PromotionRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopCoupon' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopCouponController(
|
|
new \Domain\Coupon\CouponRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopAttribute' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopAttributeController(
|
|
new \Domain\Attribute\AttributeRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopPaymentMethod' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopPaymentMethodController(
|
|
new \Domain\PaymentMethod\PaymentMethodRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopTransport' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopTransportController(
|
|
new \Domain\Transport\TransportRepository( $mdb ),
|
|
new \Domain\PaymentMethod\PaymentMethodRepository( $mdb )
|
|
);
|
|
},
|
|
'Pages' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\PagesController(
|
|
new \Domain\Pages\PagesRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb ),
|
|
new \Domain\Layouts\LayoutsRepository( $mdb )
|
|
);
|
|
},
|
|
'Integrations' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\IntegrationsController(
|
|
new \Domain\Integrations\IntegrationsRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopStatuses' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopStatusesController(
|
|
new \Domain\ShopStatus\ShopStatusRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopProductSets' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopProductSetsController(
|
|
new \Domain\ProductSet\ProductSetRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopProducer' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopProducerController(
|
|
new \Domain\Producer\ProducerRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopCategory' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopCategoryController(
|
|
new \Domain\Category\CategoryRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopProduct' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopProductController(
|
|
new \Domain\Product\ProductRepository( $mdb ),
|
|
new \Domain\Integrations\IntegrationsRepository( $mdb ),
|
|
new \Domain\Languages\LanguagesRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopClients' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\ShopClientsController(
|
|
new \Domain\Client\ClientRepository( $mdb )
|
|
);
|
|
},
|
|
'ShopOrder' => function() {
|
|
global $mdb;
|
|
$productRepo = new \Domain\Product\ProductRepository( $mdb );
|
|
return new \admin\Controllers\ShopOrderController(
|
|
new \Domain\Order\OrderAdminService(
|
|
new \Domain\Order\OrderRepository( $mdb ),
|
|
$productRepo,
|
|
new \Domain\Settings\SettingsRepository( $mdb ),
|
|
new \Domain\Transport\TransportRepository( $mdb ),
|
|
new \Domain\CronJob\CronJobRepository( $mdb )
|
|
),
|
|
$productRepo
|
|
);
|
|
},
|
|
'Update' => function() {
|
|
global $mdb;
|
|
return new \admin\Controllers\UpdateController(
|
|
new \Domain\Update\UpdateRepository( $mdb )
|
|
);
|
|
},
|
|
];
|
|
|
|
return self::$newControllers;
|
|
}
|
|
|
|
public static function update()
|
|
{
|
|
global $mdb;
|
|
|
|
$repository = new \Domain\Update\UpdateRepository( $mdb );
|
|
$repository->runPendingMigrations();
|
|
}
|
|
}
|