diff --git a/core/Controller/Redirect/RedirectAdminController.php b/core/Controller/Redirect/RedirectAdminController.php new file mode 100644 index 0000000..da62811 --- /dev/null +++ b/core/Controller/Redirect/RedirectAdminController.php @@ -0,0 +1,154 @@ +doIndex($page, $query, $request, $session); + } + + /** + * @Route("/admin/redirect/new", name="admin_redirect_new", methods={"GET", "POST"}) + */ + public function new(Factory $factory, EntityManager $entityManager, Request $request): Response + { + return $this->doNew($factory->create(), $entityManager, $request); + } + + /** + * @Route("/admin/redirect/show/{entity}", name="admin_redirect_show", methods={"GET"}) + */ + public function show(Entity $entity): Response + { + return $this->doShow($entity); + } + + /** + * @Route("/admin/redirect/filter", name="admin_redirect_filter", methods={"GET"}) + */ + public function filter(Session $session): Response + { + return $this->doFilter($session); + } + + /** + * @Route("/admin/redirect/edit/{entity}", name="admin_redirect_edit", methods={"GET", "POST"}) + */ + public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response + { + return $this->doEdit($entity, $entityManager, $request); + } + + /** + * @Route("/admin/redirect/sort/{page}", name="admin_redirect_sort", methods={"POST"}, requirements={"page":"\d+"}) + */ + public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response + { + return $this->doSort($page, $query, $entityManager, $request, $session); + } + + /** + * @Route("/admin/redirect/batch/{page}", name="admin_redirect_batch", methods={"POST"}, requirements={"page":"\d+"}) + */ + public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response + { + return $this->doBatch($page, $query, $entityManager, $request, $session); + } + + /** + * @Route("/admin/redirect/delete/{entity}", name="admin_redirect_delete", methods={"DELETE"}) + */ + public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response + { + return $this->doDelete($entity, $entityManager, $request); + } + + protected function getConfiguration(): CrudConfiguration + { + return CrudConfiguration::create() + ->setPageTitle('index', 'Redirects') + ->setPageTitle('edit', '{label}') + ->setPageTitle('new', 'New redirect') + + ->setPageRoute('index', 'admin_redirect_index') + ->setPageRoute('new', 'admin_redirect_new') + ->setPageRoute('edit', 'admin_redirect_edit') + ->setPageRoute('sort', 'admin_redirect_sort') + ->setPageRoute('batch', 'admin_redirect_batch') + ->setPageRoute('delete', 'admin_redirect_delete') + ->setPageRoute('filter', 'admin_redirect_filter') + + ->setForm('edit', Type::class, []) + ->setForm('new', Type::class) + ->setForm('filter', FilterType::class) + + ->setView('form', '@Core/redirect/redirect_admin/_form.html.twig') + ->setMaxPerPage('index', 100) + ->setIsSortableCollection('index', true) + + ->setAction('index', 'show', false) + ->setAction('edit', 'show', false) + + ->setField('index', 'Label', Field\TextField::class, [ + 'property' => 'label', + 'attr' => ['class' => 'col-4'], + ]) + ->setField('index', 'Rule', Field\TextField::class, [ + 'view' => '@Core/redirect/redirect_admin/field/rule.html.twig', + 'attr' => ['class' => 'col-4'], + ]) + ->setField('index', 'Enabled', Field\ButtonField::class, [ + 'property_builder' => function(EntityInterface $entity) { + return $entity->getIsEnabled() ? 'Yes' : 'No'; + }, + 'attr' => ['class' => 'col-2'], + 'button_attr_builder' => function(EntityInterface $entity) { + return ['class' => 'btn btn-sm btn-'.($entity->getIsEnabled() ? 'success' : 'primary')]; + }, + ]) + ->setField('index', 'Type', Field\ButtonField::class, [ + 'property' => 'redirectCode', + 'attr' => ['class' => 'col-2'], + 'button_attr' => ['class' => 'btn btn-sm btn-light border-secondary font-weight-bold'], + ]) + ->setBatchAction('index', 'enable', 'Enable', function (EntityInterface $entity, EntityManager $manager) { + $entity->setIsEnabled(true); + + $manager->update($entity); + }) + ->setBatchAction('index', 'disable', 'Disable', function (EntityInterface $entity, EntityManager $manager) { + $entity->setIsEnabled(false); + + $manager->update($entity); + }) + ->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) { + $manager->delete($entity); + }) + ; + } + + protected function getSection(): string + { + return 'site_navigation'; + } +} diff --git a/core/Entity/Redirect.php b/core/Entity/Redirect.php new file mode 100644 index 0000000..97cbf9b --- /dev/null +++ b/core/Entity/Redirect.php @@ -0,0 +1,211 @@ +id; + } + + public function getScheme(): ?string + { + return $this->scheme; + } + + public function setScheme(string $scheme): self + { + $this->scheme = $scheme; + + return $this; + } + + public function getDomain(): ?string + { + return $this->domain; + } + + public function setDomain(string $domain): self + { + $this->domain = $domain; + + return $this; + } + + public function getDomainType(): ?string + { + return $this->domainType; + } + + public function setDomainType(string $domainType): self + { + $this->domainType = $domainType; + + return $this; + } + + public function getRule(): ?string + { + return $this->rule; + } + + public function setRule(string $rule): self + { + $this->rule = $rule; + + return $this; + } + + public function getRuleType(): ?string + { + return $this->ruleType; + } + + public function setRuleType(string $ruleType): self + { + $this->ruleType = $ruleType; + + return $this; + } + + public function getLocation(): ?string + { + return $this->location; + } + + public function setLocation(string $location): self + { + $this->location = $location; + + return $this; + } + + public function getRedirectCode(): ?int + { + return $this->redirectCode; + } + + public function setRedirectCode(int $redirectCode): self + { + $this->redirectCode = $redirectCode; + + return $this; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function getSortOrder(): ?int + { + return $this->sortOrder; + } + + public function setSortOrder(?int $sortOrder): self + { + $this->sortOrder = $sortOrder; + + return $this; + } + + public function getIsEnabled(): ?bool + { + return $this->isEnabled; + } + + public function setIsEnabled(bool $isEnabled): self + { + $this->isEnabled = $isEnabled; + + return $this; + } + + public function getReuseQueryString(): ?bool + { + return $this->reuseQueryString; + } + + public function setReuseQueryString(bool $reuseQueryString): self + { + $this->reuseQueryString = $reuseQueryString; + + return $this; + } +} diff --git a/core/EventListener/RedirectListener.php b/core/EventListener/RedirectListener.php new file mode 100644 index 0000000..5694ec3 --- /dev/null +++ b/core/EventListener/RedirectListener.php @@ -0,0 +1,58 @@ + + */ +class RedirectListener +{ + protected RedirectMatcher $matcher; + protected RedirectRepositoryQuery $repository; + + public function __construct(RedirectMatcher $matcher, RedirectRepositoryQuery $repository) + { + $this->matcher = $matcher; + $this->repository = $repository; + } + + public function onKernelException(ExceptionEvent $event) + { + $request = $event->getRequest(); + + if (!$event->getThrowable() instanceof NotFoundHttpException) { + return; + } + + $redirects = $this->repository + ->orderBy('.sortOrder') + ->where('.isEnabled=1') + ->find() + ; + + $uri = $event->getRequest()->getUri(); + + foreach ($redirects as $redirect) { + if ($this->matcher->match($redirect, $uri)) { + if ($redirect->getReuseQueryString() && count($event->getRequest()->query)) { + $query = sprintf('?%s', http_build_query($event->getRequest()->query->all())); + } else { + $query = ''; + } + + $event->setResponse(new RedirectResponse( + $redirect->getLocation().$query, + $redirect->getRedirectCode() + )); + } + } + } +} diff --git a/core/Factory/RedirectFactory.php b/core/Factory/RedirectFactory.php new file mode 100644 index 0000000..d7a138a --- /dev/null +++ b/core/Factory/RedirectFactory.php @@ -0,0 +1,14 @@ +add( + 'label', + TextType::class, + [ + 'label' => 'Label', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'scheme', + ChoiceType::class, + [ + 'label' => 'Scheme', + 'required' => false, + 'choices' => [ + 'http(s)://' => 'all', + 'http://' => 'http', + 'https://' => 'https', + ], + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'domain', + TextType::class, + [ + 'label' => 'Domain', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'domainType', + ChoiceType::class, + [ + 'label' => 'Type', + 'required' => false, + 'choices' => [ + 'Domain' => 'domain', + 'Regular expression' => 'regexp', + ], + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'rule', + TextType::class, + [ + 'label' => '', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'ruleType', + ChoiceType::class, + [ + 'label' => 'Type', + 'required' => false, + 'choices' => [ + 'Path' => 'path', + 'Regular expression' => 'regexp', + ], + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'location', + TextType::class, + [ + 'label' => 'Location', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'redirectCode', + ChoiceType::class, + [ + 'label' => 'Code', + 'required' => false, + 'choices' => [ + '301 - Moved Permanently' => 301, + '307 - Temporary Redirect' => 307, + ], + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => null, + 'csrf_protection' => false, + ]); + } +} diff --git a/core/Form/RedirectType.php b/core/Form/RedirectType.php new file mode 100644 index 0000000..3acce35 --- /dev/null +++ b/core/Form/RedirectType.php @@ -0,0 +1,182 @@ +add( + 'label', + TextType::class, + [ + 'label' => 'Label', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'scheme', + ChoiceType::class, + [ + 'label' => 'Scheme', + 'required' => true, + 'choices' => [ + 'http(s)://' => 'all', + 'http://' => 'http', + 'https://' => 'https', + ], + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'domain', + TextType::class, + [ + 'label' => 'Domain', + 'required' => true, + 'attr' => [ + ], + 'help' => 'Regular expression: do not add the delimiter', + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'domainType', + ChoiceType::class, + [ + 'label' => 'Type', + 'required' => true, + 'choices' => [ + 'Domain' => 'domain', + 'Regular expression' => 'regexp', + ], + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'rule', + TextType::class, + [ + 'label' => 'Rule', + 'required' => true, + 'attr' => [ + ], + 'help' => 'Regular expression: do not add the delimiter', + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'ruleType', + ChoiceType::class, + [ + 'label' => 'Type', + 'required' => true, + 'choices' => [ + 'Path' => 'path', + 'Regular expression' => 'regexp', + ], + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'location', + TextType::class, + [ + 'label' => 'Location', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'redirectCode', + ChoiceType::class, + [ + 'label' => 'Code', + 'required' => true, + 'choices' => [ + '301 - Moved Permanently' => 301, + '307 - Temporary Redirect' => 307, + ], + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'isEnabled', + CheckboxType::class, + [ + 'label' => 'Enabled', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'reuseQueryString', + CheckboxType::class, + [ + 'label' => 'Reuse the query string', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Redirect::class, + ]); + } +} diff --git a/core/Repository/RedirectRepository.php b/core/Repository/RedirectRepository.php new file mode 100644 index 0000000..c6d6070 --- /dev/null +++ b/core/Repository/RedirectRepository.php @@ -0,0 +1,21 @@ + +
+
+
+ {{ form_row(form.label) }} +
+
+
+ {{ form_row(form.isEnabled) }} +
+
+
+ {{ form_row(form.scheme) }} +
+
+ +
+
+ {{ form_row(form.domainType) }} +
+
+ {{ form_row(form.domain) }} +
+
+ +
+
+ {{ form_row(form.ruleType) }} +
+
+ {{ form_row(form.rule) }} +
+
+ +
+
+ {{ form_row(form.redirectCode) }} +
+
+ {{ form_row(form.location) }} + {{ form_row(form.reuseQueryString) }} +
+
+
+ diff --git a/core/Resources/views/redirect/redirect_admin/field/rule.html.twig b/core/Resources/views/redirect/redirect_admin/field/rule.html.twig new file mode 100644 index 0000000..34bd810 --- /dev/null +++ b/core/Resources/views/redirect/redirect_admin/field/rule.html.twig @@ -0,0 +1,23 @@ +
+ + {%- if entity.scheme == 'all' -%} + http(s) + {%- else -%} + {{ entity.scheme }} + {%- endif -%}:// + + + {{ entity.domain }} + + + {{ entity.rule }} + +
+ +→ + +
+ + {{ entity.location }} + +
diff --git a/core/Router/RedirectMatcher.php b/core/Router/RedirectMatcher.php new file mode 100644 index 0000000..5002294 --- /dev/null +++ b/core/Router/RedirectMatcher.php @@ -0,0 +1,64 @@ + + */ +class RedirectMatcher +{ + public function match(Redirect $redirect, $url): bool + { + $data = $this->parse($url); + + if (!$this->matchSchema($redirect->getScheme(), $data['scheme'])) { + return false; + } + + if (!$this->matchDomain($redirect->getDomain(), $redirect->getDomainType(), $data['host'])) { + return false; + } + + if (!$this->matchRule($redirect->getRule(), $redirect->getRuleType(), $data['path'])) { + return false; + } + + return true; + } + + protected function matchSchema(string $redirectScheme, $scheme): bool + { + if ('all' === $redirectScheme) { + return true; + } + + return $redirectScheme === $scheme; + } + + protected function matchDomain(string $domain, string $type, string $host): bool + { + if ('domain' === $type) { + return $domain === $host; + } + + return preg_match('`'.$domain.'`', $host) > 0; + } + + protected function matchRule(string $rule, string $type, string $path): bool + { + if ('path' === $type) { + return $rule === $path; + } + + return preg_match('`'.$rule.'`', $path) > 0; + } + + protected function parse($url): array + { + return parse_url($url); + } +}