Passed
Push — master ( 9e3c7a...14aabe )
by Greg
06:13
created

SiteMapModule::sitemapRepositories()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 3
dl 0
loc 10
rs 10
c 0
b 0
f 0
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\FlashMessages;
29
use Fisharebest\Webtrees\GedcomRecord;
30
use Fisharebest\Webtrees\Html;
31
use Fisharebest\Webtrees\I18N;
32
use Fisharebest\Webtrees\Individual;
33
use Fisharebest\Webtrees\Media;
34
use Fisharebest\Webtrees\Note;
35
use Fisharebest\Webtrees\Repository;
36
use Fisharebest\Webtrees\Services\TreeService;
37
use Fisharebest\Webtrees\Source;
38
use Fisharebest\Webtrees\Tree;
39
use Illuminate\Database\Capsule\Manager as DB;
40
use Illuminate\Database\Query\Expression;
41
use Illuminate\Support\Collection;
42
use Psr\Http\Message\ResponseInterface;
43
use Psr\Http\Message\ServerRequestInterface;
44
use Psr\Http\Server\RequestHandlerInterface;
45
use function app;
46
use function assert;
47
use function date;
48
use function redirect;
49
use function response;
50
use function route;
51
use function view;
52
53
/**
54
 * Class SiteMapModule
55
 */
56
class SiteMapModule extends AbstractModule implements ModuleConfigInterface, RequestHandlerInterface
57
{
58
    use ModuleConfigTrait;
59
60
    private const RECORDS_PER_VOLUME = 500; // Keep sitemap files small, for memory, CPU and max_allowed_packet limits.
61
    private const CACHE_LIFE         = 1;//209600; // Two weeks
62
63
    /** @var TreeService */
64
    private $tree_service;
65
66
    /**
67
     * TreesMenuModule constructor.
68
     *
69
     * @param TreeService $tree_service
70
     */
71
    public function __construct(TreeService $tree_service)
72
    {
73
        $this->tree_service = $tree_service;
74
    }
75
76
    /**
77
     * Initialization.
78
     *
79
     * @return void
80
     */
81
    public function boot(): void
82
    {
83
        $router_container = app(RouterContainer::class);
84
        assert($router_container instanceof RouterContainer);
85
86
        $router_container->getMap()
87
            ->get('sitemap-style', '/sitemap.xsl', $this);
88
89
        $router_container->getMap()
90
            ->get('sitemap-index', '/sitemap.xml', $this);
91
92
        $router_container->getMap()
93
            ->get('sitemap-file', '/sitemap-{tree}-{records}-{page}.xml', $this)
94
            ->tokens([
95
                'records' => 'INDI|NOTE|OBJE|REPO|SOUR',
96
                'page'    => '\d+',
97
            ]);
98
    }
99
100
    /**
101
     * A sentence describing what this module does.
102
     *
103
     * @return string
104
     */
105
    public function description(): string
106
    {
107
        /* I18N: Description of the “Sitemaps” module */
108
        return I18N::translate('Generate sitemap files for search engines.');
109
    }
110
111
    /**
112
     * Should this module be enabled when it is first installed?
113
     *
114
     * @return bool
115
     */
116
    public function isEnabledByDefault(): bool
117
    {
118
        return false;
119
    }
120
121
    /**
122
     * @param ServerRequestInterface $request
123
     *
124
     * @return ResponseInterface
125
     */
126
    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

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

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