Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.62% covered (success)
97.62%
41 / 42
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
SitemapsController
97.62% covered (success)
97.62%
41 / 42
66.67% covered (warning)
66.67%
2 / 3
9
0.00% covered (danger)
0.00%
0 / 1
 index
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 _generateUrls
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
5
 _renderXml
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * Sitemaps Controller
5 *
6 * Generates XML sitemap for search engines
7 *
8 * @link https://www.sitemaps.org/protocol.html
9 */
10class SitemapsController extends AppController
11{
12    /**
13     * Models
14     */
15    public $uses = ['Set', 'Tag'];
16
17    /**
18     * Generate XML sitemap
19     *
20     * Outputs sitemap.xml with all public URLs.
21     * Caches rendered XML for 1 day. Webserver handles gzip compression.
22     *
23     * @return void
24     */
25    public function index()
26    {
27        $cacheKey = 'sitemap_xml';
28        $xml = Cache::read($cacheKey, 'long');
29
30        if ($xml === false)
31        {
32            $urls = $this->_generateUrls();
33            $xml = $this->_renderXml($urls);
34            Cache::write($cacheKey, $xml, 'long');
35        }
36
37        $this->response->type('xml');
38        $this->response->body($xml);
39        $this->autoRender = false;
40    }
41
42    /**
43     * Generate all URLs for sitemap
44     *
45     * Note: Google ignores <priority> and <changefreq>, so we only use <loc>.
46     * See: https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap
47     *
48     * @return string[]
49     */
50    private function _generateUrls()
51    {
52        $urls = [];
53
54        // Homepage
55        $urls[] = Router::url('/', true);
56
57        // All public puzzle sets
58        $sets = $this->Set->find('all', [
59            'conditions' => ['Set.public' => 1],
60            'fields' => ['Set.id'],
61            'order' => ['Set.id' => 'ASC']
62        ]);
63
64        foreach ($sets as $set)
65            $urls[] = Router::url(['controller' => 'sets', 'action' => 'view', $set['Set']['id']], true);
66
67        // All tags
68        $tags = $this->Tag->find('all', [
69            'fields' => ['Tag.name'],
70            'order' => ['Tag.name' => 'ASC']
71        ]);
72
73        foreach ($tags as $tag)
74            $urls[] = Router::url(['controller' => 'sets', 'action' => 'tag', $tag['Tag']['name']], true);
75
76        // Static pages
77        $staticPages = [
78            ['controller' => 'hero', 'action' => 'index'],
79            ['controller' => 'time_mode', 'action' => 'play'],
80        ];
81
82        foreach ($staticPages as $page)
83            $urls[] = Router::url(['controller' => $page['controller'], 'action' => $page['action']], true);
84
85        // All individual puzzles in public sets
86        $baseUrl = Router::url('/', true);
87        $puzzleIds = Util::query("
88SELECT set_connection.id
89FROM set_connection
90    JOIN `set` ON set_connection.set_id = `set`.id
91WHERE `set`.public = 1
92ORDER BY set_connection.id ASC");
93
94        foreach ($puzzleIds as $row)
95            $urls[] = $baseUrl . $row['id'];
96
97        return $urls;
98    }
99
100    /**
101     * Render URL list as XML sitemap string
102     *
103     * @param string[] $urls
104     * @return string
105     */
106    private function _renderXml(array $urls): string
107    {
108        $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
109        $xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
110
111        foreach ($urls as $url)
112            $xml .= "\t<url><loc>" . htmlspecialchars($url, ENT_XML1, 'UTF-8') . "</loc></url>\n";
113
114        $xml .= '</urlset>' . "\n";
115
116        return $xml;
117    }
118}