summaryrefslogtreecommitdiff
path: root/app/Functions/FunctionsDb.php
diff options
context:
space:
mode:
Diffstat (limited to 'app/Functions/FunctionsDb.php')
-rw-r--r--app/Functions/FunctionsDb.php1081
1 files changed, 1081 insertions, 0 deletions
diff --git a/app/Functions/FunctionsDb.php b/app/Functions/FunctionsDb.php
new file mode 100644
index 0000000000..f0bdabe01a
--- /dev/null
+++ b/app/Functions/FunctionsDb.php
@@ -0,0 +1,1081 @@
+<?php
+namespace Fisharebest\Webtrees\Functions;
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2015 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+use Fisharebest\Webtrees\Auth;
+use Fisharebest\Webtrees\Database;
+use Fisharebest\Webtrees\Date;
+use Fisharebest\Webtrees\Date\FrenchDate;
+use Fisharebest\Webtrees\Date\GregorianDate;
+use Fisharebest\Webtrees\Date\HijriDate;
+use Fisharebest\Webtrees\Date\JalaliDate;
+use Fisharebest\Webtrees\Date\JewishDate;
+use Fisharebest\Webtrees\Date\JulianDate;
+use Fisharebest\Webtrees\Fact;
+use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Note;
+use Fisharebest\Webtrees\Repository;
+use Fisharebest\Webtrees\Soundex;
+use Fisharebest\Webtrees\Source;
+use Fisharebest\Webtrees\Tree;
+
+/**
+ * Class FunctionsDb - common functions
+ */
+class FunctionsDb {
+ /**
+ * Fetch all records linked to a record - when deleting an object, we must
+ * also delete all links to it.
+ *
+ * @param string $xref
+ * @param int $gedcom_id
+ *
+ * @return string[]
+ */
+ public static function fetchAllLinks($xref, $gedcom_id) {
+ return
+ Database::prepare(
+ "SELECT l_from FROM `##link` WHERE l_file = ? AND l_to = ?" .
+ " UNION " .
+ "SELECT xref FROM `##change` WHERE status = 'pending' AND gedcom_id = ? AND new_gedcom LIKE" .
+ " CONCAT('%@', ?, '@%')"
+ )->execute(array(
+ $gedcom_id,
+ $xref,
+ $gedcom_id,
+ $xref,
+ ))->fetchOneColumn();
+ }
+
+ /**
+ * Get a list of all the sources.
+ *
+ * @param Tree $tree
+ *
+ * @return Source[] array
+ */
+ public static function getSourceList(Tree $tree) {
+ $rows = Database::prepare(
+ "SELECT s_id AS xref, s_gedcom AS gedcom FROM `##sources` WHERE s_file = :tree_id"
+ )->execute(array(
+ 'tree_id' => $tree->getTreeId(),
+ ))->fetchAll();
+
+ $list = array();
+ foreach ($rows as $row) {
+ $list[] = Source::getInstance($row->xref, $tree, $row->gedcom);
+ }
+ usort($list, '\Fisharebest\Webtrees\GedcomRecord::compare');
+
+ return $list;
+ }
+
+ /**
+ * Get a list of all the repositories.
+ *
+ * @param Tree $tree
+ *
+ * @return Repository[] array
+ */
+ public static function getRepositoryList(Tree $tree) {
+ $rows = Database::prepare(
+ "SELECT o_id AS xref, o_gedcom AS gedcom FROM `##other` WHERE o_type = 'REPO' AND o_file = ?"
+ )->execute(array(
+ $tree->getTreeId(),
+ ))->fetchAll();
+
+ $list = array();
+ foreach ($rows as $row) {
+ $list[] = Repository::getInstance($row->xref, $tree, $row->gedcom);
+ }
+ usort($list, '\Fisharebest\Webtrees\GedcomRecord::compare');
+
+ return $list;
+ }
+
+ /**
+ * Get a list of all the shared notes.
+ *
+ * @param Tree $tree
+ *
+ * @return Note[] array
+ */
+ public static function getNoteList(Tree $tree) {
+ $rows = Database::prepare(
+ "SELECT o_id AS xref, o_gedcom AS gedcom FROM `##other` WHERE o_type = 'NOTE' AND o_file = :tree_id"
+ )->execute(array(
+ 'tree_id' => $tree->getTreeId(),
+ ))->fetchAll();
+
+ $list = array();
+ foreach ($rows as $row) {
+ $list[] = Note::getInstance($row->xref, $tree, $row->gedcom);
+ }
+ usort($list, '\Fisharebest\Webtrees\GedcomRecord::compare');
+
+ return $list;
+ }
+
+ /**
+ * Search all individuals
+ *
+ * @param string[] $query Search terms
+ * @param Tree[] $trees The trees to search
+ *
+ * @return Individual[]
+ */
+ public static function searchIndividuals(array $query, array $trees) {
+ // Convert the query into a regular expression
+ $queryregex = array();
+
+ $sql = "SELECT i_id AS xref, i_file AS gedcom_id, i_gedcom AS gedcom FROM `##individuals` WHERE 1";
+ $args = array();
+
+ foreach ($query as $n => $q) {
+ $queryregex[] = preg_quote(I18N::strtoupper($q), '/');
+ $sql .= " AND i_gedcom COLLATE :collate_" . $n . " LIKE CONCAT('%', :query_" . $n . ", '%')";
+ $args['collate_' . $n] = I18N::collation();
+ $args['query_' . $n] = Filter::escapeLike($q);
+ }
+
+ $sql .= " AND i_file IN (";
+ foreach ($trees as $n => $tree) {
+ $sql .= $n ? ", " : "";
+ $sql .= ":tree_id_" . $n;
+ $args['tree_id_' . $n] = $tree->getTreeId();
+ }
+ $sql .= ")";
+
+ $list = array();
+ $rows = Database::prepare($sql)->execute($args)->fetchAll();
+ foreach ($rows as $row) {
+ // SQL may have matched on private data or gedcom tags, so check again against privatized data.
+ $record = Individual::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
+ // Ignore non-genealogy data
+ $gedrec = preg_replace('/\n\d (_UID|_WT_USER|FILE|FORM|TYPE|CHAN|REFN|RESN) .*/', '', $record->getGedcom());
+ // Ignore links and tags
+ $gedrec = preg_replace('/\n\d ' . WT_REGEX_TAG . '( @' . WT_REGEX_XREF . '@)?/', '', $gedrec);
+ // Re-apply the filtering
+ $gedrec = I18N::strtoupper($gedrec);
+ foreach ($queryregex as $regex) {
+ if (!preg_match('/' . $regex . '/', $gedrec)) {
+ continue 2;
+ }
+ }
+ $list[] = $record;
+ }
+
+ return $list;
+ }
+
+ /**
+ * Search the names of individuals
+ *
+ * @param string[] $query Search terms
+ * @param Tree[] $trees The trees to search
+ *
+ * @return Individual[]
+ */
+ public static function searchIndividualNames(array $query, array $trees) {
+ $sql = "SELECT DISTINCT i_id AS xref, i_file AS gedcom_id, i_gedcom AS gedcom, n_full FROM `##individuals` JOIN `##name` ON i_id=n_id AND i_file=n_file WHERE 1";
+ $args = array();
+
+ // Convert the query into a SQL expression
+ foreach ($query as $n => $q) {
+ $sql .= " AND n_full COLLATE :collate_" . $n . " LIKE CONCAT('%', :query_" . $n . ", '%')";
+ $args['collate_' . $n] = I18N::collation();
+ $args['query_' . $n] = Filter::escapeLike($q);
+ }
+
+ $sql .= " AND i_file IN (";
+ foreach ($trees as $n => $tree) {
+ $sql .= $n ? ", " : "";
+ $sql .= ":tree_id_" . $n;
+ $args['tree_id_' . $n] = $tree->getTreeId();
+ }
+ $sql .= ")";
+ $list = array();
+ $rows = Database::prepare($sql)->execute($args)->fetchAll();
+ foreach ($rows as $row) {
+ $indi = Individual::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
+ // The individual may have private names - and the DB search may have found it.
+ if ($indi->canShowName()) {
+ foreach ($indi->getAllNames() as $num => $name) {
+ if ($name['fullNN'] === $row->n_full) {
+ $indi->setPrimaryName($num);
+ // We need to clone $indi, as we may have multiple references to the
+ // same person in this list, and the "primary name" would otherwise
+ // be shared amongst all of them.
+ $list[] = clone $indi;
+ // Only need to match an individual on one name
+ break;
+ }
+ }
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Search for individuals names/places using phonetic matching
+ *
+ * @param string $soundex
+ * @param string $lastname
+ * @param string $firstname
+ * @param string $place
+ * @param Tree[] $trees
+ *
+ * @return Individual[]
+ */
+ public static function searchIndividualsPhonetic($soundex, $lastname, $firstname, $place, array $trees) {
+ switch ($soundex) {
+ case 'Russell':
+ $givn_sdx = Soundex::russell($firstname);
+ $surn_sdx = Soundex::russell($lastname);
+ $plac_sdx = Soundex::russell($place);
+ break;
+ case 'DaitchM':
+ $givn_sdx = Soundex::daitchMokotoff($firstname);
+ $surn_sdx = Soundex::daitchMokotoff($lastname);
+ $plac_sdx = Soundex::daitchMokotoff($place);
+ break;
+ default:
+ throw new \DomainException('soundex: ' . $soundex);
+ }
+
+ // Nothing to search for? Return nothing.
+ if (!$givn_sdx && !$surn_sdx && !$plac_sdx) {
+ return array();
+ }
+
+ $sql = "SELECT DISTINCT i_id AS xref, i_file AS gedcom_id, i_gedcom AS gedcom FROM `##individuals`";
+ $args = array();
+
+ if ($place) {
+ $sql .= " JOIN `##placelinks` ON pl_file = i_file AND pl_gid = i_id";
+ $sql .= " JOIN `##places` ON p_file = pl_file AND pl_p_id = p_id";
+ }
+ if ($firstname || $lastname) {
+ $sql .= " JOIN `##name` ON i_file=n_file AND i_id=n_id";
+ }
+ $sql .= " AND i_file IN (";
+ foreach ($trees as $n => $tree) {
+ $sql .= $n ? ", " : "";
+ $sql .= ":tree_id_" . $n;
+ $args['tree_id_' . $n] = $tree->getTreeId();
+ }
+ $sql .= ")";
+
+ if ($firstname && $givn_sdx) {
+ $sql .= " AND (";
+ $givn_sdx = explode(':', $givn_sdx);
+ foreach ($givn_sdx as $n => $sdx) {
+ $sql .= $n ? " OR " : "";
+ switch ($soundex) {
+ case 'Russell':
+ $sql .= "n_soundex_givn_std LIKE CONCAT('%', :given_name_" . $n . ", '%')";
+ break;
+ case 'DaitchM':
+ $sql .= "n_soundex_givn_dm LIKE CONCAT('%', :given_name_" . $n . ", '%')";
+ break;
+ }
+ $args['given_name_' . $n] = $sdx;
+ }
+ $sql .= ")";
+ }
+
+ if ($lastname && $surn_sdx) {
+ $sql .= " AND (";
+ $surn_sdx = explode(':', $surn_sdx);
+ foreach ($surn_sdx as $n => $sdx) {
+ $sql .= $n ? " OR " : "";
+ switch ($soundex) {
+ case 'Russell':
+ $sql .= "n_soundex_surn_std LIKE CONCAT('%', :surname_" . $n . ", '%')";
+ break;
+ case 'DaitchM':
+ $sql .= "n_soundex_surn_dm LIKE CONCAT('%', :surname_" . $n . ", '%')";
+ break;
+ }
+ $args['surname_' . $n] = $sdx;
+ }
+ $sql .= ")";
+ }
+
+ if ($place && $plac_sdx) {
+ $sql .= " AND (";
+ $plac_sdx = explode(':', $plac_sdx);
+ foreach ($plac_sdx as $n => $sdx) {
+ $sql .= $n ? " OR " : "";
+ switch ($soundex) {
+ case 'Russell':
+ $sql .= "p_std_soundex LIKE CONCAT('%', :place_" . $n . ", '%')";
+ break;
+ case 'DaitchM':
+ $sql .= "p_dm_soundex LIKE CONCAT('%', :place_" . $n . ", '%')";
+ break;
+ }
+ $args['place_' . $n] = $sdx;
+ }
+ $sql .= ")";
+ }
+
+ $list = array();
+ $rows = Database::prepare($sql)->execute($args)->fetchAll();
+ foreach ($rows as $row) {
+ $indi = Individual::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
+ if ($indi->canShowName()) {
+ $list[] = $indi;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * get recent changes since the given julian day inclusive
+ *
+ * @param int $jd leave empty to include all
+ * @param bool $allgeds
+ *
+ * @return string[] List of XREFs of records with changes
+ */
+ public static function getRecentChanges($jd = 0, $allgeds = false) {
+ global $WT_TREE;
+
+ $sql = "SELECT d_gid FROM `##dates` WHERE d_fact='CHAN' AND d_julianday1>=?";
+ $vars = array($jd);
+ if (!$allgeds) {
+ $sql .= " AND d_file=?";
+ $vars[] = $WT_TREE->getTreeId();
+ }
+ $sql .= " ORDER BY d_julianday1 DESC";
+
+ return Database::prepare($sql)->execute($vars)->fetchOneColumn();
+ }
+
+ /**
+ * Search family records
+ *
+ * @param string[] $query Search terms
+ * @param Tree[] $trees The trees to search
+ *
+ * @return Family[]
+ */
+ public static function searchFamilies(array $query, array $trees) {
+ // Convert the query into a regular expression
+ $queryregex = array();
+
+ $sql = "SELECT f_id AS xref, f_file AS gedcom_id, f_gedcom AS gedcom FROM `##families` WHERE 1";
+ $args = array();
+
+ foreach ($query as $n => $q) {
+ $queryregex[] = preg_quote(I18N::strtoupper($q), '/');
+ $sql .= " AND f_gedcom COLLATE :collate_" . $n . " LIKE CONCAT('%', :query_" . $n . ", '%')";
+ $args['collate_' . $n] = I18N::collation();
+ $args['query_' . $n] = Filter::escapeLike($q);
+ }
+
+ $sql .= " AND f_file IN (";
+ foreach ($trees as $n => $tree) {
+ $sql .= $n ? ", " : "";
+ $sql .= ":tree_id_" . $n;
+ $args['tree_id_' . $n] = $tree->getTreeId();
+ }
+ $sql .= ")";
+
+ $list = array();
+ $rows = Database::prepare($sql)->execute($args)->fetchAll();
+ foreach ($rows as $row) {
+ // SQL may have matched on private data or gedcom tags, so check again against privatized data.
+ $record = Family::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
+ // Ignore non-genealogy data
+ $gedrec = preg_replace('/\n\d (_UID|_WT_USER|FILE|FORM|TYPE|CHAN|REFN|RESN) .*/', '', $record->getGedcom());
+ // Ignore links and tags
+ $gedrec = preg_replace('/\n\d ' . WT_REGEX_TAG . '( @' . WT_REGEX_XREF . '@)?/', '', $gedrec);
+ // Ignore tags
+ $gedrec = preg_replace('/\n\d ' . WT_REGEX_TAG . ' ?/', '', $gedrec);
+ // Re-apply the filtering
+ $gedrec = I18N::strtoupper($gedrec);
+ foreach ($queryregex as $regex) {
+ if (!preg_match('/' . $regex . '/', $gedrec)) {
+ continue 2;
+ }
+ }
+ $list[] = $record;
+ }
+
+ return $list;
+ }
+
+ /**
+ * Search the names of the husb/wife in a family
+ *
+ * @param string[] $query Search terms
+ * @param Tree[] $trees The trees to search
+ *
+ * @return Family[]
+ */
+ public static function searchFamilyNames(array $query, array $trees) {
+ // No query => no results
+ if (!$query) {
+ return array();
+ }
+
+ $sql =
+ "SELECT DISTINCT f_id AS xref, f_file AS gedcom_id, f_gedcom AS gedcom" .
+ " FROM `##families`" .
+ " LEFT JOIN `##name` husb ON f_husb = husb.n_id AND f_file = husb.n_file" .
+ " LEFT JOIN `##name` wife ON f_wife = wife.n_id AND f_file = wife.n_file" .
+ " WHERE 1";
+ $args = array();
+
+ foreach ($query as $n => $q) {
+ $sql .= " AND (husb.n_full COLLATE :husb_collate_" . $n . " LIKE CONCAT('%', :husb_query_" . $n . ", '%') OR wife.n_full COLLATE :wife_collate_" . $n . " LIKE CONCAT('%', :wife_query_" . $n . ", '%'))";
+ $args['husb_collate_' . $n] = I18N::collation();
+ $args['husb_query_' . $n] = Filter::escapeLike($q);
+ $args['wife_collate_' . $n] = I18N::collation();
+ $args['wife_query_' . $n] = Filter::escapeLike($q);
+ }
+
+ $sql .= " AND f_file IN (";
+ foreach ($trees as $n => $tree) {
+ $sql .= $n ? ", " : "";
+ $sql .= ":tree_id_" . $n;
+ $args['tree_id_' . $n] = $tree->getTreeId();
+ }
+ $sql .= ")";
+
+ $list = array();
+ $rows = Database::prepare($sql)->execute($args)->fetchAll();
+ foreach ($rows as $row) {
+ $indi = Family::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
+ if ($indi->canShowName()) {
+ $list[] = $indi;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Search the sources
+ *
+ * @param string[] $query Search terms
+ * @param Tree[] $trees The tree to search
+ *
+ * @return Source[]
+ */
+ public static function searchSources($query, $trees) {
+ // Convert the query into a regular expression
+ $queryregex = array();
+
+ $sql = "SELECT s_id AS xref, s_file AS gedcom_id, s_gedcom AS gedcom FROM `##sources` WHERE 1";
+ $args = array();
+
+ foreach ($query as $n => $q) {
+ $queryregex[] = preg_quote(I18N::strtoupper($q), '/');
+ $sql .= " AND s_gedcom COLLATE :collate_" . $n . " LIKE CONCAT('%', :query_" . $n . ", '%')";
+ $args['collate_' . $n] = I18N::collation();
+ $args['query_' . $n] = Filter::escapeLike($q);
+ }
+
+ $sql .= " AND s_file IN (";
+ foreach ($trees as $n => $tree) {
+ $sql .= $n ? ", " : "";
+ $sql .= ":tree_id_" . $n;
+ $args['tree_id_' . $n] = $tree->getTreeId();
+ }
+ $sql .= ")";
+
+ $list = array();
+ $rows = Database::prepare($sql)->execute($args)->fetchAll();
+ foreach ($rows as $row) {
+ // SQL may have matched on private data or gedcom tags, so check again against privatized data.
+ $record = Source::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
+ // Ignore non-genealogy data
+ $gedrec = preg_replace('/\n\d (_UID|_WT_USER|FILE|FORM|TYPE|CHAN|REFN|RESN) .*/', '', $record->getGedcom());
+ // Ignore links and tags
+ $gedrec = preg_replace('/\n\d ' . WT_REGEX_TAG . '( @' . WT_REGEX_XREF . '@)?/', '', $gedrec);
+ // Ignore tags
+ $gedrec = preg_replace('/\n\d ' . WT_REGEX_TAG . ' ?/', '', $gedrec);
+ // Re-apply the filtering
+ $gedrec = I18N::strtoupper($gedrec);
+ foreach ($queryregex as $regex) {
+ if (!preg_match('/' . $regex . '/', $gedrec)) {
+ continue 2;
+ }
+ }
+ $list[] = $record;
+ }
+
+ return $list;
+ }
+
+ /**
+ * Search the shared notes
+ *
+ * @param string[] $query Search terms
+ * @param Tree[] $trees The tree to search
+ *
+ * @return Note[]
+ */
+ public static function searchNotes(array $query, array $trees) {
+ // Convert the query into a regular expression
+ $queryregex = array();
+
+ $sql = "SELECT o_id AS xref, o_file AS gedcom_id, o_gedcom AS gedcom FROM `##other` WHERE o_type = 'NOTE'";
+ $args = array();
+
+ foreach ($query as $n => $q) {
+ $queryregex[] = preg_quote(I18N::strtoupper($q), '/');
+ $sql .= " AND o_gedcom COLLATE :collate_" . $n . " LIKE CONCAT('%', :query_" . $n . ", '%')";
+ $args['collate_' . $n] = I18N::collation();
+ $args['query_' . $n] = Filter::escapeLike($q);
+ }
+
+ $sql .= " AND o_file IN (";
+ foreach ($trees as $n => $tree) {
+ $sql .= $n ? ", " : "";
+ $sql .= ":tree_id_" . $n;
+ $args['tree_id_' . $n] = $tree->getTreeId();
+ }
+ $sql .= ")";
+
+ $list = array();
+ $rows = Database::prepare($sql)->execute($args)->fetchAll();
+ foreach ($rows as $row) {
+ // SQL may have matched on private data or gedcom tags, so check again against privatized data.
+ $record = Note::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
+ // Ignore non-genealogy data
+ $gedrec = preg_replace('/\n\d (_UID|_WT_USER|FILE|FORM|TYPE|CHAN|REFN|RESN) .*/', '', $record->getGedcom());
+ // Ignore links and tags
+ $gedrec = preg_replace('/\n\d ' . WT_REGEX_TAG . '( @' . WT_REGEX_XREF . '@)?/', '', $gedrec);
+ // Ignore tags
+ $gedrec = preg_replace('/\n\d ' . WT_REGEX_TAG . ' ?/', '', $gedrec);
+ // Re-apply the filtering
+ $gedrec = I18N::strtoupper($gedrec);
+ foreach ($queryregex as $regex) {
+ if (!preg_match('/' . $regex . '/', $gedrec)) {
+ continue 2;
+ }
+ }
+ $list[] = $record;
+ }
+
+ return $list;
+ }
+
+ /**
+ * Search the repositories
+ *
+ * @param string[] $query Search terms
+ * @param Tree[] $trees The trees to search
+ *
+ * @return Repository[]
+ */
+ public static function searchRepositories(array $query, array $trees) {
+ // Convert the query into a regular expression
+ $queryregex = array();
+
+ $sql = "SELECT o_id AS xref, o_file AS gedcom_id, o_gedcom AS gedcom FROM `##other` WHERE o_type = 'REPO'";
+ $args = array();
+
+ foreach ($query as $n => $q) {
+ $queryregex[] = preg_quote(I18N::strtoupper($q), '/');
+ $sql .= " AND o_gedcom COLLATE :collate_" . $n . " LIKE CONCAT('%', :query_" . $n . ", '%')";
+ $args['collate_' . $n] = I18N::collation();
+ $args['query_' . $n] = Filter::escapeLike($q);
+ }
+
+ $sql .= " AND o_file IN (";
+ foreach ($trees as $n => $tree) {
+ $sql .= $n ? ", " : "";
+ $sql .= ":tree_id_" . $n;
+ $args['tree_id_' . $n] = $tree->getTreeId();
+ }
+ $sql .= ")";
+
+ $list = array();
+ $rows = Database::prepare($sql)->execute($args)->fetchAll();
+ foreach ($rows as $row) {
+ // SQL may have matched on private data or gedcom tags, so check again against privatized data.
+ $record = Repository::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
+ // Ignore non-genealogy data
+ $gedrec = preg_replace('/\n\d (_UID|_WT_USER|FILE|FORM|TYPE|CHAN|REFN|RESN) .*/', '', $record->getGedcom());
+ // Ignore links and tags
+ $gedrec = preg_replace('/\n\d ' . WT_REGEX_TAG . '( @' . WT_REGEX_XREF . '@)?/', '', $gedrec);
+ // Ignore tags
+ $gedrec = preg_replace('/\n\d ' . WT_REGEX_TAG . ' ?/', '', $gedrec);
+ // Re-apply the filtering
+ $gedrec = I18N::strtoupper($gedrec);
+ foreach ($queryregex as $regex) {
+ if (!preg_match('/' . $regex . '/', $gedrec)) {
+ continue 2;
+ }
+ }
+ $list[] = $record;
+ }
+
+ return $list;
+ }
+
+ /**
+ * Find the record for the given rin.
+ *
+ * @param string $rin
+ *
+ * @return string
+ */
+ public static function findRin($rin) {
+ global $WT_TREE;
+
+ $xref =
+ Database::prepare("SELECT i_id FROM `##individuals` WHERE i_rin=? AND i_file=?")
+ ->execute(array($rin, $WT_TREE->getTreeId()))
+ ->fetchOne();
+
+ return $xref ? $xref : $rin;
+ }
+
+ /**
+ * Get array of common surnames
+ *
+ * This function returns a simple array of the most common surnames
+ * found in the individuals list.
+ *
+ * @param int $min The number of times a surname must occur before it is added to the array
+ * @param Tree $tree
+ *
+ * @return mixed[][]
+ */
+ public static function getCommonSurnames($min, Tree $tree) {
+ $COMMON_NAMES_ADD = $tree->getPreference('COMMON_NAMES_ADD');
+ $COMMON_NAMES_REMOVE = $tree->getPreference('COMMON_NAMES_REMOVE');
+
+ $topsurns = self::getTopSurnames($tree->getTreeId(), $min, 0);
+ foreach (explode(',', $COMMON_NAMES_ADD) as $surname) {
+ if ($surname && !array_key_exists($surname, $topsurns)) {
+ $topsurns[$surname] = $min;
+ }
+ }
+ foreach (explode(',', $COMMON_NAMES_REMOVE) as $surname) {
+ unset($topsurns[I18N::strtoupper($surname)]);
+ }
+
+ //-- check if we found some, else recurse
+ if (empty($topsurns) && $min > 2) {
+ return self::getCommonSurnames($min / 2, $tree);
+ } else {
+ uksort($topsurns, '\Fisharebest\Webtrees\I18N::strcasecmp');
+ foreach ($topsurns as $key => $value) {
+ $topsurns[$key] = array('name' => $key, 'match' => $value);
+ }
+
+ return $topsurns;
+ }
+ }
+
+ /**
+ * get the top surnames
+ *
+ * @param int $ged_id fetch surnames from this gedcom
+ * @param int $min only fetch surnames occuring this many times
+ * @param int $max only fetch this number of surnames (0=all)
+ *
+ * @return string[]
+ */
+ public static function getTopSurnames($ged_id, $min, $max) {
+ // Use n_surn, rather than n_surname, as it is used to generate URLs for
+ // the indi-list, etc.
+ $max = (int) $max;
+ if ($max == 0) {
+ return
+ Database::prepare(
+ "SELECT SQL_CACHE n_surn, COUNT(n_surn) FROM `##name`" .
+ " WHERE n_file = :tree_id AND n_type != '_MARNM' AND n_surn NOT IN ('@N.N.', '', '?', 'UNKNOWN')" .
+ " GROUP BY n_surn HAVING COUNT(n_surn) >= :min" .
+ " ORDER BY 2 DESC"
+ )->execute(array(
+ 'tree_id' => $ged_id,
+ 'min' => $min,
+ ))->fetchAssoc();
+ } else {
+ return
+ Database::prepare(
+ "SELECT SQL_CACHE n_surn, COUNT(n_surn) FROM `##name`" .
+ " WHERE n_file = :tree_id AND n_type != '_MARNM' AND n_surn NOT IN ('@N.N.', '', '?', 'UNKNOWN')" .
+ " GROUP BY n_surn HAVING COUNT(n_surn) >= :min" .
+ " ORDER BY 2 DESC" .
+ " LIMIT :limit"
+ )->execute(array(
+ 'tree_id' => $ged_id,
+ 'min' => $min,
+ 'limit' => $max,
+ ))->fetchAssoc();
+ }
+ }
+
+ /**
+ * Get a list of events whose anniversary occured on a given julian day.
+ * Used on the on-this-day/upcoming blocks and the day/month calendar views.
+ *
+ * @param int $jd the julian day
+ * @param string $facts restrict the search to just these facts or leave blank for all
+ * @param Tree $tree the tree to search
+ *
+ * @return Fact[]
+ */
+ public static function getAnniversaryEvents($jd, $facts, Tree $tree) {
+ $found_facts = array();
+ foreach (array(
+ new GregorianDate($jd),
+ new JulianDate($jd),
+ new FrenchDate($jd),
+ new JewishDate($jd),
+ new HijriDate($jd),
+ new JalaliDate($jd),
+ ) as $anniv) {
+ // Build a SQL where clause to match anniversaries in the appropriate calendar.
+ $ind_sql =
+ "SELECT DISTINCT i_id AS xref, i_gedcom AS gedcom, d_type, d_day, d_month, d_year, d_fact" .
+ " FROM `##dates` JOIN `##individuals` ON d_gid = i_id AND d_file = i_file" .
+ " WHERE d_type = :type AND d_file = :tree_id";
+ $fam_sql =
+ "SELECT DISTINCT f_id AS xref, f_gedcom AS gedcom, d_type, d_day, d_month, d_year, d_fact" .
+ " FROM `##dates` JOIN `##families` ON d_gid = f_id AND d_file = f_file" .
+ " WHERE d_type = :type AND d_file = :tree_id";
+ $args = array(
+ 'type' => $anniv->format('%@'),
+ 'tree_id' => $tree->getTreeId(),
+ );
+
+ $where = "";
+ // SIMPLE CASES:
+ // a) Non-hebrew anniversaries
+ // b) Hebrew months TVT, SHV, IYR, SVN, TMZ, AAV, ELL
+ if (!$anniv instanceof JewishDate || in_array($anniv->m, array(1, 5, 6, 9, 10, 11, 12, 13))) {
+ // Dates without days go on the first day of the month
+ // Dates with invalid days go on the last day of the month
+ if ($anniv->d === 1) {
+ $where .= " AND d_day <= 1";
+ } elseif ($anniv->d === $anniv->daysInMonth()) {
+ $where .= " AND d_day >= :day";
+ $args['day'] = $anniv->d;
+ } else {
+ $where .= " AND d_day = :day";
+ $args['day'] = $anniv->d;
+ }
+ $where .= " AND d_mon = :month";
+ $args['month'] = $anniv->m;
+ } else {
+ // SPECIAL CASES:
+ switch ($anniv->m) {
+ case 2:
+ // 29 CSH does not include 30 CSH (but would include an invalid 31 CSH if there were no 30 CSH)
+ if ($anniv->d === 1) {
+ $where .= " AND d_day <= 1 AND d_mon = 2";
+ } elseif ($anniv->d === 30) {
+ $where .= " AND d_day >= 30 AND d_mon = 2";
+ } elseif ($anniv->d === 29 && $anniv->daysInMonth() === 29) {
+ $where .= " AND (d_day = 29 OR d_day > 30) AND d_mon = 2";
+ } else {
+ $where .= " AND d_day = :day AND d_mon = 2";
+ $args['day'] = $anniv->d;
+ }
+ break;
+ case 3:
+ // 1 KSL includes 30 CSH (if this year didn’t have 30 CSH)
+ // 29 KSL does not include 30 KSL (but would include an invalid 31 KSL if there were no 30 KSL)
+ if ($anniv->d === 1) {
+ $tmp = new JewishDate(array($anniv->y, 'CSH', 1));
+ if ($tmp->daysInMonth() === 29) {
+ $where .= " AND (d_day <= 1 AND d_mon = 3 OR d_day = 30 AND d_mon = 2)";
+ } else {
+ $where .= " AND d_day <= 1 AND d_mon = 3";
+ }
+ } elseif ($anniv->d === 30) {
+ $where .= " AND d_day >= 30 AND d_mon = 3";
+ } elseif ($anniv->d == 29 && $anniv->daysInMonth() === 29) {
+ $where .= " AND (d_day = 29 OR d_day > 30) AND d_mon = 3";
+ } else {
+ $where .= " AND d_day = :day AND d_mon = 3";
+ $args['day'] = $anniv->d;
+ }
+ break;
+ case 4:
+ // 1 TVT includes 30 KSL (if this year didn’t have 30 KSL)
+ if ($anniv->d === 1) {
+ $tmp = new JewishDate(array($anniv->y, 'KSL', 1));
+ if ($tmp->daysInMonth() === 29) {
+ $where .= " AND (d_day <=1 AND d_mon = 4 OR d_day = 30 AND d_mon = 3)";
+ } else {
+ $where .= " AND d_day <= 1 AND d_mon = 4";
+ }
+ } elseif ($anniv->d === $anniv->daysInMonth()) {
+ $where .= " AND d_day >= :day AND d_mon=4";
+ $args['day'] = $anniv->d;
+ } else {
+ $where .= " AND d_day = :day AND d_mon=4";
+ $args['day'] = $anniv->d;
+ }
+ break;
+ case 7: // ADS includes ADR (non-leap)
+ if ($anniv->d === 1) {
+ $where .= " AND d_day <= 1";
+ } elseif ($anniv->d === $anniv->daysInMonth()) {
+ $where .= " AND d_day >= :day";
+ $args['day'] = $anniv->d;
+ } else {
+ $where .= " AND d_day = :day";
+ $args['day'] = $anniv->d;
+ }
+ $where .= " AND (d_mon = 6 AND MOD(7 * d_year + 1, 19) >= 7 OR d_mon = 7)";
+ break;
+ case 8: // 1 NSN includes 30 ADR, if this year is non-leap
+ if ($anniv->d === 1) {
+ if ($anniv->isLeapYear()) {
+ $where .= " AND d_day <= 1 AND d_mon = 8";
+ } else {
+ $where .= " AND (d_day <= 1 AND d_mon = 8 OR d_day = 30 AND d_mon = 6)";
+ }
+ } elseif ($anniv->d === $anniv->daysInMonth()) {
+ $where .= " AND d_day >= :day AND d_mon = 8";
+ $args['day'] = $anniv->d;
+ } else {
+ $where .= " AND d_day = :day AND d_mon = 8";
+ $args['day'] = $anniv->d;
+ }
+ break;
+ }
+ }
+ // Only events in the past (includes dates without a year)
+ $where .= " AND d_year <= :year";
+ $args['year'] = $anniv->y;
+
+ if ($facts) {
+ // Restrict to certain types of fact
+ $where .= " AND d_fact IN (";
+ preg_match_all('/([_A-Z]+)/', $facts, $matches);
+ foreach ($matches[1] as $n => $fact) {
+ $where .= $n ? ", " : "";
+ $where .= ":fact_" . $n;
+ $args['fact_' . $n] = $fact;
+ }
+ $where .= ")";
+ } else {
+ // If no facts specified, get all except these
+ $where .= " AND d_fact NOT IN ('CHAN', 'BAPL', 'SLGC', 'SLGS', 'ENDL', 'CENS', 'RESI', '_TODO')";
+ }
+
+ $order_by = " ORDER BY d_day, d_year DESC";
+
+ // Now fetch these anniversaries
+ foreach (array('INDI' => $ind_sql . $where . $order_by, 'FAM' => $fam_sql . $where . $order_by) as $type => $sql) {
+ $rows = Database::prepare($sql)->execute($args)->fetchAll();
+ foreach ($rows as $row) {
+ if ($type === 'INDI') {
+ $record = Individual::getInstance($row->xref, $tree, $row->gedcom);
+ } else {
+ $record = Family::getInstance($row->xref, $tree, $row->gedcom);
+ }
+ $anniv_date = new Date($row->d_type . ' ' . $row->d_day . ' ' . $row->d_month . ' ' . $row->d_year);
+ foreach ($record->getFacts() as $fact) {
+ if (($fact->getDate()->minimumDate() == $anniv_date->minimumDate() || $fact->getDate()->maximumDate() == $anniv_date->minimumDate()) && $fact->getTag() === $row->d_fact) {
+ $fact->anniv = $row->d_year === 0 ? 0 : $anniv->y - $row->d_year;
+ $found_facts[] = $fact;
+ }
+ }
+ }
+ }
+ }
+
+ return $found_facts;
+ }
+
+ /**
+ * Get a list of events which occured during a given date range.
+ *
+ * @param int $jd1 the start range of julian day
+ * @param int $jd2 the end range of julian day
+ * @param string $facts restrict the search to just these facts or leave blank for all
+ * @param Tree $tree the tree to search
+ *
+ * @return Fact[]
+ */
+ public static function getCalendarEvents($jd1, $jd2, $facts, Tree $tree) {
+ // If no facts specified, get all except these
+ $skipfacts = "CHAN,BAPL,SLGC,SLGS,ENDL,CENS,RESI,NOTE,ADDR,OBJE,SOUR,PAGE,DATA,TEXT";
+ if ($facts != '_TODO') {
+ $skipfacts .= ',_TODO';
+ }
+
+ $found_facts = array();
+
+ // Events that start or end during the period
+ $where = "WHERE (d_julianday1>={$jd1} AND d_julianday1<={$jd2} OR d_julianday2>={$jd1} AND d_julianday2<={$jd2})";
+
+ // Restrict to certain types of fact
+ if (empty($facts)) {
+ $excl_facts = "'" . preg_replace('/\W+/', "','", $skipfacts) . "'";
+ $where .= " AND d_fact NOT IN ({$excl_facts})";
+ } else {
+ $incl_facts = "'" . preg_replace('/\W+/', "','", $facts) . "'";
+ $where .= " AND d_fact IN ({$incl_facts})";
+ }
+ // Only get events from the current gedcom
+ $where .= " AND d_file=" . $tree->getTreeId();
+
+ // Now fetch these events
+ $ind_sql = "SELECT d_gid AS xref, i_gedcom AS gedcom, d_type, d_day, d_month, d_year, d_fact, d_type FROM `##dates`, `##individuals` {$where} AND d_gid=i_id AND d_file=i_file GROUP BY d_julianday1, d_gid ORDER BY d_julianday1";
+ $fam_sql = "SELECT d_gid AS xref, f_gedcom AS gedcom, d_type, d_day, d_month, d_year, d_fact, d_type FROM `##dates`, `##families` {$where} AND d_gid=f_id AND d_file=f_file GROUP BY d_julianday1, d_gid ORDER BY d_julianday1";
+ foreach (array('INDI' => $ind_sql, 'FAM' => $fam_sql) as $type => $sql) {
+ $rows = Database::prepare($sql)->fetchAll();
+ foreach ($rows as $row) {
+ if ($type === 'INDI') {
+ $record = Individual::getInstance($row->xref, $tree, $row->gedcom);
+ } else {
+ $record = Family::getInstance($row->xref, $tree, $row->gedcom);
+ }
+ $anniv_date = new Date($row->d_type . ' ' . $row->d_day . ' ' . $row->d_month . ' ' . $row->d_year);
+ foreach ($record->getFacts() as $fact) {
+ if (($fact->getDate()->minimumDate() == $anniv_date->minimumDate() || $fact->getDate()->maximumDate() == $anniv_date->minimumDate()) && $fact->getTag() === $row->d_fact) {
+ $fact->anniv = 0;
+ $found_facts[] = $fact;
+ }
+ }
+ }
+ }
+
+ return $found_facts;
+ }
+
+ /**
+ * Get the list of current and upcoming events, sorted by anniversary date
+ *
+ * @param int $jd1
+ * @param int $jd2
+ * @param string $events
+ * @param Tree $tree
+ *
+ * @return Fact[]
+ */
+ public static function getEventsList($jd1, $jd2, $events, Tree $tree) {
+ $found_facts = array();
+ for ($jd = $jd1; $jd <= $jd2; ++$jd) {
+ $found_facts = array_merge($found_facts, self::getAnniversaryEvents($jd, $events, $tree));
+ }
+
+ return $found_facts;
+ }
+
+ /**
+ * Check if a media file is shared (i.e. used by another gedcom)
+ *
+ * @param string $file_name
+ * @param int $ged_id
+ *
+ * @return bool
+ */
+ public static function isMediaUsedInOtherTree($file_name, $ged_id) {
+ return
+ (bool) Database::prepare("SELECT COUNT(*) FROM `##media` WHERE m_filename LIKE ? AND m_file<>?")
+ ->execute(array("%{$file_name}", $ged_id))
+ ->fetchOne();
+ }
+
+ /**
+ * @param int $user_id
+ *
+ * @return string[][]
+ */
+ public static function getUserBlocks($user_id) {
+ global $WT_TREE;
+
+ $blocks = array('main' => array(), 'side' => array());
+ $rows = Database::prepare(
+ "SELECT SQL_CACHE location, block_id, module_name" .
+ " FROM `##block`" .
+ " JOIN `##module` USING (module_name)" .
+ " JOIN `##module_privacy` USING (module_name)" .
+ " WHERE user_id=?" .
+ " AND status='enabled'" .
+ " AND `##module_privacy`.gedcom_id=?" .
+ " AND access_level>=?" .
+ " ORDER BY location, block_order"
+ )->execute(array($user_id, $WT_TREE->getTreeId(), Auth::accessLevel($WT_TREE)))->fetchAll();
+ foreach ($rows as $row) {
+ $blocks[$row->location][$row->block_id] = $row->module_name;
+ }
+
+ return $blocks;
+ }
+
+ /**
+ * Get the blocks for the specified tree
+ *
+ * @param int $gedcom_id
+ *
+ * @return string[][]
+ */
+ public static function getTreeBlocks($gedcom_id) {
+ if ($gedcom_id < 0) {
+ $access_level = Auth::PRIV_NONE;
+ } else {
+ $access_level = Auth::accessLevel(Tree::findById($gedcom_id));
+ }
+
+ $blocks = array('main' => array(), 'side' => array());
+ $rows = Database::prepare(
+ "SELECT SQL_CACHE location, block_id, module_name" .
+ " FROM `##block`" .
+ " JOIN `##module` USING (module_name)" .
+ " JOIN `##module_privacy` USING (module_name, gedcom_id)" .
+ " WHERE gedcom_id = :tree_id" .
+ " AND status='enabled'" .
+ " AND access_level >= :access_level" .
+ " ORDER BY location, block_order"
+ )->execute(array(
+ 'tree_id' => $gedcom_id,
+ 'access_level' => $access_level,
+ ))->fetchAll();
+ foreach ($rows as $row) {
+ $blocks[$row->location][$row->block_id] = $row->module_name;
+ }
+
+ return $blocks;
+ }
+
+ /**
+ * Update favorites after merging records.
+ *
+ * @param string $xref_from
+ * @param string $xref_to
+ * @param Tree $tree
+ *
+ * @return int
+ */
+ public static function updateFavorites($xref_from, $xref_to, Tree $tree) {
+ return
+ Database::prepare("UPDATE `##favorite` SET xref=? WHERE xref=? AND gedcom_id=?")
+ ->execute(array($xref_to, $xref_from, $tree->getTreeId()))
+ ->rowCount();
+ }
+}