agent foundation
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace MintyPHP\Support;
|
||||
|
||||
// AES-256-GCM encryption/decryption for sensitive stored values (e.g. SSO secrets).
|
||||
// Requires APP_CRYPTO_KEY — see EnvValidator for accepted key formats.
|
||||
class Crypto
|
||||
{
|
||||
private const CIPHER = 'aes-256-gcm';
|
||||
@@ -33,6 +35,8 @@ class Crypto
|
||||
throw new \RuntimeException('Encryption failed');
|
||||
}
|
||||
|
||||
// Outer base64 wraps a JSON object with v (version), iv, auth tag, and ciphertext —
|
||||
// all inner binary values are base64url-encoded to stay JSON-safe.
|
||||
$payload = json_encode([
|
||||
'v' => 1,
|
||||
'iv' => self::base64UrlEncode($iv),
|
||||
@@ -94,6 +98,7 @@ class Crypto
|
||||
return null;
|
||||
}
|
||||
|
||||
// Accepts 64-char hex (32 bytes) or base64-encoded 32 bytes — both yield a 256-bit key.
|
||||
$hex = preg_match('/^[0-9a-f]{64}$/i', $raw) ? hex2bin($raw) : false;
|
||||
if (is_string($hex) && strlen($hex) === 32) {
|
||||
return $hex;
|
||||
@@ -107,6 +112,7 @@ class Crypto
|
||||
return null;
|
||||
}
|
||||
|
||||
// URL-safe base64: replaces +/ with -_ and strips = padding.
|
||||
private static function base64UrlEncode(string $value): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace MintyPHP\Support;
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
// Session-based flash message store. Messages are written before a redirect and consumed on the next page.
|
||||
class Flash
|
||||
{
|
||||
private const SESSION_KEY = 'flash_messages';
|
||||
@@ -19,6 +20,7 @@ class Flash
|
||||
}
|
||||
}
|
||||
|
||||
// If a key is given, any existing message with the same key+scope is replaced — prevents duplicate notices.
|
||||
public static function add(string $type, string $message, ?string $scope = null, ?string $key = null): string
|
||||
{
|
||||
self::ensureSession();
|
||||
@@ -93,6 +95,7 @@ class Flash
|
||||
}
|
||||
}
|
||||
|
||||
// Prevents messages from being cleared on the next render — useful when a redirect chain crosses multiple pages.
|
||||
public static function keep(int $times = 1)
|
||||
{
|
||||
self::ensureSession();
|
||||
|
||||
@@ -9,8 +9,11 @@ use MintyPHP\Service\Access\AuthorizationService;
|
||||
use MintyPHP\Service\Auth\AuthService;
|
||||
use MintyPHP\Service\Tenant\TenantService;
|
||||
|
||||
// Session-based web auth guard. Enforces login and RBAC for page actions.
|
||||
// For API token authentication, see ApiAuth instead.
|
||||
class Guard
|
||||
{
|
||||
// Re-check tenant assignment at most once per minute to pick up permission changes without re-login.
|
||||
private const TENANT_REFRESH_INTERVAL = 60;
|
||||
/** @var (callable(): AuthService)|null */
|
||||
private static $authServiceResolver = null;
|
||||
@@ -56,7 +59,7 @@ class Guard
|
||||
return;
|
||||
}
|
||||
|
||||
// Current tenant was deleted, refresh session data
|
||||
// Current tenant was deleted — reload session so the user is switched to another active tenant or logged out.
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($userId > 0) {
|
||||
self::authService()->loadTenantDataIntoSession($userId);
|
||||
@@ -106,6 +109,7 @@ class Guard
|
||||
...$context,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
// JSON requests get a 403/404 response body; browser requests get a redirect to the forbidden page.
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code($decision->status());
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace MintyPHP\Support\Search;
|
||||
|
||||
class SearchQueryNormalizer
|
||||
{
|
||||
// Prepares a user query for SQL LIKE. Escapes \ and _ (LIKE special chars),
|
||||
// converts * to %, and wraps with % wildcards if not already present.
|
||||
public static function normalizeLikeQuery(string $query): string
|
||||
{
|
||||
$query = trim($query);
|
||||
@@ -25,6 +27,7 @@ class SearchQueryNormalizer
|
||||
return $query;
|
||||
}
|
||||
|
||||
// Strips wildcard chars for relevance scoring — score queries need a clean term, not a LIKE pattern.
|
||||
public static function normalizeScoreQuery(string $query): string
|
||||
{
|
||||
$query = trim($query);
|
||||
|
||||
@@ -28,6 +28,8 @@ class SearchSpecialResourceProvider
|
||||
return $rows;
|
||||
}
|
||||
|
||||
// Two-tier match: first checks against the hotkey service's keyword list (e.g. "shortcut", "keyboard"),
|
||||
// then falls back to substring matching against individual entry labels/key combos.
|
||||
private static function matchesHotkeyQueryWith(string $query, array $entries): bool
|
||||
{
|
||||
$query = trim(mb_strtolower($query));
|
||||
@@ -222,6 +224,7 @@ class SearchSpecialResourceProvider
|
||||
$descriptionParts[] = $snippet;
|
||||
}
|
||||
|
||||
// Map raw doc scores (0–1000+) to coarser search_score buckets for result ranking.
|
||||
$rawScore = (int) $match['score'];
|
||||
$searchScore = match (true) {
|
||||
$rawScore >= 1000 => 400,
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace MintyPHP\Support\Search;
|
||||
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
// Defines all SQL-backed search resources. Each resource declares count/preview/result queries
|
||||
// with a {{tenantFilter}} placeholder that is substituted at query time based on the user's tenant scope.
|
||||
class SearchSqlResourceProvider
|
||||
{
|
||||
public static function tenantScopeFilters(): array
|
||||
@@ -25,6 +27,7 @@ class SearchSqlResourceProvider
|
||||
'key' => 'address-book',
|
||||
'label' => t('Address book'),
|
||||
'permission' => PermissionService::ADDRESS_BOOK_VIEW,
|
||||
// escape '\\\\' — four backslashes: PHP reduces to two, SQL reduces to one literal backslash escape char.
|
||||
'countSql' => "select count(*) from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}}",
|
||||
'countParams' => [$like, $like, $like],
|
||||
'previewSql' => "select uuid, first_name, last_name, email from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name limit ?",
|
||||
|
||||
@@ -42,7 +42,7 @@ function setAppContainer(\MintyPHP\App\AppContainer $container): void
|
||||
|
||||
\MintyPHP\Http\ApiBootstrap::configure(
|
||||
static fn (): \MintyPHP\Service\Audit\ApiAuditService => $container->get(\MintyPHP\Service\Audit\ApiAuditService::class),
|
||||
static fn (): \MintyPHP\Service\Settings\SettingGateway => $container->get(\MintyPHP\Service\Settings\SettingGateway::class),
|
||||
static fn (): \MintyPHP\Service\Settings\SettingsApiPolicyGateway => $container->get(\MintyPHP\Service\Settings\SettingsApiPolicyGateway::class),
|
||||
static fn (): \MintyPHP\Service\Security\RateLimiterService => $container->get(\MintyPHP\Service\Security\RateLimiterService::class),
|
||||
static fn (): \MintyPHP\Http\ApiSystemAuditReporter => $container->get(\MintyPHP\Http\ApiSystemAuditReporter::class)
|
||||
);
|
||||
@@ -290,6 +290,66 @@ function allowRegistration(): bool
|
||||
return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature flag: allow frontend telemetry events.
|
||||
*/
|
||||
function frontendTelemetryEnabled(): bool
|
||||
{
|
||||
$value = appSetting('frontend_telemetry_enabled');
|
||||
if ($value === null || $value === '') {
|
||||
return false;
|
||||
}
|
||||
return in_array(strtolower(trim($value)), ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend telemetry sample rate normalized to 0..1.
|
||||
*/
|
||||
function frontendTelemetrySampleRate(): float
|
||||
{
|
||||
$value = appSetting('frontend_telemetry_sample_rate');
|
||||
if ($value === null || trim($value) === '') {
|
||||
return 0.2;
|
||||
}
|
||||
|
||||
$rate = is_numeric($value) ? (float) $value : NAN;
|
||||
if (!is_finite($rate) || $rate < 0 || $rate > 1) {
|
||||
return 0.2;
|
||||
}
|
||||
|
||||
return $rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowed frontend telemetry events as canonical short keys.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function frontendTelemetryAllowedEvents(): array
|
||||
{
|
||||
$allowed = ['warn_once', 'ajax_error'];
|
||||
$value = appSetting('frontend_telemetry_allowed_events');
|
||||
if ($value === null || trim($value) === '') {
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
$items = preg_split('/[\s,]+/', strtolower(trim($value))) ?: [];
|
||||
$normalized = array_values(array_unique(array_filter(
|
||||
array_map(
|
||||
static fn ($entry): string => trim((string) $entry),
|
||||
$items
|
||||
),
|
||||
static fn ($entry): bool => in_array($entry, $allowed, true)
|
||||
)));
|
||||
|
||||
if ($normalized === []) {
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
sort($normalized, SORT_STRING);
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve active primary color (tenant override -> app setting).
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user