Passed
Push — master ( 74670d...1e1ace )
by Greg
05:45
created

SiteMapModule::sitemapFamilies()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 9
rs 10
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Aura\Router\Route;
23
use Aura\Router\RouterContainer;
24
use Fig\Http\Message\StatusCodeInterface;
25
use Fisharebest\Webtrees\Auth;
26
use Fisharebest\Webtrees\Cache;
27
use Fisharebest\Webtrees\Exceptions\HttpNotFoundException;
28
use Fisharebest\Webtrees\Family;
29
use Fisharebest\Webtrees\FlashMessages;
30
use Fisharebest\Webtrees\GedcomRecord;
31
use Fisharebest\Webtrees\Html;
32
use Fisharebest\Webtrees\I18N;
33
use Fisharebest\Webtrees\Individual;
34
use Fisharebest\Webtrees\Media;
35
use Fisharebest\Webtrees\Note;
36
use Fisharebest\Webtrees\Repository;
37
use Fisharebest\Webtrees\Services\TreeService;
38
use Fisharebest\Webtrees\Source;
39
use Fisharebest\Webtrees\Submitter;
40
use Fisharebest\Webtrees\Tree;
41
use Illuminate\Database\Capsule\Manager as DB;
42
use Illuminate\Database\Query\Expression;
43
use Illuminate\Support\Collection;
44
use Psr\Http\Message\ResponseInterface;
45
use Psr\Http\Message\ServerRequestInterface;
46
use Psr\Http\Server\RequestHandlerInterface;
47
48
use function app;
49
use function assert;
50
use function date;
51
use function redirect;
52
use function response;
53
use function route;
54
use function view;
55
56
/**
57
 * Class SiteMapModule
58
 */
