2026-02-04 23:31:53 +01:00
|
|
|
<?php
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
use MintyPHP\DB;
|
|
|
|
|
use MintyPHP\I18n;
|
2026-02-04 23:31:53 +01:00
|
|
|
use MintyPHP\Router;
|
2026-02-23 12:58:19 +01:00
|
|
|
use MintyPHP\Service\Access\PermissionService;
|
|
|
|
|
use MintyPHP\Service\User\UserServicesFactory;
|
2026-02-04 23:31:53 +01:00
|
|
|
use MintyPHP\Support\Guard;
|
|
|
|
|
use MintyPHP\Support\SearchConfig;
|
|
|
|
|
|
|
|
|
|
Guard::requireLogin();
|
|
|
|
|
|
|
|
|
|
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
|
|
|
|
if ($userId > 0) {
|
2026-02-23 12:58:19 +01:00
|
|
|
permissionGateway()->getUserPermissions($userId);
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$query = trim((string) ($_GET['search'] ?? ''));
|
|
|
|
|
if ($query === '') {
|
|
|
|
|
Router::json(['data' => [], 'total' => 0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$limit = (int) ($_GET['limit'] ?? 10);
|
|
|
|
|
if ($limit < 1) {
|
|
|
|
|
$limit = 10;
|
|
|
|
|
} elseif ($limit > 100) {
|
|
|
|
|
$limit = 100;
|
|
|
|
|
}
|
|
|
|
|
$offset = (int) ($_GET['offset'] ?? 0);
|
|
|
|
|
if ($offset < 0) {
|
|
|
|
|
$offset = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$locale = I18n::$locale ?? (I18n::$defaultLocale ?? 'de');
|
2026-02-23 12:58:19 +01:00
|
|
|
$userTenantRepository = (new UserServicesFactory())->createUserTenantRepository();
|
|
|
|
|
$tenantIds = $userId > 0 ? $userTenantRepository->listTenantIdsByUserId($userId) : [];
|
2026-02-04 23:31:53 +01:00
|
|
|
$tenantScoped = !empty($tenantIds);
|
|
|
|
|
|
|
|
|
|
$tenantScopeFilters = SearchConfig::tenantScopeFilters();
|
|
|
|
|
$resources = SearchConfig::resources($query, $locale);
|
|
|
|
|
$items = [];
|
|
|
|
|
|
|
|
|
|
foreach ($resources as $resource) {
|
|
|
|
|
$key = (string) ($resource['key'] ?? '');
|
|
|
|
|
if ($key === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$perm = (string) ($resource['permission'] ?? '');
|
2026-02-23 12:58:19 +01:00
|
|
|
if ($perm !== '' && !permissionGateway()->userHas($userId, $perm)) {
|
2026-02-04 23:31:53 +01:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tenantFilter = $tenantScopeFilters[$key] ?? '';
|
|
|
|
|
$isTenantScoped = $tenantFilter !== '';
|
|
|
|
|
if ($isTenantScoped && !$tenantScoped) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sql = $resource['resultSql'] ?? '';
|
|
|
|
|
if ($sql === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$sql = str_replace('{{tenantFilter}}', $isTenantScoped ? $tenantFilter : '', $sql);
|
|
|
|
|
$params = $resource['resultParams'] ?? [];
|
|
|
|
|
if ($isTenantScoped && strpos($sql, '???') !== false) {
|
|
|
|
|
$params[] = $tenantIds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$rows = DB::select($sql, ...$params);
|
|
|
|
|
foreach ($rows as $row) {
|
|
|
|
|
$data = $row;
|
|
|
|
|
if (is_array($row) && count($row) === 1) {
|
|
|
|
|
$data = reset($row);
|
|
|
|
|
}
|
|
|
|
|
$data = is_array($data) ? $data : [];
|
|
|
|
|
$mapped = SearchConfig::mapResultItem($key, (string) ($resource['label'] ?? ''), $data);
|
|
|
|
|
if ($mapped !== null) {
|
|
|
|
|
$items[] = $mapped;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
$items = array_merge($items, SearchConfig::hotkeyResultItems($query));
|
2026-02-23 12:58:19 +01:00
|
|
|
if (permissionGateway()->userHas($userId, PermissionService::DOCS_VIEW)) {
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
$items = array_merge($items, SearchConfig::docsResultItems($query));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 19:28:12 +01:00
|
|
|
$scoreQuery = SearchConfig::normalizeScoreQuery($query);
|
|
|
|
|
$queryLower = mb_strtolower($scoreQuery);
|
2026-02-04 23:31:53 +01:00
|
|
|
$scoreItem = static function (array $item) use ($queryLower): int {
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
$manualScore = (int) ($item['search_score'] ?? 0);
|
|
|
|
|
if ($manualScore > 0) {
|
|
|
|
|
return $manualScore;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 23:31:53 +01:00
|
|
|
$title = mb_strtolower((string) ($item['title'] ?? ''));
|
|
|
|
|
$description = mb_strtolower((string) ($item['description'] ?? ''));
|
|
|
|
|
if ($title === $queryLower) {
|
|
|
|
|
return 400;
|
|
|
|
|
}
|
|
|
|
|
if ($title !== '' && strpos($title, $queryLower) === 0) {
|
|
|
|
|
return 300;
|
|
|
|
|
}
|
|
|
|
|
if ($title !== '' && strpos($title, $queryLower) !== false) {
|
|
|
|
|
return 200;
|
|
|
|
|
}
|
|
|
|
|
if ($description !== '' && strpos($description, $queryLower) !== false) {
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
usort($items, static function (array $a, array $b) use ($scoreItem): int {
|
|
|
|
|
$scoreA = $scoreItem($a);
|
|
|
|
|
$scoreB = $scoreItem($b);
|
|
|
|
|
if ($scoreA !== $scoreB) {
|
|
|
|
|
return $scoreB <=> $scoreA;
|
|
|
|
|
}
|
|
|
|
|
$title = strcmp((string) ($a['title'] ?? ''), (string) ($b['title'] ?? ''));
|
|
|
|
|
if ($title !== 0) {
|
|
|
|
|
return $title;
|
|
|
|
|
}
|
|
|
|
|
return strcmp((string) ($a['type'] ?? ''), (string) ($b['type'] ?? ''));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$total = count($items);
|
|
|
|
|
$items = array_slice($items, $offset, $limit);
|
|
|
|
|
|
|
|
|
|
Router::json([
|
|
|
|
|
'data' => $items,
|
|
|
|
|
'total' => $total,
|
|
|
|
|
]);
|