Files
PHPShop/moneta/class/SettingsWriteGuard.php

110 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
namespace MonetaPhpShop;
/**
* Ìîæíî ëè ñîõðàíèòü íàñòðîéêè ìîäóëÿ èç ýòîãî çàïðîñà.
*
* ßäðî PHPShop ïðîâåðÿåò ïðàâà, òîëüêî åñëè èìÿ äåéñòâèÿ â `actionList[...]` ñîäåðæèò ñóôôèêñ
* `.modules.edit`, à èìÿ ïðèñûëàåò êëèåíò: `actionUpdate` áåç ñóôôèêñà ÿäðî âûçûâàåò ëþáîìó
* âîøåäøåìó â àäìèíêó (`PHPShopGUI::getAction()`, ïðîâåðåíî íà ñòåíäå 7.0). Òîêåíà ïðîòèâ CSRF
* ó ÿäðà íåò, cookie ñåññèè íà ñòåíäå — áåç `SameSite`. Ïîýòîìó ìîäóëü ñàì ïðîâåðÿåò ïðàâî
* «Ìîäóëè: ðåäàêòèðîâàíèå» è òî, ÷òî çàïðîñ îòïðàâëåí ñî ñòðàíèöû ýòîãî æå ìàãàçèíà.
*/
final class SettingsWriteGuard
{
public const DENIED_RIGHTS = 'rights';
public const DENIED_METHOD = 'method';
public const DENIED_ORIGIN = 'origin';
/**
* Ïðè÷èíà îòêàçà èëè null, åñëè ñîõðàíÿòü ìîæíî.
*
* Òîëüêî POST: ÿäðî áåð¸ò `actionList` èç `$_REQUEST`, è áåç ýòîé ïðîâåðêè íàñòðîéêè
* ñîõðàíÿë áû îáû÷íûé ïåðåõîä ïî ññûëêå. Ó GET áðàóçåð íå øë¸ò Origin, à Referer ñî ñòðàíèöû
* òîãî æå ñàéòà ïðîâåðêó èñòî÷íèêà ïðîø¸ë áû — ññûëêà íà âèòðèíå ñáðîñèëà áû íàñòðîéêè.
*
* @param bool $canEditModules ïðàâî «Ìîäóëè: ðåäàêòèðîâàíèå» ó àäìèíèñòðàòîðà
* @param array<string, mixed> $server `$_SERVER`
*/
public static function denialReason(bool $canEditModules, array $server): ?string
{
if (!$canEditModules) {
return self::DENIED_RIGHTS;
}
if (self::header($server, 'REQUEST_METHOD') !== 'POST') {
return self::DENIED_METHOD;
}
return self::isSameOrigin($server) ? null : self::DENIED_ORIGIN;
}
/**
* Origin (áðàóçåð ñòàâèò åãî ñàì, ñòðàíèöà ÷óæîãî ñàéòà ïîäìåíèòü íå ìîæåò), à åñëè åãî íåò —
* Referer, äîëæåí óêàçûâàòü íà òîò æå õîñò, íà êîòîðûé ïðèø¸ë çàïðîñ. Áåç îáîèõ — îòêàç:
* òàê æå ÿäðî íå ïóñêàåò áåç Referer âîññòàíîâëåíèå ïàðîëÿ â àäìèíêó.
*
* Õîñò çàïðîñà — `Host` è, çà îáðàòíûì ïðîêñè, `X-Forwarded-Host`: ïîäñòàâèòü ñâîé çàãîëîâîê
* â ìåæñàéòîâûé çàïðîñ áðàóçåð ñòðàíèöå íå äàñò.
*
* @param array<string, mixed> $server
*/
public static function isSameOrigin(array $server): bool
{
$origin = self::header($server, 'HTTP_ORIGIN');
$source = ($origin !== '') ? $origin : self::header($server, 'HTTP_REFERER');
$sourceHost = self::urlHost($source);
if ($sourceHost === null) {
return false;
}
$requestHosts = explode(',', self::header($server, 'HTTP_X_FORWARDED_HOST'));
$requestHosts[] = self::header($server, 'HTTP_HOST');
foreach ($requestHosts as $requestHost) {
$requestHost = trim($requestHost);
if ($requestHost !== '' && self::normalizeHost($requestHost) === $sourceHost) {
return true;
}
}
return false;
}
/**
* Õîñò è ïîðò èç àáñîëþòíîãî àäðåñà http(s) èëè null. Origin `null` (ïåñî÷íèöà, data:) — null.
*/
private static function urlHost(string $url): ?string
{
$parts = parse_url($url);
if (!is_array($parts) || !isset($parts['scheme'], $parts['host'])) {
return null;
}
if (!in_array(strtolower($parts['scheme']), ['http', 'https'], true)) {
return null;
}
return self::normalizeHost($parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : ''));
}
/**
* Íèæíèé ðåãèñòð, áåç ñòàíäàðòíûõ ïîðòîâ: áðàóçåð íå ïèøåò `:443` â Origin, à `Host` çà
* ïðîêñè èíîãäà ïèøåò.
*/
private static function normalizeHost(string $host): string
{
return (string) preg_replace('/:(80|443)\z/', '', strtolower($host));
}
/**
* @param array<string, mixed> $server
*/
private static function header(array $server, string $name): string
{
$value = $server[$name] ?? '';
return is_string($value) ? trim($value) : '';
}
}