59
class SiteMapModule extends AbstractModule implements ModuleConfigInterface, RequestHandlerInterface
60
{
61
    use ModuleConfigTrait;
62
63
    private const RECORDS_PER_VOLUME = 500; // Keep sitemap files small, for memory, CPU and max_allowed_packet limits.
64
    private const CACHE_LIFE         = 209600; // Two weeks
65
66
    private const PRIORITY = [
67
        Family::RECORD_TYPE     => 0.7,
68
        Individual::RECORD_TYPE => 0.9,
69
        Media::RECORD_TYPE      => 0.5,
70
        Note::RECORD_TYPE       => 0.3,
71
        Repository::RECORD_TYPE => 0.5,
72
        Source::RECORD_TYPE     => 0.5,
73
        Submitter::RECORD_TYPE  => 0.3,
74
    ];
75
76
    /** @var TreeService */
77
    private $tree_service;
78
79
    /**
80
     * TreesMenuModule constructor.
81
     *
82
     * @param TreeService $tree_service
83
     */
84
    public function __construct(TreeService $tree_service)
85
    {
86
        $this->tree_service = $tree_service;
87
    }
88
89
    /**
90
     * Initialization.
91
     *
92
     * @return void
93
     */
94
    public function boot(): void
95
    {
96
        $router_container = app(RouterContainer::class);
97
        assert($router_container instanceof RouterContainer);
98
99
        $router_container->getMap()
100
            ->get('sitemap-style', '/sitemap.xsl', $this);
101
102
        $router_container->getMap()
103
            ->get('sitemap-index', '/sitemap.xml', $this);
104
105
        $router_container->getMap()
106
            ->get('sitemap-file', '/sitemap-{tree}-{type}-{page}.xml', $this);
107
    }
108
109
    /**
110
     * A sentence describing what this module does.
111
     *
112
     * @return string
113
     */
114
    public function description(): string
115
    {
116
        /* I18N: Description of the “Sitemaps” module */
117
        return I18N::translate('Generate sitemap files for search engines.');
118
    }
119
120
    /**
121
     * Should this module be enabled when it is first installed?
122
     *
123
     * @return bool
124
     */
125
    public function isEnabledByDefault(): bool
126
    {
127
        return false;
128
    }
129
130
    /**
131
     * @param ServerRequestInterface $request
132
     *
133
     * @return ResponseInterface
134
     */
135
    public function getAdminAction(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

135
    public function getAdminAction(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
136
    {
137
        $this->layout = 'layouts/administration';
138
139
        $sitemap_url = route('sitemap-index');
140
141
        // This list comes from https://en.wikipedia.org/wiki/Sitemaps
142
        $submit_urls = [
143
            'Bing/Yahoo' => Html::url('https://www.bing.com/webmaster/ping.aspx', ['siteMap' => $sitemap_url]),
144
            'Google'     => Html::url('https://www.google.com/webmasters/tools/ping', ['sitemap' => $sitemap_url]),
145
        ];
146
147
        return $this->viewResponse('modules/sitemap/config', [
148
            'all_trees'   => $this->tree_service->all(),
149
            'sitemap_url' => $sitemap_url,
150
            'submit_urls' => $submit_urls,
151
            'title'       => $this->title(),
152
        ]);
153
    }
154
155
    /**
156
     * How should this module be identified in the control panel, etc.?
157
     *
158
     * @return string
159
     */
160
    public function title(): string
161
    {
162
        /* I18N: Name of a module - see http://en.wikipedia.org/wiki/Sitemaps */
163
        return I18N::translate('Sitemaps');
164
    }
165
166
    /**
167
     * @param ServerRequestInterface $request
168
     *
169
     * @return ResponseInterface
170
     */
171
    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
172
    {
173
        $params = (array) $request->getParsedBody();
174
175
        foreach ($this->tree_service->all() as $tree) {
176
            $include_in_sitemap = (bool) ($params['sitemap' . $tree->id()] ?? false);
177
            $tree->setPreference('include_in_sitemap', (string) $include_in_sitemap);
178
        }
179
180
        FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success');
181
182
        return redirect($this->getConfigLink());
183
    }
184
185
    /**
186
     * @param ServerRequestInterface $request
187
     *
188
     * @return ResponseInterface
189
     */
190
    public function handle(ServerRequestInterface $request): ResponseInterface
191
    {
192
        $route = $request->getAttribute('route');
193
        assert($route instanceof Route);
194
195
        if ($route->name === 'sitemap-style') {
196
            $content = view('modules/sitemap/sitemap-xsl');
197
198
            return response($content, StatusCodeInterface::STATUS_OK, [
199
                'Content-Type' => 'application/xml',
200
            ]);
201
        }
202
203
        if ($route->name === 'sitemap-index') {
204
            return $this->siteMapIndex($request);
205
        }
206
207
        return $this->siteMapFile($request);
208
    }
209
210
    /**
211
     * @param ServerRequestInterface $request
212
     *
213
     * @return ResponseInterface
214
     */
215
    private function siteMapIndex(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

215
    private function siteMapIndex(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
216
    {
217
        $cache = app('cache.files');
218
        assert($cache instanceof Cache);
219
220
        $content = $cache->remember('sitemap.xml', function (): string {
221
            // Which trees have sitemaps enabled?
222
            $tree_ids = $this->tree_service->all()->filter(static function (Tree $tree): bool {
223
                return $tree->getPreference('include_in_sitemap') === '1';
224
            })->map(static function (Tree $tree): int {
225
                return $tree->id();
226
            });
227
228
            $count_families = DB::table('families')
229
                ->join('gedcom', 'f_file', '=', 'gedcom_id')
230
                ->whereIn('gedcom_id', $tree_ids)
231
                ->groupBy(['gedcom_id'])
232
                ->select([new Expression('COUNT(*) AS total'), 'gedcom_name'])
233
                ->pluck('total', 'gedcom_name');
234
235
            $count_individuals = DB::table('individuals')
236
                ->join('gedcom', 'i_file', '=', 'gedcom_id')
237
                ->whereIn('gedcom_id', $tree_ids)
238
                ->groupBy(['gedcom_id'])
239
                ->select([new Expression('COUNT(*) AS total'), 'gedcom_name'])
240
                ->pluck('total', 'gedcom_name');
241
242
            $count_media = DB::table('media')
243
                ->join('gedcom', 'm_file', '=', 'gedcom_id')
244
                ->whereIn('gedcom_id', $tree_ids)
245
                ->groupBy(['gedcom_id'])
246
                ->select([new Expression('COUNT(*) AS total'), 'gedcom_name'])
247
                ->pluck('total', 'gedcom_name');
248
249
            $count_notes = DB::table('other')
250
                ->join('gedcom', 'o_file', '=', 'gedcom_id')
251
                ->whereIn('gedcom_id', $tree_ids)
252
                ->where('o_type', '=', Note::RECORD_TYPE)
253
                ->groupBy(['gedcom_id'])
254
                ->select([new Expression('COUNT(*) AS total'), 'gedcom_name'])
255
                ->pluck('total', 'gedcom_name');
256
257
            $count_repositories = DB::table('other')
258
                ->join('gedcom', 'o_file', '=', 'gedcom_id')
259
                ->whereIn('gedcom_id', $tree_ids)
260
                ->where('o_type', '=', Repository::RECORD_TYPE)
261
                ->groupBy(['gedcom_id'])
262
                ->select([new Expression('COUNT(*) AS total'), 'gedcom_name'])
263
                ->pluck('total', 'gedcom_name');
264
265
            $count_sources = DB::table('sources')
266
                ->join('gedcom', 's_file', '=', 'gedcom_id')
267
                ->whereIn('gedcom_id', $tree_ids)
268
                ->groupBy(['gedcom_id'])
269
                ->select([new Expression('COUNT(*) AS total'), 'gedcom_name'])
270
                ->pluck('total', 'gedcom_name');
271
272
            $count_submitters = DB::table('other')
273
                ->join('gedcom', 'o_file', '=', 'gedcom_id')
274
                ->whereIn('gedcom_id', $tree_ids)
275
                ->where('o_type', '=', Submitter::RECORD_TYPE)
276
                ->groupBy(['gedcom_id'])
277
                ->select([new Expression('COUNT(*) AS total'), 'gedcom_name'])
278
                ->pluck('total', 'gedcom_name');
279
280
            // Versions 2.0.1 and earlier of this module stored large amounts of data in the settings.
281
            DB::table('module_setting')
282
                ->where('module_name', '=', $this->name())
283
                ->delete();
284
285
            return view('modules/sitemap/sitemap-index-xml', [
286
                'all_trees'          => $this->tree_service->all(),
287
                'count_families'     => $count_families,
288
                'count_individuals'  => $count_individuals,
289
                'count_media'        => $count_media,
290
                'count_notes'        => $count_notes,
291
                'count_repositories' => $count_repositories,
292
                'count_sources'      => $count_sources,
293
                'count_submitters'   => $count_submitters,
294
                'last_mod'           => date('Y-m-d'),
295
                'records_per_volume' => self::RECORDS_PER_VOLUME,
296
                'sitemap_xsl'        => route('sitemap-style'),
297
            ]);
298
        }, self::CACHE_LIFE);
299
300
        return response($content, StatusCodeInterface::STATUS_OK, [
301
            'Content-Type' => 'application/xml',
302
        ]);
303
    }
304
305
    /**
306
     * @param ServerRequestInterface $request
307
     *
308
     * @return ResponseInterface
309
     */
310
    private function siteMapFile(ServerRequestInterface $request): ResponseInterface
311
    {
312
        $tree = $request->getAttribute('tree');
313
        assert($tree instanceof Tree);
314
315
        $type = $request->getAttribute('type');
316
        $page = (int) $request->getAttribute('page');
317
318
        if ($tree->getPreference('include_in_sitemap') !== '1') {
319
            throw new HttpNotFoundException();
320
        }
321
322
        $cache = app('cache.files');
323
        assert($cache instanceof Cache);
324
325
        $cache_key = 'sitemap/' . $tree->id() . '/' . $type . '/' . $page . '.xml';
326
327
        $content = $cache->remember($cache_key, function () use ($tree, $type, $page): string {
328
            $records = $this->sitemapRecords($tree, $type, self::RECORDS_PER_VOLUME, self::RECORDS_PER_VOLUME * $page);
329
330
            return view('modules/sitemap/sitemap-file-xml', [
331
                'priority'    => self::PRIORITY[$type],
332
                'records'     => $records,
333
                'sitemap_xsl' => route('sitemap-style'),
334
                'tree'        => $tree,
335
            ]);
336
        }, self::CACHE_LIFE);
337
338
        return response($content, StatusCodeInterface::STATUS_OK, [
339
            'Content-Type' => 'application/xml',
340
        ]);
341
    }
342
343
    /**
344
     * @param Tree   $tree
345
     * @param string $type
346
     * @param int    $limit
347
     * @param int    $offset
348
     *
349
     * @return Collection<GedcomRecord>
350
     */
351
    private function sitemapRecords(Tree $tree, string $type, int $limit, int $offset): Collection
352
    {
353
        switch ($type) {
354
            case Family::RECORD_TYPE:
355
                $records = $this->sitemapFamilies($tree, $limit, $offset);
356
                break;
357
358
            case Individual::RECORD_TYPE:
359
                $records = $this->sitemapIndividuals($tree, $limit, $offset);
360
                break;
361
362
            case Media::RECORD_TYPE:
363
                $records = $this->sitemapMedia($tree, $limit, $offset);
364
                break;
365
366
            case Note::RECORD_TYPE:
367
                $records = $this->sitemapNotes($tree, $limit, $offset);
368
                break;
369
370
            case Repository::RECORD_TYPE:
371
                $records = $this->sitemapRepositories($tree, $limit, $offset);
372
                break;
373
374
            case Source::RECORD_TYPE:
375
                $records = $this->sitemapSources($tree, $limit, $offset);
376
                break;
377
378
            case Submitter::RECORD_TYPE:
379
                $records = $this->sitemapSubmitters($tree, $limit, $offset);
380
                break;
381
382
            default:
383
                throw new HttpNotFoundException('Invalid record type: ' . $type);
384
        }
385
386
        // Skip private records.
387
        $records = $records->filter(static function (GedcomRecord $record): bool {
388
            return $record->canShow(Auth::PRIV_PRIVATE);
389
        });
390
391
        return $records;
392
    }
393
394
    /**
395
     * @param Tree $tree
396
     * @param int  $limit
397
     * @param int  $offset
398
     *
399
     * @return Collection<Family>
400
     */
401
    private function sitemapFamilies(Tree $tree, int $limit, int $offset): Collection
402
    {
403
        return DB::table('families')
404
            ->where('f_file', '=', $tree->id())
405
            ->orderBy('f_id')
406
            ->skip($offset)
407
            ->take($limit)
408
            ->get()
409
            ->map(Family::rowMapper($tree));
410
    }
411
412
    /**
413
     * @param Tree $tree
414
     * @param int  $limit
415
     * @param int  $offset
416
     *
417
     * @return Collection<Individual>
418
     */
419
    private function sitemapIndividuals(Tree $tree, int $limit, int $offset): Collection
420
    {
421
        return DB::table('individuals')
422
            ->where('i_file', '=', $tree->id())
423
            ->orderBy('i_id')
424
            ->skip($offset)
425
            ->take($limit)
426
            ->get()
427
            ->map(Individual::rowMapper($tree));
428
    }
429
430
    /**
431
     * @param Tree $tree
432
     * @param int  $limit
433
     * @param int  $offset
434
     *
435
     * @return Collection<Media>
436
     */
437
    private function sitemapMedia(Tree $tree, int $limit, int $offset): Collection
438
    {
439
        return DB::table('media')
440
            ->where('m_file', '=', $tree->id())
441
            ->orderBy('m_id')
442
            ->skip($offset)
443
            ->take($limit)
444
            ->get()
445
            ->map(Media::rowMapper($tree));
446
    }
447
448
    /**
449
     * @param Tree $tree
450
     * @param int  $limit
451
     * @param int  $offset
452
     *
453
     * @return Collection<Note>
454
     */
455
    private function sitemapNotes(Tree $tree, int $limit, int $offset): Collection
456
    {
457
        return DB::table('other')
458
            ->where('o_file', '=', $tree->id())
459
            ->where('o_type', '=', Note::RECORD_TYPE)
460
            ->orderBy('o_id')
461
            ->skip($offset)
462
            ->take($limit)
463
            ->get()
464
            ->map(Note::rowMapper($tree));
465
    }
466
467
    /**
468
     * @param Tree $tree
469
     * @param int  $limit
470
     * @param int  $offset
471
     *
472
     * @return Collection<Repository>
473
     */
474
    private function sitemapRepositories(Tree $tree, int $limit, int $offset): Collection
475
    {
476
        return DB::table('other')
477
            ->where('o_file', '=', $tree->id())
478
            ->where('o_type', '=', Repository::RECORD_TYPE)
479
            ->orderBy('o_id')
480
            ->skip($offset)
481
            ->take($limit)
482
            ->get()
483
            ->map(Repository::rowMapper($tree));
484
    }
485
486
    /**
487
     * @param Tree $tree
488
     * @param int  $limit
489
     * @param int  $offset
490
     *
491
     * @return Collection<Source>
492
     */
493
    private function sitemapSources(Tree $tree, int $limit, int $offset): Collection
494
    {
495
        return DB::table('sources')
496
            ->where('s_file', '=', $tree->id())
497
            ->orderBy('s_id')
498
            ->skip($offset)
499
            ->take($limit)
500
            ->get()
501
            ->map(Source::rowMapper($tree));
502
    }
503
504
    /**
505
     * @param Tree $tree
506
     * @param int  $limit
507
     * @param int  $offset
508
     *
509
     * @return Collection<Submitter>
510
     */
511
    private function sitemapSubmitters(Tree $tree, int $limit, int $offset): Collection
512
    {
513
        return DB::table('other')
514
            ->where('o_file', '=', $tree->id())
515
            ->where('o_type', '=', Submitter::RECORD_TYPE)
516
            ->orderBy('o_id')
517
            ->skip($offset)
518
            ->take($limit)
519
            ->get()
520
            ->map(Submitter::rowMapper($tree));
521
    }
522
}
523