Completed
Push — develop ( 9087a8...c9b4ef )
by Greg
16:31 queued 05:44
created

Select2::commonConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 0
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2018 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
17
namespace Fisharebest\Webtrees;
18
19
use RecursiveDirectoryIterator;
20
use RecursiveIteratorIterator;
21
22
/**
23
 * Generate markup and AJAX responses for SELECT2 queries.
24
 *
25
 * @link https://select2.github.io/
26
 */
27
class Select2 extends Html {
28
	// Send this many results with each request.
29
	const RESULTS_PER_PAGE = 20;
30
31
	// Don't send queries with fewer than this many characters
32
	const MINIMUM_INPUT_LENGTH = '1';
33
34
	// Don't send queries until this many milliseconds.
35
	const DELAY = '350';
36
37
	// API endpoints
38
	const URL_FAM  = 'action.php?action=select2-family';
39
	const URL_INDI = 'action.php?action=select2-individual';
40
	const URL_NOTE = 'action.php?action=select2-note';
41
	const URL_OBJE = 'action.php?action=select2-media';
42
	const URL_PLAC = 'action.php?action=select2-place';
43
	const URL_REPO = 'action.php?action=select2-repository';
44
	const URL_SOUR = 'action.php?action=select2-source';
45
	const URL_SUBM = 'action.php?action=select2-submitter';
46
	const URL_FLAG = 'action.php?action=select2-flag';
47
48
	/**
49
	 * Select2 configuration that is common to all searches.
50
	 *
51
	 * @return string[]
52
	 */
53
	private static function commonConfig() {
54
		return [
55
			'autocomplete'                    => 'off',
56
			'class'                           => 'form-control select2',
57
			'data-ajax--delay'                => self::DELAY,
58
			'data-ajax--minimum-input-length' => self::MINIMUM_INPUT_LENGTH,
59
			'data-ajax--type'                 => 'POST',
60
			'data-allow-clear'                => 'true',
61
			'data-placeholder'                => '',
62
		];
63
	}
64
65
	/**
66
	 * Select2 configuration for a family lookup.
67
	 *
68
	 * @return string[]
69
	 */
70
	public static function familyConfig() {
71
		return self::commonConfig() + ['data-ajax--url' => self::URL_FAM];
72
	}
73
74
	/**
75
	 * Look up a family.
76
	 *
77
	 * @param Tree   $tree  Search this tree.
78
	 * @param int    $page  Skip this number of pages.  Starts with zero.
79
	 * @param string $query Search terms.
80
	 *
81
	 * @return mixed[]
82
	 */
83
	public static function familySearch(Tree $tree, $page, $query) {
84
		$offset  = $page * self::RESULTS_PER_PAGE;
85
		$more    = false;
86
		$results = [];
87
88
		$cursor = Database::prepare("SELECT DISTINCT 'FAM' AS type, f_id AS xref, f_gedcom AS gedcom, husb_name.n_sort, wife_name.n_sort" .
89
			" FROM `##families`" .
90
			" JOIN `##name` AS husb_name ON f_husb = husb_name.n_id AND f_file = husb_name.n_file" .
91
			" JOIN `##name` AS wife_name ON f_wife = wife_name.n_id AND f_file = wife_name.n_file" .
92
			" WHERE (CONCAT(husb_name.n_full, ' ', wife_name.n_full) LIKE CONCAT('%', REPLACE(:query, ' ', '%'), '%') OR f_id = :xref) AND f_file = :tree_id" .
93
			" AND husb_name.n_type <> '_MARNM' AND wife_name.n_type <> '_MARNM'" .
94
			" ORDER BY husb_name.n_sort, wife_name.n_sort COLLATE :collation")->execute([
95
			'query'     => $query,
96
			'xref'      => $query,
97
			'tree_id'   => $tree->getTreeId(),
98
			'collation' => I18N::collation(),
99
		]);
100
101
		while (is_object($row = $cursor->fetch())) {
102
			$family = Family::getInstance($row->xref, $tree, $row->gedcom);
103
			// Filter for privacy
104
			if ($family !== null && $family->canShowName()) {
105
				if ($offset > 0) {
106
					// Skip results
107
					$offset--;
108
				} elseif (count($results) === self::RESULTS_PER_PAGE) {
109
					// Stop when we have found a page of results
110
					$more = true;
111
					break;
112
				} else {
113
					// Add to the results
114
					$results[] = [
115
						'id'   => $row->xref,
116
						'text' => view('selects/family', ['family' => $family]),
117
					];
118
				}
119
			}
120
		}
121
		$cursor->closeCursor();
122
123
		return [
124
			'results'    => $results,
125
			'pagination' => [
126
				'more' => $more,
127
			],
128
		];
129
	}
130
131
	/**
132
	 * Select2 configuration for a flag icon lookup.
133
	 *
134
	 * @return string[]
135
	 */
136
	public static function flagConfig() {
137
		return self::commonConfig() + ['data-ajax--url' => self::URL_FLAG];
138
	}
139
140
	/**
141
	 * Format a flag icon for display in a Select2 control.
142
	 *
143
	 * @param string $flag
144
	 *
145
	 * @return string
146
	 */
147
	public static function flagValue($flag) {
148
		return '<img src="' . WT_MODULES_DIR . 'googlemap/places/flags/' . $flag . '"> ' . $flag;
149
	}
150
151
	/**
152
	 * Look up a flag icon.
153
	 *
154
	 * @param int    $page  Skip this number of pages.  Starts with zero.
155
	 * @param string $query Search terms.
156
	 *
157
	 * @return mixed[]
158
	 */
159
	public static function flagSearch($page, $query) {
160
		$offset    = $page * self::RESULTS_PER_PAGE;
161
		$more      = false;
162
		$results   = [];
163
		$directory = WT_ROOT . WT_MODULES_DIR . 'googlemap/places/flags/';
164
		$di        = new RecursiveDirectoryIterator($directory);
165
		$it        = new RecursiveIteratorIterator($di);
166
167
		$flag_files = [];
168
		foreach ($it as $file) {
169
			$file_path = substr($file->getPathname(), strlen($directory));
170
			if ($file->getExtension() === 'png' && stripos($file_path, $query) !== false) {
171
				if ($offset > 0) {
172
					// Skip results
173
					$offset--;
174
				} elseif (count($flag_files) >= self::RESULTS_PER_PAGE) {
175
					$more = true;
176
					break;
177
				} else {
178
					$flag_files[] = $file_path;
179
				}
180
			}
181
		}
182
183
		foreach ($flag_files as $flag_file) {
184
			$results[] = [
185
				'id'   => $flag_file,
186
				'text' => self::flagValue($flag_file),
187
			];
188
		}
189
190
		return [
191
			'results'    => $results,
192
			'pagination' => [
193
				'more' => $more,
194
			],
195
		];
196
	}
197
198
	/**
199
	 * Select2 configuration for an individual lookup.
200
	 *
201
	 * @return string[]
202
	 */
203
	public static function individualConfig() {
204
		return self::commonConfig() + ['data-ajax--url' => self::URL_INDI];
205
	}
206
207
	/**
208
	 * Look up an individual.
209
	 *
210
	 * @param Tree   $tree  Search this tree.
211
	 * @param int    $page  Skip this number of pages.  Starts with zero.
212
	 * @param string $query Search terms.
213
	 *
214
	 * @return mixed[]
215
	 */
216
	public static function individualSearch(Tree $tree, $page, $query) {
217
		$offset  = $page * self::RESULTS_PER_PAGE;
218
		$more    = false;
219
		$results = [];
220
		$cursor  = Database::prepare("SELECT i_id AS xref, i_gedcom AS gedcom, n_num" . " FROM `##individuals`" . " JOIN `##name` ON i_id = n_id AND i_file = n_file" . " WHERE (n_full LIKE CONCAT('%', REPLACE(:query, ' ', '%'), '%') OR i_id = :xref) AND i_file = :tree_id" . " ORDER BY n_full COLLATE :collation")->execute([
221
			'query'     => $query,
222
			'xref'      => $query,
223
			'tree_id'   => $tree->getTreeId(),
224
			'collation' => I18N::collation(),
225
		]);
226
227
		while (is_object($row = $cursor->fetch())) {
228
			$individual = Individual::getInstance($row->xref, $tree, $row->gedcom);
229
			$individual->setPrimaryName($row->n_num);
230
			// Filter for privacy
231
			if ($individual !== null && $individual->canShowName()) {
232
				if ($offset > 0) {
233
					// Skip results
234
					$offset--;
235
				} elseif (count($results) === self::RESULTS_PER_PAGE) {
236
					// Stop when we have found a page of results
237
					$more = true;
238
					break;
239
				} else {
240
					// Add to the results
241
					$results[] = [
242
						'id'   => $row->xref,
243
						'text' => view('selects/individual', ['individual' => $individual]),
244
					];
245
				}
246
			}
247
		}
248
		$cursor->closeCursor();
249
250
		return [
251
			'results'    => $results,
252
			'pagination' => [
253
				'more' => $more,
254
			],
255
		];
256
	}
257
258
	/**
259
	 * Select2 configuration for a media object lookup.
260
	 *
261
	 * @return string[]
262
	 */
263
	public static function mediaObjectConfig() {
264
		return self::commonConfig() + ['data-ajax--url' => self::URL_OBJE];
265
	}
266
267
	/**
268
	 * Look up a media object.
269
	 *
270
	 * @param Tree   $tree  Search this tree.
271
	 * @param int    $page  Skip this number of pages.  Starts with zero.
272
	 * @param string $query Search terms.
273
	 *
274
	 * @return mixed[]
275
	 */
276
	public static function mediaObjectSearch(Tree $tree, $page, $query) {
277
		$offset  = $page * self::RESULTS_PER_PAGE;
278
		$more    = false;
279
		$results = [];
280
		$cursor  = Database::prepare("SELECT m_id AS xref, m_gedcom AS gedcom, n_full" . " FROM `##media`" . " JOIN `##name` ON m_id = n_id AND m_file = n_file" . " WHERE (n_full LIKE CONCAT('%', REPLACE(:query, ' ', '%'), '%') OR m_id = :xref) AND m_file = :tree_id" . " ORDER BY n_full COLLATE :collation")->execute([
281
			'query'     => $query,
282
			'xref'      => $query,
283
			'tree_id'   => $tree->getTreeId(),
284
			'collation' => I18N::collation(),
285
		]);
286
287
		while (is_object($row = $cursor->fetch())) {
288
			$media = Media::getInstance($row->xref, $tree, $row->gedcom);
289
			// Filter for privacy
290
			if ($media !== null && $media->canShow()) {
291
				if ($offset > 0) {
292
					// Skip results
293
					$offset--;
294
				} elseif (count($results) === self::RESULTS_PER_PAGE) {
295
					// Stop when we have found a page of results
296
					$more = true;
297
					break;
298
				} else {
299
					// Add to the results
300
					$results[] = [
301
						'id'   => $row->xref,
302
						'text' => view('selects/media', ['media' => $media]),
303
					];
304
				}
305
			}
306
		}
307
		$cursor->closeCursor();
308
309
		return [
310
			'results'    => $results,
311
			'pagination' => [
312
				'more' => $more,
313
			],
314
		];
315
	}
316
317
	/**
318
	 * Select2 configuration for a note.
319
	 *
320
	 * @return string[]
321
	 */
322
	public static function noteConfig() {
323
		return self::commonConfig() + ['data-ajax--url' => self::URL_NOTE];
324
	}
325
326
	/**
327
	 * Look up a note.
328
	 *
329
	 * @param Tree   $tree  Search this tree.
330
	 * @param int    $page  Skip this number of pages.  Starts with zero.
331
	 * @param string $query Search terms.
332
	 *
333
	 * @return mixed[]
334
	 */
335
	public static function noteSearch(Tree $tree, $page, $query) {
336
		$offset  = $page * self::RESULTS_PER_PAGE;
337
		$more    = false;
338
		$results = [];
339
		$cursor  = Database::prepare("SELECT o_id AS xref, o_gedcom AS gedcom, n_full" . " FROM `##other`" . " JOIN `##name` ON o_id = n_id AND o_file = n_file" . " WHERE (n_full LIKE CONCAT('%', REPLACE(:query, ' ', '%'), '%') OR o_id = :xref) AND o_file = :tree_id AND o_type='NOTE'" . " ORDER BY n_full COLLATE :collation")->execute([
340
			'query'     => $query,
341
			'xref'      => $query,
342
			'tree_id'   => $tree->getTreeId(),
343
			'collation' => I18N::collation(),
344
		]);
345
346
		while (is_object($row = $cursor->fetch())) {
347
			$note = Note::getInstance($row->xref, $tree, $row->gedcom);
348
			// Filter for privacy
349
			if ($note !== null && $note->canShowName()) {
350
				if ($offset > 0) {
351
					// Skip results
352
					$offset--;
353
				} elseif (count($results) === self::RESULTS_PER_PAGE) {
354
					// Stop when we have found a page of results
355
					$more = true;
356
					break;
357
				} else {
358
					// Add to the results
359
					$results[] = [
360
						'id'   => $row->xref,
361
						'text' => view('selects/note', ['note' => $note]),
362
					];
363
				}
364
			}
365
		}
366
		$cursor->closeCursor();
367
368
		return [
369
			'results'    => $results,
370
			'pagination' => [
371
				'more' => $more,
372
			],
373
		];
374
	}
375
376
	/**
377
	 * Select2 configuration for a note.
378
	 *
379
	 * @return string[]
380
	 */
381
	public static function placeConfig() {
382
		return self::commonConfig() + ['data-ajax--url' => self::URL_PLAC];
383
	}
384
385
	/**
386
	 * Look up a place name.
387
	 *
388
	 * @param Tree   $tree   Search this tree.
389
	 * @param int    $page   Skip this number of pages.  Starts with zero.
390
	 * @param string $query  Search terms.
391
	 * @param bool   $create if true, include the query in the results so it can be created.
392
	 *
393
	 * @return mixed[]
394
	 */
395
	public static function placeSearch(Tree $tree, $page, $query, $create) {
396
		$offset  = $page * self::RESULTS_PER_PAGE;
397
		$results = [];
398
		$found   = false;
399
400
		// Do not filter by privacy. Place names on their own do not identify individuals.
401
		foreach (Place::findPlaces($query, $tree) as $place) {
402
			$place_name = $place->getGedcomName();
403
			if ($place_name === $query) {
404
				$found = true;
405
			}
406
			$results[] = [
407
				'id'   => $place_name,
408
				'text' => $place_name,
409
			];
410
		}
411
412
		// No place found? Use an external gazetteer
413
		if (empty($results) && $tree->getPreference('GEONAMES_ACCOUNT')) {
414
			$url =
415
				"http://api.geonames.org/searchJSON" .
416
				"?name_startsWith=" . urlencode($query) .
417
				"&lang=" . WT_LOCALE .
418
				"&fcode=CMTY&fcode=ADM4&fcode=PPL&fcode=PPLA&fcode=PPLC" .
419
				"&style=full" .
420
				"&username=" . $tree->getPreference('GEONAMES_ACCOUNT');
421
			// try to use curl when file_get_contents not allowed
422
			if (ini_get('allow_url_fopen')) {
423
				$json   = file_get_contents($url);
424
				$places = json_decode($json, true);
425
			} elseif (function_exists('curl_init')) {
426
				$ch = curl_init();
427
				curl_setopt($ch, CURLOPT_URL, $url);
428
				curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
429
				$json   = curl_exec($ch);
430
				$places = json_decode($json, true);
431
				curl_close($ch);
432
			} else {
433
				$places = [];
434
			}
435
			if (isset($places['geonames']) && is_array($places['geonames'])) {
436
				foreach ($places['geonames'] as $k => $place) {
437
					$place_name = $place['name'] . ', ' . $place['adminName2'] . ', ' . $place['adminName1'] . ', ' . $place['countryName'];
438
					if ($place_name === $query) {
439
						$found = true;
440
					}
441
					$results[] = [
442
						'id'   => $place_name,
443
						'text' => $place_name,
444
					];
445
				}
446
			}
447
		}
448
449
		// Include the query term in the results.  This allows the user to select a
450
		// place that doesn't already exist in the database.
451
		if (!$found && $create) {
452
			array_unshift($results, [
453
				'id'   => $query,
454
				'text' => $query,
455
			]);
456
		}
457
458
		$more    = count($results) > $offset + self::RESULTS_PER_PAGE;
459
		$results = array_slice($results, $offset, self::RESULTS_PER_PAGE);
460
461
		return [
462
			'results'    => $results,
463
			'pagination' => [
464
				'more' => $more,
465
			],
466
		];
467
	}
468
469
	/**
470
	 * Select2 configuration for a repository lookup.
471
	 *
472
	 * @return string[]
473
	 */
474
	public static function repositoryConfig() {
475
		return self::commonConfig() + ['data-ajax--url' => self::URL_REPO];
476
	}
477
478
	/**
479
	 * Look up a repository.
480
	 *
481
	 * @param Tree   $tree  Search this tree.
482
	 * @param int    $page  Skip this number of pages.  Starts with zero.
483
	 * @param string $query Search terms.
484
	 *
485
	 * @return mixed[]
486
	 */
487
	public static function repositorySearch(Tree $tree, $page, $query) {
488
		$offset  = $page * self::RESULTS_PER_PAGE;
489
		$more    = false;
490
		$results = [];
491
		$cursor  = Database::prepare("SELECT o_id AS xref, o_gedcom AS gedcom, n_full" . " FROM `##other`" . " JOIN `##name` ON o_id = n_id AND o_file = n_file" . " WHERE (n_full LIKE CONCAT('%', REPLACE(:query, ' ', '%'), '%') OR o_id = :xref) AND o_file = :tree_id AND o_type = 'REPO'" . " ORDER BY n_full COLLATE :collation")->execute([
492
			'query'     => $query,
493
			'xref'      => $query,
494
			'tree_id'   => $tree->getTreeId(),
495
			'collation' => I18N::collation(),
496
		]);
497
498
		while (is_object($row = $cursor->fetch())) {
499
			$repository = Repository::getInstance($row->xref, $tree, $row->gedcom);
500
			// Filter for privacy
501
			if ($repository !== null && $repository->canShow()) {
502
				if ($offset > 0) {
503
					// Skip results
504
					$offset--;
505
				} elseif (count($results) === self::RESULTS_PER_PAGE) {
506
					// Stop when we have found a page of results
507
					$more = true;
508
					break;
509
				} else {
510
					// Add to the results
511
					$results[] = [
512
						'id'   => $row->xref,
513
						'text' => view('selects/repository', ['repository' => $repository]),
514
					];
515
				}
516
			}
517
		}
518
		$cursor->closeCursor();
519
520
		return [
521
			'results'    => $results,
522
			'pagination' => [
523
				'more' => $more,
524
			],
525
		];
526
	}
527
528
	/**
529
	 * Select2 configuration for a source lookup.
530
	 *
531
	 * @return string[]
532
	 */
533
	public static function sourceConfig() {
534
		return self::commonConfig() + ['data-ajax--url' => self::URL_SOUR];
535
	}
536
537
	/**
538
	 * Look up a source.
539
	 *
540
	 * @param Tree   $tree  Search this tree.
541
	 * @param int    $page  Skip this number of pages.  Starts with zero.
542
	 * @param string $query Search terms.
543
	 *
544
	 * @return mixed[]
545
	 */
546
	public static function sourceSearch(Tree $tree, $page, $query) {
547
		$offset  = $page * self::RESULTS_PER_PAGE;
548
		$more    = false;
549
		$results = [];
550
		$cursor  = Database::prepare("SELECT s_id AS xref, s_gedcom AS gedcom, n_full" . " FROM `##sources`" . " JOIN `##name` ON s_id = n_id AND s_file = n_file" . " WHERE (n_full LIKE CONCAT('%', REPLACE(:query, ' ', '%'), '%') OR s_id = :xref) AND s_file = :tree_id" . " ORDER BY n_full COLLATE :collation")->execute([
551
			'query'     => $query,
552
			'xref'      => $query,
553
			'tree_id'   => $tree->getTreeId(),
554
			'collation' => I18N::collation(),
555
		]);
556
557
		while (is_object($row = $cursor->fetch())) {
558
			$source = Source::getInstance($row->xref, $tree, $row->gedcom);
559
			// Filter for privacy
560
			if ($source !== null && $source->canShow()) {
561
				if ($offset > 0) {
562
					// Skip results
563
					$offset--;
564
				} elseif (count($results) === self::RESULTS_PER_PAGE) {
565
					// Stop when we have found a page of results
566
					$more = true;
567
					break;
568
				} else {
569
					// Add to the results
570
					$results[] = [
571
						'id'   => $row->xref,
572
						'text' => view('selects/source', ['source' => $source]),
573
					];
574
				}
575
			}
576
		}
577
		$cursor->closeCursor();
578
579
		return [
580
			'results'    => $results,
581
			'pagination' => [
582
				'more' => $more,
583
			],
584
		];
585
	}
586
587
	/**
588
	 * Select2 configuration for a submitter lookup.
589
	 *
590
	 * @return string[]
591
	 */
592
	public static function submitterConfig() {
593
		return self::commonConfig() + ['data-ajax--url' => self::URL_SUBM];
594
	}
595
596
	/**
597
	 * Look up a submitter.
598
	 *
599
	 * @param Tree   $tree  Search this tree.
600
	 * @param int    $page  Skip this number of pages.  Starts with zero.
601
	 * @param string $query Search terms.
602
	 *
603
	 * @return mixed[]
604
	 */
605
	public static function submitterSearch(Tree $tree, $page, $query) {
606
		$offset  = $page * self::RESULTS_PER_PAGE;
607
		$more    = false;
608
		$results = [];
609
		$cursor  = Database::prepare(
610
			"SELECT o_id AS xref, o_gedcom AS gedcom" .
611
			" FROM `##other`" .
612
			" WHERE (o_id LIKE CONCAT('%', REPLACE(:query, ' ', '%'), '%') OR o_id = :xref) AND o_file = :tree_id AND o_type = 'SUBM'" .
613
			" ORDER BY o_id COLLATE :collation")->execute([
614
			'query'     => $query,
615
			'xref'      => $query,
616
			'tree_id'   => $tree->getTreeId(),
617
			'collation' => I18N::collation(),
618
		]);
619
620
		while (is_object($row = $cursor->fetch())) {
621
			$submitter = GedcomRecord::getInstance($row->xref, $tree, $row->gedcom);
622
			// Filter for privacy
623
			if ($submitter !== null && $submitter->canShow()) {
624
				if ($offset > 0) {
625
					// Skip results
626
					$offset--;
627
				} elseif (count($results) === self::RESULTS_PER_PAGE) {
628
					// Stop when we have found a page of results
629
					$more = true;
630
					break;
631
				} else {
632
					// Add to the results
633
					$results[] = [
634
						'id'   => $row->xref,
635
						'text' => view('selects/submitter', ['submitter' => $submitter]),
636
					];
637
				}
638
			}
639
		}
640
		$cursor->closeCursor();
641
642
		return [
643
			'results'    => $results,
644
			'pagination' => [
645
				'more' => $more,
646
			],
647
		];
648
	}
649
}
650