Passed
Push — master ( 27825e...43e2cc )
by Greg
05:43
created

AutocompleteController::select2Note()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 15
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 23
rs 9.7666
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2019 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
declare(strict_types=1);
17
18
namespace Fisharebest\Webtrees\Http\Controllers;
19
20
use Fisharebest\Flysystem\Adapter\ChrootAdapter;
21
use Fisharebest\Webtrees\Auth;
22
use Fisharebest\Webtrees\Family;
23
use Fisharebest\Webtrees\GedcomRecord;
24
use Fisharebest\Webtrees\Individual;
25
use Fisharebest\Webtrees\Media;
26
use Fisharebest\Webtrees\Note;
27
use Fisharebest\Webtrees\Repository;
28
use Fisharebest\Webtrees\Services\SearchService;
29
use Fisharebest\Webtrees\Site;
30
use Fisharebest\Webtrees\Source;
31
use Fisharebest\Webtrees\Tree;
32
use Illuminate\Database\Capsule\Manager as DB;
33
use Illuminate\Database\Query\JoinClause;
34
use Illuminate\Support\Collection;
35
use Illuminate\Support\Str;
36
use League\Flysystem\Filesystem;
37
use Psr\Http\Message\ResponseInterface;
38
use Psr\Http\Message\ServerRequestInterface;
39
use function array_map;
40
use function array_merge;
41
use function array_slice;
42
use function array_unique;
43
use function array_unshift;
44
use function count;
45
use function curl_close;
46
use function curl_exec;
47
use function curl_init;
48
use function curl_setopt;
49
use function file_get_contents;
50
use function function_exists;
51
use function ini_get;
52
use function is_array;
53
use function json_decode;
54
use function preg_match_all;
55
use function preg_quote;
56
use function rawurlencode;
57
use function response;
58
use function view;
59
use const CURLOPT_RETURNTRANSFER;
60
use const CURLOPT_URL;
61
62
/**
63
 * Controller for the autocomplete callbacks
64
 */
