xref: /webtrees/app/Module/SiteMapModule.php (revision 94582e90d3cfca0ccb8293b56de3ad502f9f0f70)
18c2e8227SGreg Roach<?php
23976b470SGreg Roach
38c2e8227SGreg Roach/**
48c2e8227SGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
68c2e8227SGreg Roach * This program is free software: you can redistribute it and/or modify
78c2e8227SGreg Roach * it under the terms of the GNU General Public License as published by
88c2e8227SGreg Roach * the Free Software Foundation, either version 3 of the License, or
98c2e8227SGreg Roach * (at your option) any later version.
108c2e8227SGreg Roach * This program is distributed in the hope that it will be useful,
118c2e8227SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
128c2e8227SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
138c2e8227SGreg Roach * GNU General Public License for more details.
148c2e8227SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
168c2e8227SGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
2076692c8bSGreg Roachnamespace Fisharebest\Webtrees\Module;
2176692c8bSGreg Roach
226ccdf4f0SGreg Roachuse Fig\Http\Message\StatusCodeInterface;
23659aa375SGreg Roachuse Fisharebest\Webtrees\Auth;
246f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
25c5a1ffe6SGreg Roachuse Fisharebest\Webtrees\Family;
26a5f7ed67SGreg Roachuse Fisharebest\Webtrees\FlashMessages;
27a5f7ed67SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
28b1b85189SGreg Roachuse Fisharebest\Webtrees\Html;
2981b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException;
300e62c4b8SGreg Roachuse Fisharebest\Webtrees\I18N;
310e62c4b8SGreg Roachuse Fisharebest\Webtrees\Individual;
320e62c4b8SGreg Roachuse Fisharebest\Webtrees\Media;
330e62c4b8SGreg Roachuse Fisharebest\Webtrees\Note;
3481b729d3SGreg Roachuse Fisharebest\Webtrees\Registry;
350e62c4b8SGreg Roachuse Fisharebest\Webtrees\Repository;
363df1e584SGreg Roachuse Fisharebest\Webtrees\Services\TreeService;
370e62c4b8SGreg Roachuse Fisharebest\Webtrees\Source;
38c5a1ffe6SGreg Roachuse Fisharebest\Webtrees\Submitter;
390e62c4b8SGreg Roachuse Fisharebest\Webtrees\Tree;
40b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
41a69f5655SGreg Roachuse Illuminate\Database\Query\Expression;
42886b77daSGreg Roachuse Illuminate\Support\Collection;
436ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
446ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
4516a40a66SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
46d519210eSGreg Roach
4716a40a66SGreg Roachuse function date;
483df1e584SGreg Roachuse function redirect;
4916a40a66SGreg Roachuse function response;
5016a40a66SGreg Roachuse function route;
513df1e584SGreg Roachuse function view;
523df1e584SGreg Roach
538c2e8227SGreg Roach/**
548c2e8227SGreg Roach * Class SiteMapModule
558c2e8227SGreg Roach */
5616a40a66SGreg Roachclass SiteMapModule extends AbstractModule implements ModuleConfigInterface, RequestHandlerInterface
57c1010edaSGreg Roach{
5849a243cbSGreg Roach    use ModuleConfigTrait;
5949a243cbSGreg Roach
6016d6367aSGreg Roach    private const RECORDS_PER_VOLUME = 500; // Keep sitemap files small, for memory, CPU and max_allowed_packet limits.
610daf451eSGreg Roach    private const CACHE_LIFE         = 209600; // Two weeks
628c2e8227SGreg Roach
63c5a1ffe6SGreg Roach    private const PRIORITY = [
64c5a1ffe6SGreg Roach        Family::RECORD_TYPE     => 0.7,
65c5a1ffe6SGreg Roach        Individual::RECORD_TYPE => 0.9,
66c5a1ffe6SGreg Roach        Media::RECORD_TYPE      => 0.5,
67c5a1ffe6SGreg Roach        Note::RECORD_TYPE       => 0.3,
68c5a1ffe6SGreg Roach        Repository::RECORD_TYPE => 0.5,
69c5a1ffe6SGreg Roach        Source::RECORD_TYPE     => 0.5,
70c5a1ffe6SGreg Roach        Submitter::RECORD_TYPE  => 0.3,
71c5a1ffe6SGreg Roach    ];
72c5a1ffe6SGreg Roach
7343f2f523SGreg Roach    private TreeService $tree_service;
743df1e584SGreg Roach
753df1e584SGreg Roach    /**
763df1e584SGreg Roach     * @param TreeService $tree_service
773df1e584SGreg Roach     */
783df1e584SGreg Roach    public function __construct(TreeService $tree_service)
793df1e584SGreg Roach    {
803df1e584SGreg Roach        $this->tree_service = $tree_service;
813df1e584SGreg Roach    }
823df1e584SGreg Roach
83a5f7ed67SGreg Roach    /**
8416a40a66SGreg Roach     * Initialization.
8516a40a66SGreg Roach     *
8616a40a66SGreg Roach     * @return void
8716a40a66SGreg Roach     */
8816a40a66SGreg Roach    public function boot(): void
8916a40a66SGreg Roach    {
90158900c2SGreg Roach        Registry::routeFactory()->routeMap()
9114aabe72SGreg Roach            ->get('sitemap-style', '/sitemap.xsl', $this);
9214aabe72SGreg Roach
93158900c2SGreg Roach        Registry::routeFactory()->routeMap()
9416a40a66SGreg Roach            ->get('sitemap-index', '/sitemap.xml', $this);
9516a40a66SGreg Roach
96158900c2SGreg Roach        Registry::routeFactory()->routeMap()
97c5a1ffe6SGreg Roach            ->get('sitemap-file', '/sitemap-{tree}-{type}-{page}.xml', $this);
9816a40a66SGreg Roach    }
9916a40a66SGreg Roach
10049a243cbSGreg Roach    public function description(): string
101c1010edaSGreg Roach    {
102bbb76c12SGreg Roach        /* I18N: Description of the “Sitemaps” module */
103bbb76c12SGreg Roach        return I18N::translate('Generate sitemap files for search engines.');
1048c2e8227SGreg Roach    }
1058c2e8227SGreg Roach
10676692c8bSGreg Roach    /**
107abafa13cSGreg Roach     * Should this module be enabled when it is first installed?
108abafa13cSGreg Roach     *
109abafa13cSGreg Roach     * @return bool
110abafa13cSGreg Roach     */
111abafa13cSGreg Roach    public function isEnabledByDefault(): bool
112abafa13cSGreg Roach    {
113abafa13cSGreg Roach        return false;
114abafa13cSGreg Roach    }
115abafa13cSGreg Roach
116abafa13cSGreg Roach    /**
11757ab2231SGreg Roach     * @param ServerRequestInterface $request
11857ab2231SGreg Roach     *
1196ccdf4f0SGreg Roach     * @return ResponseInterface
1208c2e8227SGreg Roach     */
12149528f2bSGreg Roach    public function getAdminAction(ServerRequestInterface $request): ResponseInterface
122c1010edaSGreg Roach    {
123a5f7ed67SGreg Roach        $this->layout = 'layouts/administration';
124a5f7ed67SGreg Roach
12516a40a66SGreg Roach        $sitemap_url = route('sitemap-index');
126a5f7ed67SGreg Roach
127291c1b19SGreg Roach        return $this->viewResponse('modules/sitemap/config', [
1283df1e584SGreg Roach            'all_trees'   => $this->tree_service->all(),
129a5f7ed67SGreg Roach            'sitemap_url' => $sitemap_url,
13049a243cbSGreg Roach            'title'       => $this->title(),
131a5f7ed67SGreg Roach        ]);
1328c2e8227SGreg Roach    }
1338c2e8227SGreg Roach
1348c2e8227SGreg Roach    /**
1356ccdf4f0SGreg Roach     * How should this module be identified in the control panel, etc.?
136a5f7ed67SGreg Roach     *
1376ccdf4f0SGreg Roach     * @return string
1388c2e8227SGreg Roach     */
1396ccdf4f0SGreg Roach    public function title(): string
1406ccdf4f0SGreg Roach    {
141ad3143ccSGreg Roach        /* I18N: Name of a module - see https://en.wikipedia.org/wiki/Sitemaps */
1426ccdf4f0SGreg Roach        return I18N::translate('Sitemaps');
1436ccdf4f0SGreg Roach    }
1446ccdf4f0SGreg Roach
1456ccdf4f0SGreg Roach    /**
1466ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
1476ccdf4f0SGreg Roach     *
1486ccdf4f0SGreg Roach     * @return ResponseInterface
1496ccdf4f0SGreg Roach     */
1506ccdf4f0SGreg Roach    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
151c1010edaSGreg Roach    {
1523df1e584SGreg Roach        foreach ($this->tree_service->all() as $tree) {
153748dbe15SGreg Roach            $include_in_sitemap = Validator::parsedBody($request)->boolean('sitemap' . $tree->id(), false);
154a5f7ed67SGreg Roach            $tree->setPreference('include_in_sitemap', (string) $include_in_sitemap);
1558c2e8227SGreg Roach        }
156a5f7ed67SGreg Roach
15749a243cbSGreg Roach        FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success');
158a5f7ed67SGreg Roach
1596ccdf4f0SGreg Roach        return redirect($this->getConfigLink());
1608c2e8227SGreg Roach    }
1618c2e8227SGreg Roach
1628c2e8227SGreg Roach    /**
16357ab2231SGreg Roach     * @param ServerRequestInterface $request
16457ab2231SGreg Roach     *
1656ccdf4f0SGreg Roach     * @return ResponseInterface
1668c2e8227SGreg Roach     */
16716a40a66SGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
168c1010edaSGreg Roach    {
169b55cbc6bSGreg Roach        $route = Validator::attributes($request)->route();
170a5f7ed67SGreg Roach
17114aabe72SGreg Roach        if ($route->name === 'sitemap-style') {
17214aabe72SGreg Roach            $content = view('modules/sitemap/sitemap-xsl');
17314aabe72SGreg Roach
17414aabe72SGreg Roach            return response($content, StatusCodeInterface::STATUS_OK, [
1756172e7f6SGreg Roach                'content-type' => 'application/xml',
17614aabe72SGreg Roach            ]);
17714aabe72SGreg Roach        }
17814aabe72SGreg Roach
17916a40a66SGreg Roach        if ($route->name === 'sitemap-index') {
18016a40a66SGreg Roach            return $this->siteMapIndex($request);
18116a40a66SGreg Roach        }
18216a40a66SGreg Roach
18316a40a66SGreg Roach        return $this->siteMapFile($request);
18416a40a66SGreg Roach    }
18516a40a66SGreg Roach
18616a40a66SGreg Roach    /**
18716a40a66SGreg Roach     * @param ServerRequestInterface $request
18816a40a66SGreg Roach     *
18916a40a66SGreg Roach     * @return ResponseInterface
19016a40a66SGreg Roach     */
19149528f2bSGreg Roach    private function siteMapIndex(ServerRequestInterface $request): ResponseInterface
19216a40a66SGreg Roach    {
1936b9cb339SGreg Roach        $content = Registry::cache()->file()->remember('sitemap.xml', function (): string {
194659aa375SGreg Roach            // Which trees have sitemaps enabled?
195*f25fc0f9SGreg Roach            $tree_ids = $this->tree_service->all()
196*f25fc0f9SGreg Roach                ->filter(static fn (Tree $tree): bool => $tree->getPreference('include_in_sitemap') === '1')
197*f25fc0f9SGreg Roach                ->map(static fn (Tree $tree): int => $tree->id());
198659aa375SGreg Roach
199c5a1ffe6SGreg Roach            $count_families = DB::table('families')
200c5a1ffe6SGreg Roach                ->join('gedcom', 'f_file', '=', 'gedcom_id')
201c5a1ffe6SGreg Roach                ->whereIn('gedcom_id', $tree_ids)
202c5a1ffe6SGreg Roach                ->groupBy(['gedcom_id'])
203059898c9SGreg Roach                ->pluck(new Expression('COUNT(*) AS total'), 'gedcom_name');
204c5a1ffe6SGreg Roach
205fa17fb66SGreg Roach            $count_individuals = DB::table('individuals')
20616a40a66SGreg Roach                ->join('gedcom', 'i_file', '=', 'gedcom_id')
20716a40a66SGreg Roach                ->whereIn('gedcom_id', $tree_ids)
20816a40a66SGreg Roach                ->groupBy(['gedcom_id'])
209059898c9SGreg Roach                ->pluck(new Expression('COUNT(*) AS total'), 'gedcom_name');
210a5f7ed67SGreg Roach
211fa17fb66SGreg Roach            $count_media = DB::table('media')
21216a40a66SGreg Roach                ->join('gedcom', 'm_file', '=', 'gedcom_id')
21316a40a66SGreg Roach                ->whereIn('gedcom_id', $tree_ids)
21416a40a66SGreg Roach                ->groupBy(['gedcom_id'])
215059898c9SGreg Roach                ->pluck(new Expression('COUNT(*) AS total'), 'gedcom_name');
216a5f7ed67SGreg Roach
217fa17fb66SGreg Roach            $count_notes = DB::table('other')
21816a40a66SGreg Roach                ->join('gedcom', 'o_file', '=', 'gedcom_id')
21916a40a66SGreg Roach                ->whereIn('gedcom_id', $tree_ids)
220c5a1ffe6SGreg Roach                ->where('o_type', '=', Note::RECORD_TYPE)
22116a40a66SGreg Roach                ->groupBy(['gedcom_id'])
222059898c9SGreg Roach                ->pluck(new Expression('COUNT(*) AS total'), 'gedcom_name');
223a5f7ed67SGreg Roach
224fa17fb66SGreg Roach            $count_repositories = DB::table('other')
22516a40a66SGreg Roach                ->join('gedcom', 'o_file', '=', 'gedcom_id')
22616a40a66SGreg Roach                ->whereIn('gedcom_id', $tree_ids)
227c5a1ffe6SGreg Roach                ->where('o_type', '=', Repository::RECORD_TYPE)
22816a40a66SGreg Roach                ->groupBy(['gedcom_id'])
229059898c9SGreg Roach                ->pluck(new Expression('COUNT(*) AS total'), 'gedcom_name');
230a5f7ed67SGreg Roach
231fa17fb66SGreg Roach            $count_sources = DB::table('sources')
23216a40a66SGreg Roach                ->join('gedcom', 's_file', '=', 'gedcom_id')
23316a40a66SGreg Roach                ->whereIn('gedcom_id', $tree_ids)
23416a40a66SGreg Roach                ->groupBy(['gedcom_id'])
235059898c9SGreg Roach                ->pluck(new Expression('COUNT(*) AS total'), 'gedcom_name');
236a5f7ed67SGreg Roach
237c5a1ffe6SGreg Roach            $count_submitters = DB::table('other')
238c5a1ffe6SGreg Roach                ->join('gedcom', 'o_file', '=', 'gedcom_id')
239c5a1ffe6SGreg Roach                ->whereIn('gedcom_id', $tree_ids)
240c5a1ffe6SGreg Roach                ->where('o_type', '=', Submitter::RECORD_TYPE)
241c5a1ffe6SGreg Roach                ->groupBy(['gedcom_id'])
242059898c9SGreg Roach                ->pluck(new Expression('COUNT(*) AS total'), 'gedcom_name');
243c5a1ffe6SGreg Roach
24416a40a66SGreg Roach            // Versions 2.0.1 and earlier of this module stored large amounts of data in the settings.
24516a40a66SGreg Roach            DB::table('module_setting')
24616a40a66SGreg Roach                ->where('module_name', '=', $this->name())
24716a40a66SGreg Roach                ->delete();
24816a40a66SGreg Roach
24914aabe72SGreg Roach            return view('modules/sitemap/sitemap-index-xml', [
2503df1e584SGreg Roach                'all_trees'          => $this->tree_service->all(),
251c5a1ffe6SGreg Roach                'count_families'     => $count_families,
252a5f7ed67SGreg Roach                'count_individuals'  => $count_individuals,
253a5f7ed67SGreg Roach                'count_media'        => $count_media,
254a5f7ed67SGreg Roach                'count_notes'        => $count_notes,
255a5f7ed67SGreg Roach                'count_repositories' => $count_repositories,
256a5f7ed67SGreg Roach                'count_sources'      => $count_sources,
257c5a1ffe6SGreg Roach                'count_submitters'   => $count_submitters,
258a5f7ed67SGreg Roach                'last_mod'           => date('Y-m-d'),
259a5f7ed67SGreg Roach                'records_per_volume' => self::RECORDS_PER_VOLUME,
26014aabe72SGreg Roach                'sitemap_xsl'        => route('sitemap-style'),
261a5f7ed67SGreg Roach            ]);
26216a40a66SGreg Roach        }, self::CACHE_LIFE);
263a5f7ed67SGreg Roach
2646ccdf4f0SGreg Roach        return response($content, StatusCodeInterface::STATUS_OK, [
2656172e7f6SGreg Roach            'content-type' => 'application/xml',
266a5f7ed67SGreg Roach        ]);
267a5f7ed67SGreg Roach    }
268a5f7ed67SGreg Roach
269a5f7ed67SGreg Roach    /**
2706ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
271a5f7ed67SGreg Roach     *
2726ccdf4f0SGreg Roach     * @return ResponseInterface
273a5f7ed67SGreg Roach     */
27416a40a66SGreg Roach    private function siteMapFile(ServerRequestInterface $request): ResponseInterface
275c1010edaSGreg Roach    {
276b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->tree('tree');
277b55cbc6bSGreg Roach        $type = Validator::attributes($request)->string('type');
278b55cbc6bSGreg Roach        $page = Validator::attributes($request)->integer('page');
279a5f7ed67SGreg Roach
280659aa375SGreg Roach        if ($tree->getPreference('include_in_sitemap') !== '1') {
281659aa375SGreg Roach            throw new HttpNotFoundException();
282659aa375SGreg Roach        }
283659aa375SGreg Roach
284c5a1ffe6SGreg Roach        $cache_key = 'sitemap/' . $tree->id() . '/' . $type . '/' . $page . '.xml';
28516a40a66SGreg Roach
2866b9cb339SGreg Roach        $content = Registry::cache()->file()->remember($cache_key, function () use ($tree, $type, $page): string {
287c5a1ffe6SGreg Roach            $records = $this->sitemapRecords($tree, $type, self::RECORDS_PER_VOLUME, self::RECORDS_PER_VOLUME * $page);
28816a40a66SGreg Roach
28914aabe72SGreg Roach            return view('modules/sitemap/sitemap-file-xml', [
290c5a1ffe6SGreg Roach                'priority'    => self::PRIORITY[$type],
2919e3c7a99SGreg Roach                'records'     => $records,
29214aabe72SGreg Roach                'sitemap_xsl' => route('sitemap-style'),
2939e3c7a99SGreg Roach                'tree'        => $tree,
2949e3c7a99SGreg Roach            ]);
29516a40a66SGreg Roach        }, self::CACHE_LIFE);
296a5f7ed67SGreg Roach
2976ccdf4f0SGreg Roach        return response($content, StatusCodeInterface::STATUS_OK, [
2986172e7f6SGreg Roach            'content-type' => 'application/xml',
299a5f7ed67SGreg Roach        ]);
300a5f7ed67SGreg Roach    }
301a5f7ed67SGreg Roach
302a5f7ed67SGreg Roach    /**
303a5f7ed67SGreg Roach     * @param Tree   $tree
304a5f7ed67SGreg Roach     * @param string $type
305a5f7ed67SGreg Roach     * @param int    $limit
306a5f7ed67SGreg Roach     * @param int    $offset
307a5f7ed67SGreg Roach     *
30836779af1SGreg Roach     * @return Collection<int,GedcomRecord>
309a5f7ed67SGreg Roach     */
310886b77daSGreg Roach    private function sitemapRecords(Tree $tree, string $type, int $limit, int $offset): Collection
311c1010edaSGreg Roach    {
312a5f7ed67SGreg Roach        switch ($type) {
313c5a1ffe6SGreg Roach            case Family::RECORD_TYPE:
314c5a1ffe6SGreg Roach                $records = $this->sitemapFamilies($tree, $limit, $offset);
315c5a1ffe6SGreg Roach                break;
316c5a1ffe6SGreg Roach
31716a40a66SGreg Roach            case Individual::RECORD_TYPE:
318a5f7ed67SGreg Roach                $records = $this->sitemapIndividuals($tree, $limit, $offset);
319a5f7ed67SGreg Roach                break;
320a5f7ed67SGreg Roach
32116a40a66SGreg Roach            case Media::RECORD_TYPE:
322a5f7ed67SGreg Roach                $records = $this->sitemapMedia($tree, $limit, $offset);
323a5f7ed67SGreg Roach                break;
324a5f7ed67SGreg Roach
32516a40a66SGreg Roach            case Note::RECORD_TYPE:
326a5f7ed67SGreg Roach                $records = $this->sitemapNotes($tree, $limit, $offset);
327a5f7ed67SGreg Roach                break;
328a5f7ed67SGreg Roach
32916a40a66SGreg Roach            case Repository::RECORD_TYPE:
330a5f7ed67SGreg Roach                $records = $this->sitemapRepositories($tree, $limit, $offset);
331a5f7ed67SGreg Roach                break;
332a5f7ed67SGreg Roach
33316a40a66SGreg Roach            case Source::RECORD_TYPE:
334a5f7ed67SGreg Roach                $records = $this->sitemapSources($tree, $limit, $offset);
335a5f7ed67SGreg Roach                break;
336a5f7ed67SGreg Roach
337c5a1ffe6SGreg Roach            case Submitter::RECORD_TYPE:
338c5a1ffe6SGreg Roach                $records = $this->sitemapSubmitters($tree, $limit, $offset);
339c5a1ffe6SGreg Roach                break;
340c5a1ffe6SGreg Roach
341a5f7ed67SGreg Roach            default:
342d501c45dSGreg Roach                throw new HttpNotFoundException('Invalid record type: ' . $type);
343a5f7ed67SGreg Roach        }
344a5f7ed67SGreg Roach
345a5f7ed67SGreg Roach        // Skip private records.
346*f25fc0f9SGreg Roach        $records = $records->filter(static fn (GedcomRecord $record): bool => $record->canShow(Auth::PRIV_PRIVATE));
347a5f7ed67SGreg Roach
348a5f7ed67SGreg Roach        return $records;
349a5f7ed67SGreg Roach    }
350a5f7ed67SGreg Roach
351a5f7ed67SGreg Roach    /**
352a5f7ed67SGreg Roach     * @param Tree $tree
353a5f7ed67SGreg Roach     * @param int  $limit
354a5f7ed67SGreg Roach     * @param int  $offset
355a5f7ed67SGreg Roach     *
35636779af1SGreg Roach     * @return Collection<int,Family>
357c5a1ffe6SGreg Roach     */
358c5a1ffe6SGreg Roach    private function sitemapFamilies(Tree $tree, int $limit, int $offset): Collection
359c5a1ffe6SGreg Roach    {
360c5a1ffe6SGreg Roach        return DB::table('families')
361c5a1ffe6SGreg Roach            ->where('f_file', '=', $tree->id())
362c5a1ffe6SGreg Roach            ->orderBy('f_id')
363c5a1ffe6SGreg Roach            ->skip($offset)
364c5a1ffe6SGreg Roach            ->take($limit)
365c5a1ffe6SGreg Roach            ->get()
3666b9cb339SGreg Roach            ->map(Registry::familyFactory()->mapper($tree));
367c5a1ffe6SGreg Roach    }
368c5a1ffe6SGreg Roach
369c5a1ffe6SGreg Roach    /**
370c5a1ffe6SGreg Roach     * @param Tree $tree
371c5a1ffe6SGreg Roach     * @param int  $limit
372c5a1ffe6SGreg Roach     * @param int  $offset
373c5a1ffe6SGreg Roach     *
37436779af1SGreg Roach     * @return Collection<int,Individual>
375a5f7ed67SGreg Roach     */
376886b77daSGreg Roach    private function sitemapIndividuals(Tree $tree, int $limit, int $offset): Collection
377c1010edaSGreg Roach    {
378886b77daSGreg Roach        return DB::table('individuals')
379fa17fb66SGreg Roach            ->where('i_file', '=', $tree->id())
380fa17fb66SGreg Roach            ->orderBy('i_id')
381fa17fb66SGreg Roach            ->skip($offset)
382fa17fb66SGreg Roach            ->take($limit)
383886b77daSGreg Roach            ->get()
3846b9cb339SGreg Roach            ->map(Registry::individualFactory()->mapper($tree));
3858c2e8227SGreg Roach    }
386a5f7ed67SGreg Roach
387a5f7ed67SGreg Roach    /**
388a5f7ed67SGreg Roach     * @param Tree $tree
389a5f7ed67SGreg Roach     * @param int  $limit
390a5f7ed67SGreg Roach     * @param int  $offset
391a5f7ed67SGreg Roach     *
39236779af1SGreg Roach     * @return Collection<int,Media>
393a5f7ed67SGreg Roach     */
394886b77daSGreg Roach    private function sitemapMedia(Tree $tree, int $limit, int $offset): Collection
395c1010edaSGreg Roach    {
396886b77daSGreg Roach        return DB::table('media')
397fa17fb66SGreg Roach            ->where('m_file', '=', $tree->id())
398fa17fb66SGreg Roach            ->orderBy('m_id')
399fa17fb66SGreg Roach            ->skip($offset)
400fa17fb66SGreg Roach            ->take($limit)
401886b77daSGreg Roach            ->get()
4026b9cb339SGreg Roach            ->map(Registry::mediaFactory()->mapper($tree));
4038c2e8227SGreg Roach    }
4048c2e8227SGreg Roach
4058c2e8227SGreg Roach    /**
406a5f7ed67SGreg Roach     * @param Tree $tree
407a5f7ed67SGreg Roach     * @param int  $limit
408a5f7ed67SGreg Roach     * @param int  $offset
409a5f7ed67SGreg Roach     *
41036779af1SGreg Roach     * @return Collection<int,Note>
4118c2e8227SGreg Roach     */
412886b77daSGreg Roach    private function sitemapNotes(Tree $tree, int $limit, int $offset): Collection
413c1010edaSGreg Roach    {
414886b77daSGreg Roach        return DB::table('other')
415fa17fb66SGreg Roach            ->where('o_file', '=', $tree->id())
416c5a1ffe6SGreg Roach            ->where('o_type', '=', Note::RECORD_TYPE)
417fa17fb66SGreg Roach            ->orderBy('o_id')
418fa17fb66SGreg Roach            ->skip($offset)
419fa17fb66SGreg Roach            ->take($limit)
420886b77daSGreg Roach            ->get()
4216b9cb339SGreg Roach            ->map(Registry::noteFactory()->mapper($tree));
4228c2e8227SGreg Roach    }
4238c2e8227SGreg Roach
424a5f7ed67SGreg Roach    /**
425a5f7ed67SGreg Roach     * @param Tree $tree
426a5f7ed67SGreg Roach     * @param int  $limit
427a5f7ed67SGreg Roach     * @param int  $offset
428a5f7ed67SGreg Roach     *
42936779af1SGreg Roach     * @return Collection<int,Repository>
430a5f7ed67SGreg Roach     */
431886b77daSGreg Roach    private function sitemapRepositories(Tree $tree, int $limit, int $offset): Collection
432c1010edaSGreg Roach    {
433886b77daSGreg Roach        return DB::table('other')
434fa17fb66SGreg Roach            ->where('o_file', '=', $tree->id())
435c5a1ffe6SGreg Roach            ->where('o_type', '=', Repository::RECORD_TYPE)
436fa17fb66SGreg Roach            ->orderBy('o_id')
437fa17fb66SGreg Roach            ->skip($offset)
438fa17fb66SGreg Roach            ->take($limit)
439886b77daSGreg Roach            ->get()
4406b9cb339SGreg Roach            ->map(Registry::repositoryFactory()->mapper($tree));
441a5f7ed67SGreg Roach    }
442a5f7ed67SGreg Roach
443a5f7ed67SGreg Roach    /**
444a5f7ed67SGreg Roach     * @param Tree $tree
445a5f7ed67SGreg Roach     * @param int  $limit
446a5f7ed67SGreg Roach     * @param int  $offset
447a5f7ed67SGreg Roach     *
44836779af1SGreg Roach     * @return Collection<int,Source>
449a5f7ed67SGreg Roach     */
450886b77daSGreg Roach    private function sitemapSources(Tree $tree, int $limit, int $offset): Collection
451c1010edaSGreg Roach    {
452886b77daSGreg Roach        return DB::table('sources')
453fa17fb66SGreg Roach            ->where('s_file', '=', $tree->id())
454fa17fb66SGreg Roach            ->orderBy('s_id')
455fa17fb66SGreg Roach            ->skip($offset)
456fa17fb66SGreg Roach            ->take($limit)
457886b77daSGreg Roach            ->get()
4586b9cb339SGreg Roach            ->map(Registry::sourceFactory()->mapper($tree));
4598c2e8227SGreg Roach    }
460c5a1ffe6SGreg Roach
461c5a1ffe6SGreg Roach    /**
462c5a1ffe6SGreg Roach     * @param Tree $tree
463c5a1ffe6SGreg Roach     * @param int  $limit
464c5a1ffe6SGreg Roach     * @param int  $offset
465c5a1ffe6SGreg Roach     *
46636779af1SGreg Roach     * @return Collection<int,Submitter>
467c5a1ffe6SGreg Roach     */
468c5a1ffe6SGreg Roach    private function sitemapSubmitters(Tree $tree, int $limit, int $offset): Collection
469c5a1ffe6SGreg Roach    {
470c5a1ffe6SGreg Roach        return DB::table('other')
471c5a1ffe6SGreg Roach            ->where('o_file', '=', $tree->id())
472c5a1ffe6SGreg Roach            ->where('o_type', '=', Submitter::RECORD_TYPE)
473c5a1ffe6SGreg Roach            ->orderBy('o_id')
474c5a1ffe6SGreg Roach            ->skip($offset)
475c5a1ffe6SGreg Roach            ->take($limit)
476c5a1ffe6SGreg Roach            ->get()
4776b9cb339SGreg Roach            ->map(Registry::submitterFactory()->mapper($tree));
478c5a1ffe6SGreg Roach    }
4798c2e8227SGreg Roach}
480