Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.76% covered (success)
82.76%
24 / 29
80.00% covered (success)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SitemapController
82.76% covered (success)
82.76%
24 / 29
80.00% covered (success)
80.00%
4 / 5
9.42
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sitemapGz
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sitemapXmlGz
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 index
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 generateGzippedSitemap
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
4.59
1<?php
2
3/**
4 * The XML Sitemap Controller
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2024-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2024-06-16
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller;
21
22use phpMyFAQ\Core\Exception;
23use phpMyFAQ\Seo\SitemapXmlService;
24use Symfony\Component\HttpFoundation\RedirectResponse;
25use Symfony\Component\HttpFoundation\Response;
26use Symfony\Component\Routing\Attribute\Route;
27
28final class SitemapController extends AbstractController
29{
30    public function __construct(
31        private readonly SitemapXmlService $sitemapXmlService,
32    ) {
33        parent::__construct();
34    }
35
36    /**
37     * Returns gzipped sitemap.xml or redirects to plain XML if zlib is not available
38     *
39     * @throws Exception|\Exception
40     */
41    #[Route(path: '/sitemap.gz', name: 'public.sitemap.gz')]
42    public function sitemapGz(): Response
43    {
44        return $this->generateGzippedSitemap();
45    }
46
47    /**
48     * Returns gzipped sitemap.xml or redirects to plain XML if zlib is not available
49     *
50     * @throws Exception|\Exception
51     */
52    #[Route(path: '/sitemap.xml.gz', name: 'public.sitemap.xml.gz')]
53    public function sitemapXmlGz(): Response
54    {
55        return $this->generateGzippedSitemap();
56    }
57
58    /**
59     * @throws Exception|\Exception
60     */
61    #[Route(path: '/sitemap.xml', name: 'public.sitemap.xml')]
62    public function index(): Response
63    {
64        $xml = $this->sitemapXmlService->generateXml();
65
66        if ($xml === null) {
67            $response = new Response();
68            $response->setStatusCode(Response::HTTP_NOT_FOUND);
69            $response->setContent('XML Sitemap is disabled.');
70            return $response;
71        }
72
73        $response = new Response();
74        $response->headers->set('Content-Type', 'text/xml');
75        $response->setStatusCode(Response::HTTP_OK);
76        $response->setContent($xml);
77
78        return $response;
79    }
80
81    /**
82     * Generates a gzipped sitemap response or redirects if zlib is unavailable
83     *
84     * @throws Exception|\Exception
85     */
86    private function generateGzippedSitemap(): Response
87    {
88        // Fallback to plain XML if zlib extension is not available
89        if (!extension_loaded('zlib')) {
90            return new RedirectResponse('/sitemap.xml', Response::HTTP_MOVED_PERMANENTLY);
91        }
92
93        $xml = $this->sitemapXmlService->generateXml();
94
95        if ($xml === null) {
96            $response = new Response();
97            $response->setStatusCode(Response::HTTP_NOT_FOUND);
98            $response->setContent('XML Sitemap is disabled.');
99            return $response;
100        }
101
102        $gzipped = gzencode($xml, level: 9);
103
104        $response = new Response($gzipped === false ? '' : $gzipped);
105        $response->headers->set('Content-Type', 'application/x-gzip');
106        $response->headers->set('Content-Encoding', 'gzip');
107        $response->headers->set('Content-Disposition', 'attachment; filename="sitemap.xml.gz"');
108        $response->setStatusCode(Response::HTTP_OK);
109
110        return $response;
111    }
112}