diff --git a/src/Controller/Api/ApiController.php b/src/Controller/Api/ApiController.php index 17d4465d..30ea8c9b 100644 --- a/src/Controller/Api/ApiController.php +++ b/src/Controller/Api/ApiController.php @@ -20,6 +20,7 @@ use Packeton\Security\Provider\AuditSessionProvider; use Packeton\Service\JobPersister; use Packeton\Service\Scheduler; +use Packeton\Service\SubRepositoryHelper; use Packeton\Util\PacketonUtils; use Packeton\Webhook\HookBus; use Psr\Log\LoggerInterface; @@ -43,6 +44,7 @@ public function __construct( protected ValidatorInterface $validator, protected IntegrationRegistry $integrations, protected AuditSessionProvider $auditSessionProvider, + protected SubRepositoryHelper $subRepositoryHelper, ) { } @@ -189,12 +191,14 @@ public function editPackageAction(Request $request, #[Vars] Package $package): R #[Route('/downloads/{name}', name: 'track_download', requirements: ['name' => '%package_name_regex%'], methods: ['POST'])] + #[Route('/{slug}/downloads/{name}', name: 'track_download_slug', requirements: ['name' => '%package_name_regex%'], methods: ['POST'])] public function trackDownloadAction(Request $request, $name): Response { + $allowed = $this->subRepositoryHelper->allowedPackageIds(); $result = $this->getPackageAndVersionId($name, $request->request->get('version_normalized')); - if (!$result) { - return new JsonResponse(['status' => 'error', 'message' => 'Package not found'], 200); + if (!$result || (null !== $allowed && !in_array($result['id'], $allowed, true))) { + return new JsonResponse(['status' => 'error', 'message' => 'Package not found'], 404); } $this->downloadManager->addDownloads(['id' => $result['id'], 'vid' => $result['vid'], 'ip' => $request->getClientIp()]); @@ -223,6 +227,7 @@ public function getJobAction(string $id): Response * @inheritDoc */ #[Route('/downloads/', name: 'track_download_batch', methods: ['POST'])] + #[Route('/{slug}/downloads/', name: 'track_download_batch_slug', methods: ['POST'])] public function trackDownloadsAction(Request $request): Response { $contents = \json_decode($request->getContent(), true); @@ -234,6 +239,7 @@ public function trackDownloadsAction(Request $request): Response $ip = $request->getClientIp(); $jobs = []; + $allowed = $this->subRepositoryHelper->allowedPackageIds(); foreach ($contents['downloads'] as $package) { $result = $this->getPackageAndVersionId($package['name'], $package['version']); @@ -242,6 +248,10 @@ public function trackDownloadsAction(Request $request): Response continue; } + if (null !== $allowed && !in_array($result['id'], $allowed, true)) { + continue; + } + $audit[] = "{$package['name']}: {$package['version']}"; $jobs[] = ['id' => $result['id'], 'vid' => $result['vid'], 'ip' => $ip]; } diff --git a/src/Controller/ProviderController.php b/src/Controller/ProviderController.php index 6efaa36c..381261a6 100644 --- a/src/Controller/ProviderController.php +++ b/src/Controller/ProviderController.php @@ -21,6 +21,7 @@ class ProviderController extends AbstractController { use ControllerTrait; + use SubRepoControllerTrait; public function __construct( private readonly PackageManager $packageManager, @@ -128,9 +129,6 @@ public function packageV2Action(Request $request, string $package): Response { $isDev = str_ends_with($package, '~dev'); $packageName = preg_replace('/~dev$/', '', $package); - if (!$this->checkSubrepositoryAccess($packageName)) { - return $this->createNotFound(); - } $response = new JsonResponse([]); $response->setLastModified($this->providerManager->getLastModify($package)); @@ -138,6 +136,10 @@ public function packageV2Action(Request $request, string $package): Response return $response; } + if (!$this->checkSubrepositoryAccess($packageName)) { + return $this->createNotFound(); + } + $package = $this->packageManager->getPackageV2Json($this->getUser(), $packageName, $isDev); if (!$package) { return $this->createNotFound(); @@ -154,12 +156,6 @@ protected function createNotFound(?string $msg = null): Response return new JsonResponse(['status' => 'error', 'message' => $msg ?: 'Not Found'], 404); } - protected function checkSubrepositoryAccess(string $name): bool - { - $packages = $this->subRepositoryHelper->allowedPackageNames(); - return $packages === null || in_array($name, $packages, true); - } - protected function createJsonResponse(array $data): JsonResponse { $response = new JsonResponse($data); diff --git a/src/Controller/SubRepoControllerTrait.php b/src/Controller/SubRepoControllerTrait.php new file mode 100644 index 00000000..22a4a811 --- /dev/null +++ b/src/Controller/SubRepoControllerTrait.php @@ -0,0 +1,14 @@ +subRepositoryHelper->allowedPackageNames(); + return $packages === null || in_array($name, $packages, true); + } +} diff --git a/src/Controller/WebController.php b/src/Controller/WebController.php index d4fdeef1..c2f06b1d 100644 --- a/src/Controller/WebController.php +++ b/src/Controller/WebController.php @@ -16,6 +16,7 @@ use Doctrine\Persistence\ManagerRegistry; use Packeton\Entity\Group; use Packeton\Entity\Package; +use Packeton\Entity\SubRepository; use Packeton\Entity\Version; use Packeton\Form\Model\SearchQuery; use Packeton\Form\Type\SearchQueryType; @@ -52,6 +53,22 @@ public function indexAction(Request $request): Response ]); } + #[Route('/{slug}', name: 'sub_repository_home', methods: ['GET'], priority: -50)] + public function subRepoAction(Request $request, string $slug): Response + { + $repo = $this->registry->getRepository(SubRepository::class)->findOneBy(['slug' => $slug]); + if (!$repo instanceof SubRepository) { + throw $this->createNotFoundException(); + } + + $isHost = $this->subRepositoryHelper->getByHost($request->getHost()); + + return $this->render('subrepository/public.html.twig', [ + 'repo' => $repo, + 'repoUrl' => $request->getSchemeAndHttpHost() . ($isHost ? '' : '/'.$slug), + ]); + } + /** * Rendered by views/Web/searchSection.html.twig */ diff --git a/src/Controller/ZipballController.php b/src/Controller/ZipballController.php index 72be2d84..8e597115 100644 --- a/src/Controller/ZipballController.php +++ b/src/Controller/ZipballController.php @@ -12,6 +12,7 @@ use Packeton\Model\UploadZipballStorage; use Packeton\Package\RepTypes; use Packeton\Service\DistManager; +use Packeton\Service\SubRepositoryHelper; use Packeton\Util\PacketonUtils; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -27,11 +28,14 @@ #[Route(defaults: ['_format' => 'json'])] class ZipballController extends AbstractController { + use SubRepoControllerTrait; + public function __construct( protected DistManager $dm, protected UploadZipballStorage $storage, protected ManagerRegistry $registry, protected EventDispatcherInterface $dispatcher, + protected SubRepositoryHelper $subRepositoryHelper, protected LoggerInterface $logger, ) { } @@ -85,19 +89,33 @@ public function zipballList(Request $request): Response requirements: ['package' => '%package_name_regex%', 'hash' => '[a-f0-9]{40}(\.?[A-Za-z\.]+?)?'], methods: ['GET'] )] + #[Route( + '/{slug}/zipball/{package}/{hash}', + name: 'download_dist_package_slug', + requirements: ['package' => '%package_name_regex%', 'hash' => '[a-f0-9]{40}(\.?[A-Za-z\.]+?)?'], + methods: ['GET'] + )] public function zipballAction(#[Vars('name')] Package $package, string $hash): Response { if ((false === $this->dm->isEnabled() && false === RepTypes::isBuildInDist($package->getRepoType())) - || !\preg_match('{[a-f0-9]{40}}i', $hash, $match) || !($reference = $match[0]) + || !\preg_match('{[a-f0-9]{40}}i', $hash, $match) + || !($reference = $match[0]) + || !$this->checkSubrepositoryAccess($package->getName()) ) { return $this->createNotFound(); } - $isGranted = $this->isGranted('VIEW_ALL_VERSION', $package) || $this->isGranted('ROLE_FULL_CUSTOMER', $package); - foreach ($package->getAllVersionsByReference($reference) as $version) { - $isGranted |= $this->isGranted('ROLE_FULL_CUSTOMER', $version); + $isGranted = $this->subRepositoryHelper->isPublicAccess() + || $this->isGranted('VIEW_ALL_VERSION', $package) + || $this->isGranted('ROLE_FULL_CUSTOMER', $package); + + if (false === $isGranted) { + foreach ($package->getAllVersionsByReference($reference) as $version) { + $isGranted = $isGranted || $this->isGranted('ROLE_FULL_CUSTOMER', $version); + } } - if (!$isGranted) { + + if (true !== $isGranted) { return $this->createNotFound(); } diff --git a/src/Entity/SubRepository.php b/src/Entity/SubRepository.php index e7177217..6ae06521 100644 --- a/src/Entity/SubRepository.php +++ b/src/Entity/SubRepository.php @@ -32,6 +32,12 @@ class SubRepository #[ORM\Column(type: 'json', nullable: true)] private ?array $packages = null; + #[ORM\Column(name: 'public_access', type: 'boolean', nullable: true)] + private ?bool $publicAccess = null; + + #[ORM\Column(name: 'html_markup', type: 'text', nullable: true)] + private ?string $htmlMarkup = null; + /** @internal */ private ?array $cachedIds = null; @@ -129,4 +135,26 @@ public function setCachedIds(?array $cachedIds): static $this->cachedIds = $cachedIds; return $this; } + + public function isPublicAccess(): ?bool + { + return $this->publicAccess; + } + + public function setPublicAccess(?bool $publicAccess): static + { + $this->publicAccess = $publicAccess; + return $this; + } + + public function getHtmlMarkup(): ?string + { + return $this->htmlMarkup; + } + + public function setHtmlMarkup(?string $htmlMarkup): static + { + $this->htmlMarkup = $htmlMarkup; + return $this; + } } diff --git a/src/EventListener/PackagistListener.php b/src/EventListener/PackagistListener.php index cde14ef8..6ab3dd52 100644 --- a/src/EventListener/PackagistListener.php +++ b/src/EventListener/PackagistListener.php @@ -18,6 +18,7 @@ use Packeton\Event\FormHandlerEvent; use Packeton\Model\ProviderManager; use Packeton\Service\DistConfig; +use Packeton\Service\SubRepositoryHelper; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; @@ -38,7 +39,8 @@ class PackagistListener public function __construct( private readonly RequestStack $requestStack, private readonly ProviderManager $providerManager, - ){ + private readonly SubRepositoryHelper $subRepositoryHelper, + ) { } /** @@ -56,8 +58,10 @@ public function postLoad(Version $version, PostLoadEventArgs $event) $dist = $version->getDist(); if (isset($dist['url']) && \str_starts_with($dist['url'], DistConfig::HOSTNAME_PLACEHOLDER)) { $currentHost = $request->getSchemeAndHttpHost(); + $slug = $this->subRepositoryHelper->getCurrentSlug(); + $replacement = null !== $slug ? $currentHost . '/' . $slug : $currentHost; - $dist['url'] = \str_replace(DistConfig::HOSTNAME_PLACEHOLDER, $currentHost, $dist['url']); + $dist['url'] = \str_replace(DistConfig::HOSTNAME_PLACEHOLDER, $replacement, $dist['url']); $version->distNormalized = $dist; } } diff --git a/src/EventListener/ProtectHostListener.php b/src/EventListener/ProtectHostListener.php index 0a5fa6f4..ab156758 100644 --- a/src/EventListener/ProtectHostListener.php +++ b/src/EventListener/ProtectHostListener.php @@ -21,11 +21,14 @@ class ProtectHostListener 'root_package_v2' => 1, 'download_dist_package' => 1, 'track_download' => 1, - 'track_download_batch' =>1, - 'root_packages_slug' =>1, + 'track_download_batch' => 1, + 'root_packages_slug' => 1, 'root_providers_slug' => 1, 'root_package_slug' => 1, 'root_package_v2_slug' => 1, + 'download_dist_package_slug' => 1, + 'track_download_batch_slug' => 1, + 'track_download_slug' => 1, 'mirror_root' => 1, 'mirror_metadata_v2' => 1, 'mirror_metadata_v1' => 1, diff --git a/src/EventListener/SubRepositoryListener.php b/src/EventListener/SubRepositoryListener.php index 4a117916..9dcd4ee1 100644 --- a/src/EventListener/SubRepositoryListener.php +++ b/src/EventListener/SubRepositoryListener.php @@ -9,18 +9,21 @@ use Packeton\Model\PacketonUserInterface; use Packeton\Security\Acl\SubRepoGrantVoter; use Packeton\Service\SubRepositoryHelper; +use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Twig\Environment; class SubRepositoryListener { - public static $skipRoutes = [ - 'download_dist_package' => 1, - 'track_download_batch' => 1, - 'track_download' => 1, + private static array $skipRoutes = [ 'login' => 1, 'logout' => 1, 'subrepository_switch' => 1, @@ -28,12 +31,62 @@ class SubRepositoryListener 'api_health' => 1, ]; + private static array $downloadRoutes = [ + 'download_dist_package' => 1, + 'track_download_batch' => 1, + 'track_download' => 1, + ]; + + private static array $loginRoutes = [ + 'login' => 1, + 'change_password' => 1, + 'request_pwd_check_email' => 1, + 'request_pwd_reset' => 1, + 'oauth_login' => 1, + 'oauth_check' => 1, + ]; + public function __construct( protected SubRepositoryHelper $helper, - protected TokenStorageInterface $tokenStorage + protected TokenStorageInterface $tokenStorage, + #[AutowireServiceClosure(service: 'twig')] + protected $twig, ) { } + #[AsEventListener(event: 'kernel.exception', priority: 10)] + public function onKernelException(ExceptionEvent $event): void + { + if (!$event->getThrowable() instanceof AccessDeniedException) { + return; + } + + $request = $event->getRequest(); + $route = (string)$request->attributes->get('_route'); + $isPublicSubRepoHost = null !== ($subRepo = $this->helper->getByHost($request->getHost())) + && $this->helper->isPublicAccess($subRepo) + && null === $this->tokenStorage->getToken()?->getUser(); + + if (!$isPublicSubRepoHost || isset(SubRepoGrantVoter::$rootRoutes[$route])) { + return; + } + + $repo = $this->helper->findSubRepo($subRepo); + if ($route === 'home' && null !== $repo) { + $response = $this->getTwig()->render('subrepository/public.html.twig', ['repo' => $repo]); + $event->setResponse(new Response($response)); + $event->allowCustomResponseCode(); + return; + } + + if (isset(self::$loginRoutes[$route])) { + return; + } + + $event->setResponse(new JsonResponse(['error' => 'Not found'], 404)); + $event->allowCustomResponseCode(); + } + #[AsEventListener(event: 'kernel.request')] public function onKernelRequest(RequestEvent $event): void { @@ -70,6 +123,10 @@ public function onKernelRequest(RequestEvent $event): void $request->attributes->set('_sub_repo_type', !$withSlug ? SubRepository::AUTO_HOST : null); } + if (isset(self::$downloadRoutes[$route])) { + return; + } + if ($user instanceof PacketonUserInterface) { $allowedRepos = $user->getSubRepos() ?: []; $isAdmin = $user instanceof User ? $user->isAdmin() : in_array('ROLE_ADMIN', $token->getRoleNames()); @@ -96,4 +153,9 @@ public function onKernelRequest(RequestEvent $event): void } } } + + private function getTwig(): Environment + { + return ($this->twig)(); + } } diff --git a/src/Form/Type/SubRepositoryType.php b/src/Form/Type/SubRepositoryType.php index 7b1b0035..a0c8c742 100644 --- a/src/Form/Type/SubRepositoryType.php +++ b/src/Form/Type/SubRepositoryType.php @@ -8,6 +8,7 @@ use Packeton\Entity\Package; use Packeton\Entity\SubRepository; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -50,7 +51,15 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('urls', TextareaType::class, [ 'required' => false, 'label' => 'Subdomain or separate hostname', - 'attr' => ['placeholder' => "e.g.: repo1.example.com\nrepo2.example.com", 'rows' => 4] + 'attr' => ['placeholder' => "e.g.: repo1.example.com\nrepo2.example.com", 'rows' => 4], + ]) + ->add('publicAccess', CheckboxType::class, [ + 'required' => false, + 'label' => 'Allow public access', + ]) + ->add('htmlMarkup', TextareaType::class, [ + 'required' => false, + 'label' => 'HTML public page markup', ]) ->add('packages', ChoiceType::class, [ 'required' => false, diff --git a/src/Model/PackageManager.php b/src/Model/PackageManager.php index 374defe5..f09afc45 100644 --- a/src/Model/PackageManager.php +++ b/src/Model/PackageManager.php @@ -182,7 +182,7 @@ public function getProvidersJson(?UserInterface $user, $hash, ?int $subRepo = nu */ public function getPackageJson(?UserInterface $user, string $package) { - if ($user && $this->authorizationChecker->isGranted('ROLE_FULL_CUSTOMER')) { + if ($user && ($this->authorizationChecker->isGranted('ROLE_FULL_CUSTOMER') || $this->subRepositoryHelper->isPublicAccess())) { $user = null; } @@ -223,7 +223,7 @@ public function getCachedPackageJson(?UserInterface $user, string $package, ?str private function dumpInMemory(?UserInterface $user = null, bool $ignoreLastModify = true, ?int $apiVersion = null, ?int $subRepo = null) { - if ($user && $this->authorizationChecker->isGranted('ROLE_FULL_CUSTOMER')) { + if ($user && ($this->authorizationChecker->isGranted('ROLE_FULL_CUSTOMER') || $this->subRepositoryHelper->isPublicAccess($subRepo))) { $user = null; } diff --git a/src/Model/PatUserScores.php b/src/Model/PatUserScores.php index 4b77f606..e5b978d4 100644 --- a/src/Model/PatUserScores.php +++ b/src/Model/PatUserScores.php @@ -10,7 +10,8 @@ class PatUserScores 'metadata' => [ 'root_packages', 'root_providers', 'metadata_changes', 'root_package', 'root_package_v2', 'download_dist_package', 'track_download', 'track_download_batch', - 'root_packages_slug', 'root_providers_slug', 'root_package_slug', 'root_package_v2_slug', + 'root_packages_slug', 'root_providers_slug', 'root_package_slug', 'root_package_v2_slug', 'download_dist_package_slug', + 'track_download_slug', 'track_download_batch_slug', ], 'mirror:read' => ['mirror_root', 'mirror_metadata_v2', 'mirror_metadata_v1', 'mirror_zipball', 'mirror_provider_includes'], 'mirror:all' => ['@mirror:read'], diff --git a/src/Package/InMemoryDumper.php b/src/Package/InMemoryDumper.php index c738f5ca..c3ca16d2 100644 --- a/src/Package/InMemoryDumper.php +++ b/src/Package/InMemoryDumper.php @@ -100,14 +100,14 @@ private function dumpRootPackages(?UserInterface $user = null, ?int $apiVersion $url = $this->router->generate('track_download', ['name' => 'VND/PKG']); $slug = $subRepo && !$this->subRepositoryHelper->isAutoHost() ? '/'. $subRepo->getSlug() : ''; - $rootFile['notify'] = str_replace('VND/PKG', '%package%', $url); - $rootFile['notify-batch'] = $this->router->generate('track_download_batch'); + $rootFile['notify'] = $slug . str_replace('VND/PKG', '%package%', $url); + $rootFile['notify-batch'] = $slug . $this->router->generate('track_download_batch'); $rootFile['metadata-changes-url'] = $this->router->generate('metadata_changes'); $rootFile['providers-url'] = $slug . '/p/%package%$%hash%.json'; if ($this->distConfig->mirrorEnabled()) { $ref = '0000000000000000000000000000000000000000.zip'; - $zipball = $this->router->generate('download_dist_package', ['package' => 'VND/PKG', 'hash' => $ref]); + $zipball = $slug . $this->router->generate('download_dist_package', ['package' => 'VND/PKG', 'hash' => $ref]); $rootFile['mirrors'][] = ['dist-url' => \str_replace(['VND/PKG', $ref], ['%package%', '%reference%.%type%'], $zipball), 'preferred' => true]; } diff --git a/src/Repository/SubEntityRepository.php b/src/Repository/SubEntityRepository.php index fa361d6f..a38d37d1 100644 --- a/src/Repository/SubEntityRepository.php +++ b/src/Repository/SubEntityRepository.php @@ -11,7 +11,7 @@ public function getSubRepositoryData(): array { $data = $this->createQueryBuilder('s') ->resetDQLPart('select') - ->select(['s.id', 's.slug', 's.urls', 's.name']) + ->select(['s.id', 's.slug', 's.urls', 's.name', 's.publicAccess as public']) ->getQuery() ->getArrayResult(); diff --git a/src/Security/Acl/SubRepoGrantVoter.php b/src/Security/Acl/SubRepoGrantVoter.php index d217a5c6..843df394 100644 --- a/src/Security/Acl/SubRepoGrantVoter.php +++ b/src/Security/Acl/SubRepoGrantVoter.php @@ -4,26 +4,53 @@ namespace Packeton\Security\Acl; +use Packeton\Service\SubRepositoryHelper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface; class SubRepoGrantVoter implements CacheableVoterInterface { - public static $subRoutes = [ + public static array $subRoutes = [ 'root_packages_slug' => 1, 'root_providers_slug' => 1, 'root_package_slug' => 1, 'root_package_v2_slug' => 1, + 'download_dist_package_slug' => 1, + 'track_download_batch_slug' => 1, + 'track_download_slug' => 1, + 'sub_repository_home' => 1, ]; + public static array $rootRoutes = [ + 'root_packages' => 1, + 'root_providers' => 1, + 'root_package' => 1, + 'root_package_v2' => 1, + 'download_dist_package' => 1, + 'track_download_batch' => 1, + 'track_download' => 1, + ]; + + public function __construct( + private readonly SubRepositoryHelper $helper + ) { + } + /** * {@inheritdoc} */ - public function vote(TokenInterface $token, mixed $subject, array $attributes): int + public function vote(TokenInterface $token, mixed $request, array $attributes): int { - if ($subject instanceof Request && isset(self::$subRoutes[$subject->attributes->get('_route')])) { - return $token->getUser() ? self::ACCESS_GRANTED : self::ACCESS_ABSTAIN; + if (!$request instanceof Request) { + return self::ACCESS_ABSTAIN; + } + + $route = $request->attributes->get('_route'); + if (isset(self::$subRoutes[$route]) + || (isset(self::$rootRoutes[$route]) && null !== $this->helper->getByHost($request->getHost())) + ) { + return $token->getUser() || $this->isPublicSubRepo($request) ? self::ACCESS_GRANTED : self::ACCESS_ABSTAIN; } return self::ACCESS_ABSTAIN; @@ -44,4 +71,22 @@ public function supportsType(string $subjectType): bool { return $subjectType === Request::class; } + + private function isPublicSubRepo(Request $request): bool + { + if (null !== ($subRepo = $this->getSubRepoForRequest($request))) { + return $this->helper->isPublicAccess($subRepo); + } + return false; + } + + private function getSubRepoForRequest(Request $request): ?int + { + $route = (string) $request->attributes->get('_route'); + if ($request->attributes->has('slug') && (SubRepoGrantVoter::$subRoutes[$route] ?? null)) { + return $this->helper->getBySlug($request->attributes->get('slug')); + } + + return $this->helper->getByHost($request->getHost()); + } } diff --git a/src/Service/SubRepositoryHelper.php b/src/Service/SubRepositoryHelper.php index b077ade8..54b0bd14 100644 --- a/src/Service/SubRepositoryHelper.php +++ b/src/Service/SubRepositoryHelper.php @@ -53,6 +53,14 @@ public function allowedPackageNames(): ?array return $entity === null ? null : (empty($packages) ? [] : $packages); } + public function getRepoOption(?int $subRepo, ?string $name = null): mixed + { + $subRepo ??= $this->getSubrepositoryId(); + $data = $this->getData()[$subRepo] ?? null; + + return null === $name ? $data : ($data[$name] ?? null); + } + public function allowedPackageIds(?array $moreAllowed = null): ?array { $entity = $this->getCurrentSubrepository(); @@ -90,6 +98,12 @@ public function isAutoHost(): bool return $req->attributes->get('_sub_repo_type') === SubRepository::AUTO_HOST; } + public function isPublicAccess(?int $subRepo = null): bool + { + $subRepo ??= $this->getSubrepositoryId(); + return $this->getData()[$subRepo]['public'] ?? false; + } + public static function applyCondition(QueryBuilder $qb, ?array $allowed): QueryBuilder { if ($allowed === null) { @@ -114,6 +128,14 @@ public function applySubRepository(QueryBuilder $qb): QueryBuilder return self::applyCondition($qb, $allowed); } + public function findSubRepo(null|int|string $subRepoOrSlug = null): ?SubRepository + { + $subRepoOrSlug ??= $this->getSubrepositoryId(); + $subRepo = is_string($subRepoOrSlug) ? $this->getBySlug($subRepoOrSlug) : $subRepoOrSlug; + + return null !== $subRepo ? $this->registry->getRepository(SubRepository::class)->find($subRepo) : null; + } + public function getCurrentSubrepository(): ?SubRepository { if (!$req = $this->requestStack->getMainRequest()) { @@ -124,9 +146,15 @@ public function getCurrentSubrepository(): ?SubRepository $subRepo = $req->attributes->get('_sub_repo'); $entity = $subRepo > 0 ? $this->registry->getRepository(SubRepository::class)->find($subRepo) : null; } + return $entity; } + public function getCurrentSlug(): ?string + { + return ($subRepo = $this->getCurrentSubrepository()) && !$this->isAutoHost() ? $subRepo->getSlug() : null; + } + public function getSubrepositoryId(): ?int { if (!$req = $this->requestStack->getMainRequest()) { @@ -161,8 +189,6 @@ public function getTwigData(?UserInterface $user = null): array protected function getData(): array { - return $this->cache->get('sub_repos_list', function () { - return $this->registry->getRepository(SubRepository::class)->getSubRepositoryData(); - }); + return $this->cache->get('sub_repos_list', fn () => $this->registry->getRepository(SubRepository::class)->getSubRepositoryData()); } } diff --git a/templates/subrepository/public.html.twig b/templates/subrepository/public.html.twig new file mode 100644 index 00000000..5ec06bb0 --- /dev/null +++ b/templates/subrepository/public.html.twig @@ -0,0 +1,77 @@ + + +
+ + ++ This is PHP package repository {{ repo.name }} site. +
+{ + "repositories": [ + { + "type": "composer", + "url": "{{ repoUrl }}" + } + ] +}+ +