Passed
Push — master ( b9de05...5e23c3 )
by Greg
06:49
created

SiteMapModule::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
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 Fig\Http\Message\StatusCodeInterface;
23
use Fisharebest\Webtrees\Carbon;
24
use Fisharebest\Webtrees\FlashMessages;
25
use Fisharebest\Webtrees\GedcomRecord;
26
use Fisharebest\Webtrees\Html;
27
use Fisharebest\Webtrees\I18N;
28
use Fisharebest\Webtrees\Individual;
29
use Fisharebest\Webtrees\Media;
30
use Fisharebest\Webtrees\Note;
31
use Fisharebest\Webtrees\Repository;
32
use Fisharebest\Webtrees\Services\TreeService;
33
use Fisharebest\Webtrees\Source;
34
use Fisharebest\Webtrees\Tree;
35
use Illuminate\Database\Capsule\Manager as DB;
36
use Illuminate\Database\Query\Expression;
37
use Illuminate\Support\Collection;
38
use Psr\Http\Message\ResponseInterface;
39
use Psr\Http\Message\ServerRequestInterface;
40
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
41
42
use function redirect;
43
use function view;
44
45
/**
46
 * Class SiteMapModule
47
 */
48
class SiteMapModule extends AbstractModule implements ModuleConfigInterface
49
{
50
    use ModuleConfigTrait;
51
52
    private const RECORDS_PER_VOLUME = 500; // Keep sitemap files small, for memory, CPU and max_allowed_packet limits.
53
    private const CACHE_LIFE         = 1209600; // Two weeks
54
55
    /** @var TreeService */
56
    private $tree_service;
57
58
    /**
59
     * TreesMenuModule constructor.
60
     *
61
     * @param TreeService $tree_service
62
     */
63
    public function __construct(TreeService $tree_service)
64
    {
65
        $this->tree_service = $tree_service;
66
    }
67
68
    /**
69
     * A sentence describing what this module does.
70
     *
71
     * @return string
72
     */
73
    public function description(): string
74
    {
75
        /* I18N: Description of the “Sitemaps” module */
76
        return I18N::translate('Generate sitemap files for search engines.');
77
    }
78
79
    /**
80
     * Should this module be enabled when it is first installed?
81
     *
82
     * @return bool
83
     */
84
    public function isEnabledByDefault(): bool
85
    {
86
        return false;
87
    }
88
89
    /**
90
     * @param ServerRequestInterface $request
91
     *
92
     * @return ResponseInterface
93
     */
94
    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

94
    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...
95
    {
96
        $this->layout = 'layouts/administration';
97
98
        $sitemap_url = route('module', [
99
            'module' => $this->name(),
100
            'action' => 'Index',
101
        ]);
102
103
        // This list comes from http://en.wikipedia.org/wiki/Sitemaps
104
        $submit_urls = [
105
            'Bing/Yahoo' => Html::url('https://www.bing.com/webmaster/ping.aspx', ['siteMap' => $sitemap_url]),
106
            'Google'     => Html::url('https://www.google.com/webmasters/tools/ping', ['sitemap' => $sitemap_url]),
107
        ];
108
109
        return $this->viewResponse('modules/sitemap/config', [
110
            'all_trees'   => $this->tree_service->all(),
111
            'sitemap_url' => $sitemap_url,
112
            'submit_urls' => $submit_urls,
113
            'title'       => $this->title(),
114
        ]);
115
    }
116
117
    /**
118
     * How should this module be identified in the control panel, etc.?
119
     *
120
     * @return string
121
     */
122
    public function title(): string
123
    {
124
        /* I18N: Name of a module - see http://en.wikipedia.org/wiki/Sitemaps */
125
        return I18N::translate('Sitemaps');
126
    }
127
128
    /**
129
     * @param ServerRequestInterface $request
130
     *
131
     * @return ResponseInterface
132
     */
133
    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
134
    {
135
        $params = $request->getParsedBody();
136
137
        foreach ($this->tree_service->all() as $tree) {
138
            $include_in_sitemap = (bool) ($params['sitemap' . $tree->id()] ?? false);
139
            $tree->setPreference('include_in_sitemap', (string) $include_in_sitemap);
140
        }
141
142
        FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success');
143
144
        return redirect($this->getConfigLink());
145
    }
146
147
    /**
148
     * @param ServerRequestInterface $request
149
     *
150
     * @return ResponseInterface
151
     */
152
    public function getIndexAction(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

152
    public function getIndexAction(/** @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...
153
    {
154
        $timestamp = (int) $this->getPreference('sitemap.timestamp');
155
156
        if ($timestamp > Carbon::now()->subSeconds(self::CACHE_LIFE)->unix()) {
157
            $content = $this->getPreference('sitemap.xml');
158
        } else {
159
            $count_individuals = DB::table('individuals')
160
                ->groupBy(['i_file'])
161
                ->select([new Expression('COUNT(*) AS total'), 'i_file'])
162
                ->pluck('total', 'i_file');
163
164
            $count_media = DB::table('media')
165
                ->groupBy(['m_file'])
166
                ->select([new Expression('COUNT(*) AS total'), 'm_file'])
167
                ->pluck('total', 'm_file');
168
169
            $count_notes = DB::table('other')
170
                ->where('o_type', '=', 'NOTE')
171
                ->groupBy(['o_file'])
172
                ->select([new Expression('COUNT(*) AS total'), 'o_file'])
173
                ->pluck('total', 'o_file');
174
175
            $count_repositories = DB::table('other')
176
                ->where('o_type', '=', 'REPO')
177
                ->groupBy(['o_file'])
178
                ->select([new Expression('COUNT(*) AS total'), 'o_file'])
179
                ->pluck('total', 'o_file');
180
181
            $count_sources = DB::table('sources')
182
                ->groupBy(['s_file'])
183
                ->select([new Expression('COUNT(*) AS total'), 's_file'])
184
                ->pluck('total', 's_file');
185
186
            $content = view('modules/sitemap/sitemap-index.xml', [
187
                'all_trees'          => $this->tree_service->all(),
188
                'count_individuals'  => $count_individuals,
189
                'count_media'        => $count_media,
190
                'count_notes'        => $count_notes,
191
                'count_repositories' => $count_repositories,
192
                'count_sources'      => $count_sources,
193
                'last_mod'           => date('Y-m-d'),
194
                'records_per_volume' => self::RECORDS_PER_VOLUME,
195
            ]);
196
197
            $this->setPreference('sitemap.xml', $content);
198
        }
199
200
        return response($content, StatusCodeInterface::STATUS_OK, [
201
            'Content-Type' => 'application/xml',
202
        ]);
203
    }
204
205
    /**
206
     * @param ServerRequestInterface $request
207
     *
208
     * @return ResponseInterface
209
     */
210
    public function getFileAction(ServerRequestInterface $request): ResponseInterface
211
    {
212
        $file = $request->getQueryParams()['file'];
213
214
        if (!preg_match('/^(\d+)-([imnrs])-(\d+)$/', $file, $match)) {
215
            throw new NotFoundHttpException('Bad sitemap file');
216
        }
217
218
        $timestamp   = (int) $this->getPreference('sitemap-' . $file . '.timestamp');
219
        $expiry_time = Carbon::now()->subSeconds(self::CACHE_LIFE)->unix();
220
221
        if ($timestamp > $expiry_time) {
222
            $content = $this->getPreference('sitemap-' . $file . '.xml');
223
        } else {
224
            $tree = Tree::findById((int) $match[1]);
225
226
            if ($tree === null) {
227
                throw new NotFoundHttpException('No such tree');
228
            }
229
230
            $records = $this->sitemapRecords($tree, $match[2], self::RECORDS_PER_VOLUME, self::RECORDS_PER_VOLUME * $match[3]);
231
232
            $content = view('modules/sitemap/sitemap-file.xml', ['records' => $records]);
233
234
            $this->setPreference('sitemap.xml', $content);
235
        }
236
237
        return response($content, StatusCodeInterface::STATUS_OK, [
238
            'Content-Type' => 'application/xml',
239
        ]);
240
    }
241
242
    /**
243
     * @param Tree   $tree
244
     * @param string $type
245
     * @param int    $limit
246
     * @param int    $offset
247
     *
248
     * @return Collection
249
     */
250
    private function sitemapRecords(Tree $tree, string $type, int $limit, int $offset): Collection
251
    {
252
        switch ($type) {
253
            case 'i':
254
                $records = $this->sitemapIndividuals($tree, $limit, $offset);
255
                break;
256
257
            case 'm':
258
                $records = $this->sitemapMedia($tree, $limit, $offset);
259
                break;
260
261
            case 'n':
262
                $records = $this->sitemapNotes($tree, $limit, $offset);
263
                break;
264
265
            case 'r':
266
                $records = $this->sitemapRepositories($tree, $limit, $offset);
267
                break;
268
269
            case 's':
270
                $records = $this->sitemapSources($tree, $limit, $offset);
271
                break;
272
273
            default:
274
                throw new NotFoundHttpException('Invalid record type: ' . $type);
275
        }
276
277
        // Skip private records.
278
        $records = $records->filter(GedcomRecord::accessFilter());
279
280
        return $records;
281
    }
282
283
    /**
284
     * @param Tree $tree
285
     * @param int  $limit
286
     * @param int  $offset
287
     *
288
     * @return Collection
289
     */
290
    private function sitemapIndividuals(Tree $tree, int $limit, int $offset): Collection
291
    {
292
        return DB::table('individuals')
293
            ->where('i_file', '=', $tree->id())
294
            ->orderBy('i_id')
295
            ->skip($offset)
296
            ->take($limit)
297
            ->get()
298
            ->map(Individual::rowMapper());
299
    }
300
301
    /**
302
     * @param Tree $tree
303
     * @param int  $limit
304
     * @param int  $offset
305
     *
306
     * @return Collection
307
     */
308
    private function sitemapMedia(Tree $tree, int $limit, int $offset): Collection
309
    {
310
        return DB::table('media')
311
            ->where('m_file', '=', $tree->id())
312
            ->orderBy('m_id')
313
            ->skip($offset)
314
            ->take($limit)
315
            ->get()
316
            ->map(Media::rowMapper());
317
    }
318
319
    /**
320
     * @param Tree $tree
321
     * @param int  $limit
322
     * @param int  $offset
323
     *
324
     * @return Collection
325
     */
326
    private function sitemapNotes(Tree $tree, int $limit, int $offset): Collection
327
    {
328
        return DB::table('other')
329
            ->where('o_file', '=', $tree->id())
330
            ->where('o_type', '=', 'NOTE')
331
            ->orderBy('o_id')
332
            ->skip($offset)
333
            ->take($limit)
334
            ->get()
335
            ->map(Note::rowMapper());
336
    }
337
338
    /**
339
     * @param Tree $tree
340
     * @param int  $limit
341
     * @param int  $offset
342
     *
343
     * @return Collection
344
     */
345
    private function sitemapRepositories(Tree $tree, int $limit, int $offset): Collection
346
    {
347
        return DB::table('other')
348
            ->where('o_file', '=', $tree->id())
349
            ->where('o_type', '=', 'REPO')
350
            ->orderBy('o_id')
351
            ->skip($offset)
352
            ->take($limit)
353
            ->get()
354
            ->map(Repository::rowMapper());
355
    }
356
357
    /**
358
     * @param Tree $tree
359
     * @param int  $limit
360
     * @param int  $offset
361
     *
362
     * @return Collection
363
     */
364
    private function sitemapSources(Tree $tree, int $limit, int $offset): Collection
365
    {
366
        return DB::table('sources')
367
            ->where('s_file', '=', $tree->id())
368
            ->orderBy('s_id')
369
            ->skip($offset)
370
            ->take($limit)
371
            ->get()
372
            ->map(Source::rowMapper());
373
    }
374
}
375