65
class AutocompleteController extends AbstractBaseController
66
{
67
    // For clients that request one page of data at a time.
68
    private const RESULTS_PER_PAGE = 20;
69
70
    /** @var SearchService */
71
    private $search_service;
72
73
    /**
74
     * AutocompleteController constructor.
75
     *
76
     * @param SearchService $search_service
77
     */
78
    public function __construct(SearchService $search_service)
79
    {
80
        $this->search_service = $search_service;
81
    }
82
83
    /**
84
     * Autocomplete for media folders.
85
     *
86
     * @param ServerRequestInterface $request
87
     * @param Tree                   $tree
88
     * @param Filesystem             $filesystem
89
     *
90
     * @return ResponseInterface
91
     */
92
    public function folder(ServerRequestInterface $request, Tree $tree, Filesystem $filesystem): ResponseInterface
93
    {
94
        $query = $request->getQueryParams()['query'] ?? '';
95
96
        $prefix = $tree->getPreference('MEDIA_DIRECTORY');
97
98
        $media_filesystem = new Filesystem(new ChrootAdapter($filesystem, $prefix));
99
100
        $contents = new Collection($media_filesystem->listContents('', true));
101
102
        $folders = $contents
103
            ->filter(static function (array $object) use ($query): bool {
104
                return $object['type'] === 'dir' && Str::contains($object['path'], $query);
105
            })
106
            ->map(static function (array $object): array {
107
                return ['value' => $object['path']];
108
            });
109
110
        return response($folders);
111
    }
112
113
    /**
114
     * Autocomplete for source citations.
115
     *
116
     * @param ServerRequestInterface $request
117
     * @param Tree                   $tree
118
     *
119
     * @return ResponseInterface
120
     */
121
    public function page(ServerRequestInterface $request, Tree $tree): ResponseInterface
122
    {
123
        $query = $request->getQueryParams()['query'] ?? '';
124
        $xref  = $request->getQueryParams()['extra'] ?? '';
125
126
        $source = Source::getInstance($xref, $tree);
127
128
        Auth::checkSourceAccess($source);
129
130
        $regex_query = preg_quote(strtr($query, [' ' => '.+']), '/');
131
132
        // Fetch all records with a link to this source
133
        $individuals = DB::table('individuals')
134
            ->join('link', static function (JoinClause $join): void {
135
                $join
136
                    ->on('l_file', '=', 'i_file')
137
                    ->on('l_from', '=', 'i_id');
138
            })
139
            ->where('i_file', '=', $tree->id())
140
            ->where('l_to', '=', $xref)
141
            ->where('l_type', '=', 'SOUR')
142
            ->distinct()
143
            ->select(['individuals.*'])
144
            ->get()
145
            ->map(Individual::rowMapper())
146
            ->filter(GedcomRecord::accessFilter());
147
148
        $families = DB::table('families')
149
            ->join('link', static function (JoinClause $join): void {
150
                $join
151
                    ->on('l_file', '=', 'f_file')
152
                    ->on('l_from', '=', 'f_id')
153
                    ->where('l_type', '=', 'SOUR');
154
            })
155
            ->where('f_file', '=', $tree->id())
156
            ->where('l_to', '=', $xref)
157
            ->where('l_type', '=', 'SOUR')
158
            ->distinct()
159
            ->select(['families.*'])
160
            ->get()
161
            ->map(Family::rowMapper())
162
            ->filter(GedcomRecord::accessFilter());
163
164
        $pages = new Collection();
165
166
        foreach ($individuals->merge($families) as $record) {
167
            if (preg_match_all('/\n1 SOUR @' . $xref . '@(?:\n[2-9].*)*\n2 PAGE (.*' . $regex_query . '.*)/i', $record->gedcom(), $matches)) {
168
                $pages = $pages->concat($matches[1]);
169
            }
170
171
            if (preg_match_all('/\n2 SOUR @' . $xref . '@(?:\n[3-9].*)*\n3 PAGE (.*' . $regex_query . '.*)/i', $record->gedcom(), $matches)) {
172
                $pages = $pages->concat($matches[1]);
173
            }
174
        }
175
176
        $pages = $pages
177
            ->unique()
178
            ->map(static function (string $page): array {
179
                return ['value' => $page];
180
            })
181
            ->all();
182
183
        return response($pages);
184
    }
185
186
    /**
187
     * /**
188
     * Autocomplete for place names.
189
     *
190
     * @param ServerRequestInterface $request
191
     * @param Tree                   $tree
192
     *
193
     * @return ResponseInterface
194
     */
195
    public function place(ServerRequestInterface $request, Tree $tree): ResponseInterface
196
    {
197
        $query = $request->getQueryParams()['query'] ?? '';
198
        $data  = [];
199
200
        foreach ($this->search_service->searchPlaces($tree, $query) as $place) {
201
            $data[] = ['value' => $place->gedcomName()];
202
        }
203
204
        $geonames = Site::getPreference('geonames');
205
206
        if (empty($data) && $geonames !== '') {
207
            // No place found? Use an external gazetteer
208
            $url =
209
                'http://api.geonames.org/searchJSON' .
210
                '?name_startsWith=' . rawurlencode($query) .
211
                '&lang=' . WT_LOCALE .
212
                '&fcode=CMTY&fcode=ADM4&fcode=PPL&fcode=PPLA&fcode=PPLC' .
213
                '&style=full' .
214
                '&username=' . rawurlencode($geonames);
215
216
            // try to use curl when file_get_contents not allowed
217
            if (ini_get('allow_url_fopen')) {
218
                $json = file_get_contents($url);
219
            } elseif (function_exists('curl_init')) {
220
                $ch = curl_init();
221
                curl_setopt($ch, CURLOPT_URL, $url);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

221
                curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_URL, $url);
Loading history...
222
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
223
                $json = curl_exec($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

223
                $json = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
224
                curl_close($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

224
                curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
225
            } else {
226
                return response([]);
227
            }
228
229
            $places = json_decode($json, true);
230
            if (isset($places['geonames']) && is_array($places['geonames'])) {
231
                foreach ($places['geonames'] as $k => $place) {
232
                    $data[] = ['value' => $place['name'] . ', ' . $place['adminName2'] . ', ' . $place['adminName1'] . ', ' . $place['countryName']];
233
                }
234
            }
235
        }
236
237
        return response($data);
238
    }
239
240
    /**
241
     * @param ServerRequestInterface $request
242
     * @param Tree                   $tree
243
     *
244
     * @return ResponseInterface
245
     */
246
    public function select2Family(ServerRequestInterface $request, Tree $tree): ResponseInterface
247
    {
248
        $page  = (int) ($request->getParsedBody()['page'] ?? 1);
249
        $query = $request->getParsedBody()['q'] ?? '';
250
251
        // Fetch one more row than we need, so we can know if more rows exist.
252
        $offset = ($page - 1) * self::RESULTS_PER_PAGE;
253
        $limit  = self::RESULTS_PER_PAGE + 1;
254
255
        // Allow private records to be selected?
256
        $filter = $tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS') === '1' ? null : GedcomRecord::accessFilter();
257
258
        $results = $this->search_service
259
            ->searchFamilyNames([$tree], [$query], $offset, $limit)
260
            ->prepend(Family::getInstance($query, $tree))
261
            ->filter($filter)
262
            ->map(static function (Family $family): array {
263
                return [
264
                    'id'    => $family->xref(),
265
                    'text'  => view('selects/family', ['family' => $family]),
266
                    'title' => ' ',
267
                ];
268
            });
269
270
        return response([
271
            'results'    => $results->slice(0, self::RESULTS_PER_PAGE)->all(),
272
            'pagination' => [
273
                'more' => $results->count() > self::RESULTS_PER_PAGE,
274
            ],
275
        ]);
276
    }
277
278
    /**
279
     * @param ServerRequestInterface $request
280
     * @param Tree                   $tree
281
     *
282
     * @return ResponseInterface
283
     */
284
    public function select2Individual(ServerRequestInterface $request, Tree $tree): ResponseInterface
285
    {
286
        $page  = (int) ($request->getParsedBody()['page'] ?? 1);
287
        $query = $request->getParsedBody()['q'] ?? '';
288
289
        // Fetch one more row than we need, so we can know if more rows exist.
290
        $offset  = ($page - 1) * self::RESULTS_PER_PAGE;
291
        $limit   = self::RESULTS_PER_PAGE + 1;
292
        $results = $this->search_service->searchIndividualNames([$tree], [$query], $offset, $limit);
293
294
        $record = Individual::getInstance($query, $tree);
295
        if ($record instanceof Individual && ($record->canShow() || $tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS') === '1')) {
296
            $results->prepend($record);
297
        }
298
299
        $results = $results
300
            ->map(static function (Individual $individual): array {
301
                return [
302
                    'id'    => $individual->xref(),
303
                    'text'  => view('selects/individual', ['individual' => $individual]),
304
                    'title' => ' ',
305
                ];
306
            });
307
308
        return response([
309
            'results'    => $results->slice(0, self::RESULTS_PER_PAGE)->all(),
310
            'pagination' => [
311
                'more' => $results->count() > self::RESULTS_PER_PAGE,
312
            ],
313
        ]);
314
    }
315
316
    /**
317
     * @param ServerRequestInterface $request
318
     * @param Tree                   $tree
319
     *
320
     * @return ResponseInterface
321
     */
322
    public function select2MediaObject(ServerRequestInterface $request, Tree $tree): ResponseInterface
323
    {
324
        $page  = (int) ($request->getParsedBody()['page'] ?? 1);
325
        $query = $request->getParsedBody()['q'] ?? '';
326
327
        // Fetch one more row than we need, so we can know if more rows exist.
328
        $offset  = ($page - 1) * self::RESULTS_PER_PAGE;
329
        $limit   = self::RESULTS_PER_PAGE + 1;
330
        $results = $this->search_service->searchMedia([$tree], [$query], $offset, $limit);
331
332
        $record = Family::getInstance($query, $tree);
333
        if ($record instanceof Family && ($record->canShow() || $tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS') === '1')) {
334
            $results->prepend($record);
335
        }
336
337
        $results = $results
338
            ->map(static function (Media $media): array {
339
                return [
340
                    'id'    => $media->xref(),
341
                    'text'  => view('selects/media', ['media' => $media]),
342
                    'title' => ' ',
343
                ];
344
            });
345
346
        return response([
347
            'results'    => $results->slice(0, self::RESULTS_PER_PAGE)->all(),
348
            'pagination' => [
349
                'more' => $results->count() > self::RESULTS_PER_PAGE,
350
            ],
351
        ]);
352
    }
353
354
    /**
355
     * @param ServerRequestInterface $request
356
     * @param Tree                   $tree
357
     *
358
     * @return ResponseInterface
359
     */
360
    public function select2Note(ServerRequestInterface $request, Tree $tree): ResponseInterface
361
    {
362
        $page  = (int) ($request->getParsedBody()['page'] ?? 1);
363
        $query = $request->getParsedBody()['q'] ?? '';
364
365
        // Fetch one more row than we need, so we can know if more rows exist.
366
        $offset = ($page - 1) * self::RESULTS_PER_PAGE;
367
        $limit  = self::RESULTS_PER_PAGE + 1;
368
369
        $results = $this->search_service
370
            ->searchNotes([$tree], [$query], $offset, $limit)
371
            ->map(static function (Note $note): array {
372
                return [
373
                    'id'    => $note->xref(),
374
                    'text'  => view('selects/note', ['note' => $note]),
375
                    'title' => ' ',
376
                ];
377
            });
378
379
        return response([
380
            'results'    => $results->slice(0, self::RESULTS_PER_PAGE)->all(),
381
            'pagination' => [
382
                'more' => $results->count() > self::RESULTS_PER_PAGE,
383
            ],
384
        ]);
385
    }
386
387
    /**
388
     * @param ServerRequestInterface $request
389
     * @param Tree                   $tree
390
     *
391
     * @return ResponseInterface
392
     */
393
    public function select2Place(ServerRequestInterface $request, Tree $tree): ResponseInterface
394
    {
395
        $page  = (int) ($request->getParsedBody()['page'] ?? 1);
396
        $query = $request->getParsedBody()['q'] ?? '';
397
398
        return response($this->placeSearch($tree, $page, $query, true));
399
    }
400
401
    /**
402
     * Look up a place name.
403
     *
404
     * @param Tree   $tree   Search this tree.
405
     * @param int    $page   Skip this number of pages.  Starts with zero.
406
     * @param string $query  Search terms.
407
     * @param bool   $create if true, include the query in the results so it can be created.
408
     *
409
     * @return mixed[]
410
     */
411
    private function placeSearch(Tree $tree, int $page, string $query, bool $create): array
412
    {
413
        $offset  = ($page - 1) * self::RESULTS_PER_PAGE;
414
        $results = [];
415
        $found   = false;
416
417
        foreach ($this->search_service->searchPlaces($tree, $query) as $place) {
418
            $place_name = $place->gedcomName();
419
            if ($place_name === $query) {
420
                $found = true;
421
            }
422
            $results[] = [
423
                'id'    => $place_name,
424
                'text'  => $place_name,
425
                'title' => ' ',
426
            ];
427
        }
428
429
        $geonames = Site::getPreference('geonames');
430
431
        // No place found? Use an external gazetteer
432
        if (empty($results) && $geonames !== '') {
433
            $url =
434
                'http://api.geonames.org/searchJSON' .
435
                '?name_startsWith=' . rawurlencode($query) .
436
                '&lang=' . WT_LOCALE .
437
                '&fcode=CMTY&fcode=ADM4&fcode=PPL&fcode=PPLA&fcode=PPLC' .
438
                '&style=full' .
439
                '&username=' . rawurlencode($geonames);
440
            // try to use curl when file_get_contents not allowed
441
            if (ini_get('allow_url_fopen')) {
442
                $json   = file_get_contents($url);
443
                $places = json_decode($json, true);
444
            } elseif (function_exists('curl_init')) {
445
                $ch = curl_init();
446
                curl_setopt($ch, CURLOPT_URL, $url);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

446
                curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_URL, $url);
Loading history...
447
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
448
                $json   = curl_exec($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

448
                $json   = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
449
                $places = json_decode($json, true);
450
                curl_close($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

450
                curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
451
            } else {
452
                $places = [];
453
            }
454
            if (isset($places['geonames']) && is_array($places['geonames'])) {
455
                foreach ($places['geonames'] as $k => $place) {
456
                    $place_name = $place['name'] . ', ' . $place['adminName2'] . ', ' . $place['adminName1'] . ', ' . $place['countryName'];
457
                    if ($place_name === $query) {
458
                        $found = true;
459
                    }
460
                    $results[] = [
461
                        'id'    => $place_name,
462
                        'text'  => $place_name,
463
                        'title' => ' ',
464
                    ];
465
                }
466
            }
467
        }
468
469
        // Include the query term in the results.  This allows the user to select a
470
        // place that doesn't already exist in the database.
471
        if (!$found && $create) {
472
            array_unshift($results, [
473
                'id'   => $query,
474
                'text' => $query,
475
            ]);
476
        }
477
478
        $more    = count($results) > $offset + self::RESULTS_PER_PAGE;
479
        $results = array_slice($results, $offset, self::RESULTS_PER_PAGE);
480
481
        return [
482
            'results'    => $results,
483
            'pagination' => [
484
                'more' => $more,
485
            ],
486
        ];
487
    }
488
489
    /**
490
     * @param ServerRequestInterface $request
491
     * @param Tree                   $tree
492
     *
493
     * @return ResponseInterface
494
     */
495
    public function select2Repository(ServerRequestInterface $request, Tree $tree): ResponseInterface
496
    {
497
        $page  = (int) ($request->getParsedBody()['page'] ?? 1);
498
        $query = $request->getParsedBody()['q'] ?? '';
499
500
        // Fetch one more row than we need, so we can know if more rows exist.
501
        $offset = ($page - 1) * self::RESULTS_PER_PAGE;
502
        $limit  = self::RESULTS_PER_PAGE + 1;
503
504
        $results = $this->search_service
505
            ->searchRepositories([$tree], [$query], $offset, $limit)
506
            ->map(static function (Repository $repository): array {
507
                return [
508
                    'id'    => $repository->xref(),
509
                    'text'  => view('selects/repository', ['repository' => $repository]),
510
                    'title' => ' ',
511
                ];
512
            });
513
514
        return response([
515
            'results'    => $results->slice(0, self::RESULTS_PER_PAGE)->all(),
516
            'pagination' => [
517
                'more' => $results->count() > self::RESULTS_PER_PAGE,
518
            ],
519
        ]);
520
    }
521
522
    /**
523
     * @param ServerRequestInterface $request
524
     * @param Tree                   $tree
525
     *
526
     * @return ResponseInterface
527
     */
528
    public function select2Source(ServerRequestInterface $request, Tree $tree): ResponseInterface
529
    {
530
        $page  = (int) ($request->getParsedBody()['page'] ?? 1);
531
        $query = $request->getParsedBody()['q'] ?? '';
532
533
        // Fetch one more row than we need, so we can know if more rows exist.
534
        $offset = ($page - 1) * self::RESULTS_PER_PAGE;
535
        $limit  = self::RESULTS_PER_PAGE + 1;
536
537
        $results = $this->search_service
538
            ->searchSourcesByName([$tree], [$query], $offset, $limit)
539
            ->map(static function (Source $source): array {
540
                return [
541
                    'id'    => $source->xref(),
542
                    'text'  => view('selects/source', ['source' => $source]),
543
                    'title' => ' ',
544
                ];
545
            });
546
547
        return response([
548
            'results'    => $results->slice(0, self::RESULTS_PER_PAGE)->all(),
549
            'pagination' => [
550
                'more' => $results->count() > self::RESULTS_PER_PAGE,
551
            ],
552
        ]);
553
    }
554
555
    /**
556
     * @param ServerRequestInterface $request
557
     * @param Tree                   $tree
558
     *
559
     * @return ResponseInterface
560
     */
561
    public function select2Submitter(ServerRequestInterface $request, Tree $tree): ResponseInterface
562
    {
563
        $page  = (int) ($request->getParsedBody()['page'] ?? 1);
564
        $query = $request->getParsedBody()['q'] ?? '';
565
566
        // Fetch one more row than we need, so we can know if more rows exist.
567
        $offset = ($page - 1) * self::RESULTS_PER_PAGE;
568
        $limit  = self::RESULTS_PER_PAGE + 1;
569
570
        $results = $this->search_service
571
            ->searchSubmitters([$tree], [$query], $offset, $limit)
572
            ->map(static function (GedcomRecord $submitter): array {
573
                return [
574
                    'id'    => $submitter->xref(),
575
                    'text'  => view('selects/submitter', ['submitter' => $submitter]),
576
                    'title' => ' ',
577
                ];
578
            });
579
580
        return response([
581
            'results'    => $results->slice(0, self::RESULTS_PER_PAGE)->all(),
582
            'pagination' => [
583
                'more' => $results->count() > self::RESULTS_PER_PAGE,
584
            ],
585
        ]);
586
    }
587
}
588