summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/ConfigData.php2
-rw-r--r--app/Controller/AdvancedSearchController.php3
-rw-r--r--app/Controller/AncestryController.php8
-rw-r--r--app/Controller/DescendancyController.php12
-rw-r--r--app/Controller/FamilyBookController.php9
-rw-r--r--app/Controller/FamilyController.php11
-rw-r--r--app/Controller/HourglassController.php9
-rw-r--r--app/Controller/IndividualController.php9
-rw-r--r--app/Controller/LifespanController.php3
-rw-r--r--app/Controller/MediaController.php3
-rw-r--r--app/Controller/PageController.php3
-rw-r--r--app/Controller/PedigreeController.php2
-rw-r--r--app/Controller/SearchController.php22
-rw-r--r--app/Controller/TimelineController.php8
-rw-r--r--app/Fact.php5
-rw-r--r--app/Functions/FunctionsCharts.php556
-rw-r--r--app/Functions/FunctionsDate.php130
-rw-r--r--app/Functions/FunctionsDb.php1081
-rw-r--r--app/Functions/FunctionsEdit.php1715
-rw-r--r--app/Functions/FunctionsExport.php314
-rw-r--r--app/Functions/FunctionsImport.php1162
-rw-r--r--app/Functions/FunctionsMedia.php97
-rw-r--r--app/Functions/FunctionsPrint.php873
-rw-r--r--app/Functions/FunctionsPrintFacts.php1150
-rw-r--r--app/Functions/FunctionsPrintLists.php2297
-rw-r--r--app/Functions/FunctionsRtl.php1154
-rw-r--r--app/Functions/functions.php2225
-rw-r--r--app/GedcomRecord.php18
-rw-r--r--app/I18N.php2
-rw-r--r--app/Media.php6
-rw-r--r--app/Module/AlbumModule.php3
-rw-r--r--app/Module/BatchUpdateModule.php3
-rw-r--r--app/Module/CensusAssistantModule.php5
-rw-r--r--app/Module/ChartsBlockModule.php10
-rw-r--r--app/Module/ClippingsCart/ClippingsCartController.php5
-rw-r--r--app/Module/ClippingsCartModule.php13
-rw-r--r--app/Module/ExtraInformationModule.php3
-rw-r--r--app/Module/FamilyNavigatorModule.php5
-rw-r--r--app/Module/FamilyTreeFavoritesModule.php16
-rw-r--r--app/Module/FamilyTreeNewsModule.php6
-rw-r--r--app/Module/FamilyTreeStatisticsModule.php8
-rw-r--r--app/Module/FrequentlyAskedQuestionsModule.php7
-rw-r--r--app/Module/GoogleMapsModule.php34
-rw-r--r--app/Module/HtmlBlockModule.php8
-rw-r--r--app/Module/IndividualFactsTabModule.php9
-rw-r--r--app/Module/LoggedInUsersModule.php3
-rw-r--r--app/Module/MediaTabModule.php11
-rw-r--r--app/Module/NotesTabModule.php8
-rw-r--r--app/Module/OnThisDayModule.php16
-rw-r--r--app/Module/RecentChangesModule.php17
-rw-r--r--app/Module/RelativesTabModule.php7
-rw-r--r--app/Module/ResearchTaskModule.php12
-rw-r--r--app/Module/ReviewChangesModule.php10
-rw-r--r--app/Module/SlideShowModule.php10
-rw-r--r--app/Module/SourcesTabModule.php8
-rw-r--r--app/Module/StoriesModule.php8
-rw-r--r--app/Module/TopGivenNamesModule.php5
-rw-r--r--app/Module/TopPageViewsModule.php5
-rw-r--r--app/Module/TopSurnamesModule.php17
-rw-r--r--app/Module/UpcomingAnniversariesModule.php16
-rw-r--r--app/Module/UserJournalModule.php3
-rw-r--r--app/Module/UserMessagesModule.php6
-rw-r--r--app/Module/YahrzeitModule.php10
-rw-r--r--app/Report/ReportHtml.php3
-rw-r--r--app/Report/ReportParserGenerate.php69
-rw-r--r--app/Report/ReportPdfCell.php3
-rw-r--r--app/Report/ReportPdfText.php3
-rw-r--r--app/Schema/Migration22.php2
-rw-r--r--app/Stats.php43
-rw-r--r--app/Theme/AdministrationTheme.php2
-rw-r--r--app/Theme/BaseTheme.php13
-rw-r--r--app/Theme/ColorsTheme.php3
-rw-r--r--app/Tree.php8
73 files changed, 13100 insertions, 245 deletions
diff --git a/app/ConfigData.php b/app/ConfigData.php
index 1a4fdc1356..3cf8ec9c5a 100644
--- a/app/ConfigData.php
+++ b/app/ConfigData.php
@@ -204,7 +204,7 @@ class ConfigData {
*/
public static function standardNameFacts() {
return array(
- 'NAME', 'NPFX', 'GIVN', 'SPFX', 'SURN', 'NSFX'
+ 'NAME', 'NPFX', 'GIVN', 'SPFX', 'SURN', 'NSFX',
);
}
}
diff --git a/app/Controller/AdvancedSearchController.php b/app/Controller/AdvancedSearchController.php
index 120323f121..51911f08d1 100644
--- a/app/Controller/AdvancedSearchController.php
+++ b/app/Controller/AdvancedSearchController.php
@@ -17,6 +17,7 @@ namespace Fisharebest\Webtrees\Controller;
*/
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Date;
+use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -638,7 +639,7 @@ class AdvancedSearchController extends SearchController {
public function printResults() {
if ($this->myindilist) {
uasort($this->myindilist, '\Fisharebest\Webtrees\GedcomRecord::compare');
- echo format_indi_table($this->myindilist);
+ echo FunctionsPrintLists::individualTable($this->myindilist);
} elseif (array_filter($this->values)) {
echo '<p class="ui-state-highlight">', I18N::translate('No results found.'), '</p>';
}
diff --git a/app/Controller/AncestryController.php b/app/Controller/AncestryController.php
index 2d374664aa..51f31f0491 100644
--- a/app/Controller/AncestryController.php
+++ b/app/Controller/AncestryController.php
@@ -16,6 +16,8 @@ namespace Fisharebest\Webtrees\Controller;
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsCharts;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Theme;
@@ -72,13 +74,13 @@ class AncestryController extends ChartController {
echo '<img src="', Theme::theme()->parameter('image-spacer'), '" height="3" width="2" alt="">';
echo '<img src="', Theme::theme()->parameter('image-hline'), '" height="3" width="', Theme::theme()->parameter('chart-descendancy-indent') - 2, '"></td><td>';
}
- print_pedigree_person($individual, $this->showFull());
+ FunctionsPrint::printPedigreePerson($individual, $this->showFull());
echo '</td><td>';
if ($sosa > 1) {
- print_url_arrow('?rootid=' . $individual->getXref() . '&amp;PEDIGREE_GENERATIONS=' . $this->generations . '&amp;show_full=' . $this->showFull() . '&amp;chart_style=' . $this->chart_style . '&amp;ged=' . $individual->getTree()->getNameUrl(), I18N::translate('Ancestors of %s', $individual->getFullName()), 3);
+ FunctionsCharts::printUrlArrow('?rootid=' . $individual->getXref() . '&amp;PEDIGREE_GENERATIONS=' . $this->generations . '&amp;show_full=' . $this->showFull() . '&amp;chart_style=' . $this->chart_style . '&amp;ged=' . $individual->getTree()->getNameUrl(), I18N::translate('Ancestors of %s', $individual->getFullName()), 3);
}
echo '</td><td class="details1">&nbsp;<span class="person_box' . ($sosa === 1 ? 'NN' : ($sosa % 2 ? 'F' : '')) . '">', I18N::number($sosa), '</span> ';
- echo '</td><td class="details1">&nbsp;', get_sosa_name($sosa), '</td>';
+ echo '</td><td class="details1">&nbsp;', FunctionsCharts::getSosaName($sosa), '</td>';
echo '</tr></tbody></table>';
// Parents
diff --git a/app/Controller/DescendancyController.php b/app/Controller/DescendancyController.php
index 3b45faf911..d2bfe2424a 100644
--- a/app/Controller/DescendancyController.php
+++ b/app/Controller/DescendancyController.php
@@ -18,6 +18,8 @@ namespace Fisharebest\Webtrees\Controller;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsCharts;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -77,7 +79,7 @@ class DescendancyController extends ChartController {
return;
}
foreach ($person->getSpouseFamilies() as $family) {
- print_sosa_family($family->getXref(), '', -1, $label, $person->getXref(), $gpid, 0, $this->showFull());
+ FunctionsCharts::printSosaFamily($family->getXref(), '', -1, $label, $person->getXref(), $gpid, 0, $this->showFull());
$i = 1;
foreach ($family->getChildren() as $child) {
$this->printChildFamily($child, $depth - 1, $label . ($i++) . '.', $person->getXref());
@@ -100,7 +102,7 @@ class DescendancyController extends ChartController {
echo "<img src=\"" . Theme::theme()->parameter('image-spacer') . "\" height=\"3\" width=\"3\" alt=\"\">";
echo "<img src=\"" . Theme::theme()->parameter('image-hline') . "\" height=\"3\" width=\"", Theme::theme()->parameter('chart-descendancy-indent') - 3, "\" alt=\"\"></td><td>";
}
- print_pedigree_person($person, $this->showFull());
+ FunctionsPrint::printPedigreePerson($person, $this->showFull());
echo '</td>';
// check if child has parents and add an arrow
@@ -108,7 +110,7 @@ class DescendancyController extends ChartController {
echo '<td>';
foreach ($person->getChildFamilies() as $cfamily) {
foreach ($cfamily->getSpouses() as $parent) {
- print_url_arrow('?rootid=' . $parent->getXref() . '&amp;generations=' . $this->generations . '&amp;chart_style=' . $this->chart_style . '&amp;show_full=' . $this->showFull() . '&amp;ged=' . $parent->getTree()->getNameUrl(), I18N::translate('Start at parents'), 2);
+ FunctionsCharts::printUrlArrow('?rootid=' . $parent->getXref() . '&amp;generations=' . $this->generations . '&amp;chart_style=' . $this->chart_style . '&amp;show_full=' . $this->showFull() . '&amp;ged=' . $parent->getTree()->getNameUrl(), I18N::translate('Start at parents'), 2);
// only show the arrow for one of the parents
break;
}
@@ -176,7 +178,7 @@ class DescendancyController extends ChartController {
echo '<ul id="' . $uid . '" class="generation">';
echo '<li>';
echo '<table><tr><td>';
- print_pedigree_person($spouse, $this->showFull());
+ FunctionsPrint::printPedigreePerson($spouse, $this->showFull());
echo '</td>';
// check if spouse has parents and add an arrow
@@ -185,7 +187,7 @@ class DescendancyController extends ChartController {
if ($spouse) {
foreach ($spouse->getChildFamilies() as $cfamily) {
foreach ($cfamily->getSpouses() as $parent) {
- print_url_arrow('?rootid=' . $parent->getXref() . '&amp;generations=' . $this->generations . '&amp;chart_style=' . $this->chart_style . '&amp;show_full=' . $this->showFull() . '&amp;ged=' . $parent->getTree()->getNameUrl(), I18N::translate('Start at parents'), 2);
+ FunctionsCharts::printUrlArrow('?rootid=' . $parent->getXref() . '&amp;generations=' . $this->generations . '&amp;chart_style=' . $this->chart_style . '&amp;show_full=' . $this->showFull() . '&amp;ged=' . $parent->getTree()->getNameUrl(), I18N::translate('Start at parents'), 2);
// only show the arrow for one of the parents
break;
}
diff --git a/app/Controller/FamilyBookController.php b/app/Controller/FamilyBookController.php
index ad2dd42f72..2aac9d6ef9 100644
--- a/app/Controller/FamilyBookController.php
+++ b/app/Controller/FamilyBookController.php
@@ -17,6 +17,7 @@ namespace Fisharebest\Webtrees\Controller;
*/
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Theme;
@@ -152,7 +153,7 @@ class FamilyBookController extends ChartController {
}
echo '<table><tr><td>';
if ($person) {
- print_pedigree_person($person, $this->showFull());
+ FunctionsPrint::printPedigreePerson($person, $this->showFull());
echo '</td><td>',
'<img class="line2" src="', Theme::theme()->parameter('image-hline'), '" width="8" height="3" alt="">';
} else {
@@ -166,7 +167,7 @@ class FamilyBookController extends ChartController {
foreach ($person->getSpouseFamilies() as $family) {
$spouse = $family->getSpouse($person);
echo '</td></tr><tr><td>';
- print_pedigree_person($spouse, $this->showFull());
+ FunctionsPrint::printPedigreePerson($spouse, $this->showFull());
$numkids += 0.95;
echo '</td><td>';
}
@@ -259,7 +260,7 @@ class FamilyBookController extends ChartController {
'<td>';
$lh = $savlh; // restore original line height
//-- print the father box
- print_pedigree_person($family->getHusband(), $this->showFull());
+ FunctionsPrint::printPedigreePerson($family->getHusband(), $this->showFull());
echo '</td>';
if ($family->getHusband()) {
echo '<td>';
@@ -282,7 +283,7 @@ class FamilyBookController extends ChartController {
'<td><img class="line4" src="', Theme::theme()->parameter('image-hline'), '" height="3"></td>',
'<td>';
//-- print the mother box
- print_pedigree_person($family->getWife(), $this->showFull());
+ FunctionsPrint::printPedigreePerson($family->getWife(), $this->showFull());
echo '</td>';
if ($family->getWife()) {
echo '<td>';
diff --git a/app/Controller/FamilyController.php b/app/Controller/FamilyController.php
index e657168ee3..d0d31f3403 100644
--- a/app/Controller/FamilyController.php
+++ b/app/Controller/FamilyController.php
@@ -18,6 +18,9 @@ namespace Fisharebest\Webtrees\Controller;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\Functions;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
+use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Menu;
@@ -177,16 +180,16 @@ class FamilyController extends GedcomRecordController {
$indifacts = $this->record->getFacts();
if ($indifacts) {
- sort_facts($indifacts);
+ Functions::sortFacts($indifacts);
foreach ($indifacts as $fact) {
- print_fact($fact, $this->record);
+ FunctionsPrintFacts::printFact($fact, $this->record);
}
} else {
echo '<tr><td class="messagebox" colspan="2">', I18N::translate('No facts for this family.'), '</td></tr>';
}
if (Auth::isEditor($this->record->getTree())) {
- print_add_new_fact($this->record->getXref(), $indifacts, 'FAM');
+ FunctionsPrint::printAddNewFact($this->record->getXref(), $indifacts, 'FAM');
echo '<tr><td class="descriptionbox">';
echo I18N::translate('Note');
@@ -205,7 +208,7 @@ class FamilyController extends GedcomRecordController {
echo I18N::translate('Media object');
echo '</td><td class="optionbox">';
echo "<a href=\"#\" onclick=\"window.open('addmedia.php?action=showmediaform&amp;linktoid=" . $this->record->getXref() . "', '_blank', edit_window_specs); return false;\">", I18N::translate('Add a new media object'), '</a>';
- echo help_link('OBJE');
+ echo FunctionsPrint::helpLink('OBJE');
echo '<br>';
echo "<a href=\"#\" onclick=\"window.open('inverselink.php?linktoid=" . $this->record->getXref() . "&amp;linkto=family', '_blank', find_window_specs); return false;\">", I18N::translate('Link to an existing media object'), '</a>';
echo '</td></tr>';
diff --git a/app/Controller/HourglassController.php b/app/Controller/HourglassController.php
index e0073a3908..abcf723008 100644
--- a/app/Controller/HourglassController.php
+++ b/app/Controller/HourglassController.php
@@ -17,6 +17,7 @@ namespace Fisharebest\Webtrees\Controller;
*/
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Theme;
@@ -120,7 +121,7 @@ class HourglassController extends ChartController {
echo '<td><img class="line4" src="' . Theme::theme()->parameter('image-hline') . '" width="7" height="3"></td>';
echo '<td>';
//-- print the father box
- print_pedigree_person($family->getHusband(), $this->showFull());
+ FunctionsPrint::printPedigreePerson($family->getHusband(), $this->showFull());
echo "</td>";
if ($family->getHusband()) {
$ARID = $family->getHusband()->getXref();
@@ -152,7 +153,7 @@ class HourglassController extends ChartController {
'<td><img class="line4" src="' . Theme::theme()->parameter('image-hline') . '" width="7" height="3" alt=""></td>',
'<td>';
//-- print the mother box
- print_pedigree_person($family->getWife(), $this->showFull());
+ FunctionsPrint::printPedigreePerson($family->getWife(), $this->showFull());
echo '</td>';
if ($family->getWife()) {
$ARID = $family->getWife()->getXref();
@@ -285,7 +286,7 @@ class HourglassController extends ChartController {
}
echo '<table id="table2_' . $pid . '"><tr><td>';
- print_pedigree_person($person, $this->showFull());
+ FunctionsPrint::printPedigreePerson($person, $this->showFull());
echo '</td><td><img class="line2" src="' . Theme::theme()->parameter('image-hline') . '" width="7" height="3">';
//----- Print the spouse
@@ -297,7 +298,7 @@ class HourglassController extends ChartController {
$temph = $this->getBoxDimensions()->height;
$this->getBoxDimensions()->width -= 10;
$this->getBoxDimensions()->height -= 10;
- print_pedigree_person($family->getSpouse($person), $this->showFull());
+ FunctionsPrint::printPedigreePerson($family->getSpouse($person), $this->showFull());
$this->getBoxDimensions()->width = $tempw;
$this->getBoxDimensions()->height = $temph;
$numkids += 0.95;
diff --git a/app/Controller/IndividualController.php b/app/Controller/IndividualController.php
index 995f764779..8777a44a3f 100644
--- a/app/Controller/IndividualController.php
+++ b/app/Controller/IndividualController.php
@@ -21,6 +21,9 @@ use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Fact;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDb;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
+use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
use Fisharebest\Webtrees\GedcomCode\GedcomCodeName;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
@@ -48,7 +51,7 @@ class IndividualController extends GedcomRecordController {
$this->record = Individual::getInstance($xref, $WT_TREE);
if (!$this->record && $WT_TREE->getPreference('USE_RIN')) {
- $rin = find_rin_id($xref);
+ $rin = FunctionsDb::findRin($xref);
$this->record = Individual::getInstance($rin, $WT_TREE);
}
@@ -209,10 +212,10 @@ class IndividualController extends GedcomRecordController {
echo '</div>';
}
if (preg_match("/\n2 SOUR/", $factrec)) {
- echo '<div id="indi_sour" class="clearfloat">', print_fact_sources($factrec, 2), '</div>';
+ echo '<div id="indi_sour" class="clearfloat">', FunctionsPrintFacts::printFactSources($factrec, 2), '</div>';
}
if (preg_match("/\n2 NOTE/", $factrec)) {
- echo '<div id="indi_note" class="clearfloat">', print_fact_notes($factrec, 2), '</div>';
+ echo '<div id="indi_note" class="clearfloat">', FunctionsPrint::printFactNotes($factrec, 2), '</div>';
}
echo '</div>';
}
diff --git a/app/Controller/LifespanController.php b/app/Controller/LifespanController.php
index f0468bcd8f..bcd062a384 100644
--- a/app/Controller/LifespanController.php
+++ b/app/Controller/LifespanController.php
@@ -20,6 +20,7 @@ use Fisharebest\ExtCalendar\GregorianCalendar;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Date;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -430,7 +431,7 @@ class LifespanController extends PageController {
$unparsedEvents[] = $fact;
}
}
- sort_facts($unparsedEvents);
+ Functions::sortFacts($unparsedEvents);
$eventinformation = array();
foreach ($unparsedEvents as $val) {
diff --git a/app/Controller/MediaController.php b/app/Controller/MediaController.php
index eba6c2d301..b7b968a473 100644
--- a/app/Controller/MediaController.php
+++ b/app/Controller/MediaController.php
@@ -19,6 +19,7 @@ namespace Fisharebest\Webtrees\Controller;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Fact;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Media;
use Fisharebest\Webtrees\Menu;
@@ -131,7 +132,7 @@ class MediaController extends GedcomRecordController {
$facts[] = new Fact('1 __FILE_SIZE__ ' . $this->record->getFilesize(), $this->record, 0);
}
- sort_facts($facts);
+ Functions::sortFacts($facts);
return $facts;
}
diff --git a/app/Controller/PageController.php b/app/Controller/PageController.php
index 309a196d00..848774552a 100644
--- a/app/Controller/PageController.php
+++ b/app/Controller/PageController.php
@@ -20,6 +20,7 @@ use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Theme;
@@ -115,7 +116,7 @@ class PageController extends BaseController {
*/
public function restrictAccess($condition) {
if ($condition !== true) {
- header('Location: ' . WT_LOGIN_URL . '?url=' . rawurlencode(get_query_url()));
+ header('Location: ' . WT_LOGIN_URL . '?url=' . rawurlencode(Functions::getQueryUrl()));
exit;
}
diff --git a/app/Controller/PedigreeController.php b/app/Controller/PedigreeController.php
index 1de6c89f66..09b7559683 100644
--- a/app/Controller/PedigreeController.php
+++ b/app/Controller/PedigreeController.php
@@ -257,7 +257,7 @@ class PedigreeController extends ChartController {
*
* @return string
*/
- public function get_menu() {
+ public function getMenu() {
$famids = $this->root->getSpouseFamilies();
$html = '';
if ($famids) {
diff --git a/app/Controller/SearchController.php b/app/Controller/SearchController.php
index e33cbb220f..946c3875cb 100644
--- a/app/Controller/SearchController.php
+++ b/app/Controller/SearchController.php
@@ -21,6 +21,8 @@ use Fisharebest\Webtrees\ConfigData;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\Functions\FunctionsDb;
+use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -238,26 +240,26 @@ class SearchController extends PageController {
// Search the individuals
if ($this->srindi && $query_terms) {
- $this->myindilist = search_indis($query_terms, $this->search_trees);
+ $this->myindilist = FunctionsDb::searchIndividuals($query_terms, $this->search_trees);
}
// Search the fams
if ($this->srfams && $query_terms) {
$this->myfamlist = array_merge(
- search_fams($query_terms, $this->search_trees),
- search_fams_names($query_terms, $this->search_trees)
+ FunctionsDb::searchFamilies($query_terms, $this->search_trees),
+ FunctionsDb::searchFamilyNames($query_terms, $this->search_trees)
);
$this->myfamlist = array_unique($this->myfamlist);
}
// Search the sources
if ($this->srsour && $query_terms) {
- $this->mysourcelist = search_sources($query_terms, $this->search_trees);
+ $this->mysourcelist = FunctionsDb::searchSources($query_terms, $this->search_trees);
}
// Search the notes
if ($this->srnote && $query_terms) {
- $this->mynotelist = search_notes($query_terms, $this->search_trees);
+ $this->mynotelist = FunctionsDb::searchNotes($query_terms, $this->search_trees);
}
// If only 1 item is returned, automatically forward to that item
@@ -462,7 +464,7 @@ class SearchController extends PageController {
Log::addSearchLog($logstring, $this->search_trees);
if ($this->search_trees) {
- $this->myindilist = search_indis_soundex($this->soundex, $this->lastname, $this->firstname, $this->place, $this->search_trees);
+ $this->myindilist = FunctionsDb::searchIndividualsPhonetic($this->soundex, $this->lastname, $this->firstname, $this->place, $this->search_trees);
} else {
$this->myindilist = array();
}
@@ -524,16 +526,16 @@ class SearchController extends PageController {
}
echo '</ul>';
if (!empty($this->myindilist)) {
- echo '<div id="individual-results-tab">', format_indi_table($this->myindilist), '</div>';
+ echo '<div id="individual-results-tab">', FunctionsPrintLists::individualTable($this->myindilist), '</div>';
}
if (!empty($this->myfamlist)) {
- echo '<div id="families-results-tab">', format_fam_table($this->myfamlist), '</div>';
+ echo '<div id="families-results-tab">', FunctionsPrintLists::familyTable($this->myfamlist), '</div>';
}
if (!empty($this->mysourcelist)) {
- echo '<div id="sources-results-tab">', format_sour_table($this->mysourcelist), '</div>';
+ echo '<div id="sources-results-tab">', FunctionsPrintLists::sourceTable($this->mysourcelist), '</div>';
}
if (!empty($this->mynotelist)) {
- echo '<div id="notes-results-tab">', format_note_table($this->mynotelist), '</div>';
+ echo '<div id="notes-results-tab">', FunctionsPrintLists::noteTable($this->mynotelist), '</div>';
}
} else {
// One or more search terms were specified, but no results were found.
diff --git a/app/Controller/TimelineController.php b/app/Controller/TimelineController.php
index 4696ddb011..85a8095065 100644
--- a/app/Controller/TimelineController.php
+++ b/app/Controller/TimelineController.php
@@ -20,6 +20,8 @@ use Fisharebest\Webtrees\Date\GregorianDate;
use Fisharebest\Webtrees\Fact;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDate;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Theme;
@@ -225,16 +227,16 @@ class TimelineController extends PageController {
echo $event->getLabel();
echo ' — ';
if ($record instanceof Individual) {
- echo format_fact_date($event, $record, false, false);
+ echo FunctionsPrint::formatFactDate($event, $record, false, false);
} elseif ($record instanceof Family) {
echo $gdate->display();
if ($record->getHusband() && $record->getHusband()->getBirthDate()->isOK()) {
- $ageh = get_age_at_event(Date::getAgeGedcom($record->getHusband()->getBirthDate(), $gdate), false);
+ $ageh = FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($record->getHusband()->getBirthDate(), $gdate), false);
} else {
$ageh = null;
}
if ($record->getWife() && $record->getWife()->getBirthDate()->isOK()) {
- $agew = get_age_at_event(Date::getAgeGedcom($record->getWife()->getBirthDate(), $gdate), false);
+ $agew = FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($record->getWife()->getBirthDate(), $gdate), false);
} else {
$agew = null;
}
diff --git a/app/Fact.php b/app/Fact.php
index 3bd0989c55..95470d5432 100644
--- a/app/Fact.php
+++ b/app/Fact.php
@@ -15,6 +15,7 @@ namespace Fisharebest\Webtrees;
* 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\Functions\FunctionsPrint;
/**
* Class Fact - Class that defines an event details object
@@ -44,7 +45,7 @@ class Fact {
/** @var Place The place of this fact, from the “2 PLAC …” attribute */
private $place;
- /** @var int Temporary(!) variable Used by sort_facts() */
+ /** @var int Temporary(!) variable Used by Functions::sortFacts() */
public $sortOrder;
/**
@@ -385,7 +386,7 @@ class Fact {
}
$date = $this->getDate();
if ($this->getTag() == 'BIRT' && $this->getParent() instanceof Individual && $this->getParent()->getTree()->getPreference('SHOW_PARENTS_AGE')) {
- $attributes[] = $date->display() . format_parents_age($this->getParent(), $date);
+ $attributes[] = $date->display() . FunctionsPrint::formatParentsAges($this->getParent(), $date);
} else {
$attributes[] = $date->display();
}
diff --git a/app/Functions/FunctionsCharts.php b/app/Functions/FunctionsCharts.php
new file mode 100644
index 0000000000..174af25c9a
--- /dev/null
+++ b/app/Functions/FunctionsCharts.php
@@ -0,0 +1,556 @@
+<?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\Family;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Theme;
+
+/**
+ * Class FunctionsCharts - common functions
+ */
+class FunctionsCharts {
+ /**
+ * print a table cell with sosa number
+ *
+ * @param int $sosa
+ * @param string $pid optional pid
+ * @param string $arrowDirection direction of link arrow
+ */
+ public static function printSosaNumber($sosa, $pid = "", $arrowDirection = "up") {
+ if (substr($sosa, -1, 1) == ".") {
+ $personLabel = substr($sosa, 0, -1);
+ } else {
+ $personLabel = $sosa;
+ }
+ if ($arrowDirection == "blank") {
+ $visibility = "hidden";
+ } else {
+ $visibility = "normal";
+ }
+ echo "<td class=\"subheaders center\" style=\"vertical-align: middle; text-indent: 0px; margin-top: 0px; white-space: nowrap; visibility: ", $visibility, ";\">";
+ echo $personLabel;
+ if ($sosa != "1" && $pid != "") {
+ if ($arrowDirection == "left") {
+ $dir = 0;
+ } elseif ($arrowDirection == "right") {
+ $dir = 1;
+ } elseif ($arrowDirection == "down") {
+ $dir = 3;
+ } else {
+ $dir = 2; // either 'blank' or 'up'
+ }
+ echo '<br>';
+ self::printUrlArrow('#' . $pid, $pid, $dir);
+ }
+ echo '</td>';
+ }
+
+ /**
+ * print the parents table for a family
+ *
+ * @param Family $family family gedcom ID
+ * @param int $sosa child sosa number
+ * @param string $label indi label (descendancy booklet)
+ * @param string $parid parent ID (descendancy booklet)
+ * @param string $gparid gd-parent ID (descendancy booklet)
+ * @param int $show_full large or small box
+ */
+ public static function printFamilyParents(Family $family, $sosa = 0, $label = '', $parid = '', $gparid = '', $show_full = 1) {
+
+ if ($show_full) {
+ $pbheight = Theme::theme()->parameter('chart-box-y') + 14;
+ } else {
+ $pbheight = Theme::theme()->parameter('compact-chart-box-y') + 14;
+ }
+
+ $husb = $family->getHusband();
+ if ($husb) {
+ echo '<a name="', $husb->getXref(), '"></a>';
+ } else {
+ $husb = new Individual('M', "0 @M@ INDI\n1 SEX M", null, $family->getTree());
+ }
+ $wife = $family->getWife();
+ if ($wife) {
+ echo '<a name="', $wife->getXref(), '"></a>';
+ } else {
+ $wife = new Individual('F', "0 @F@ INDI\n1 SEX F", null, $family->getTree());
+ }
+
+ if ($sosa) {
+ echo '<p class="name_head">', $family->getFullName(), '</p>';
+ }
+
+ /**
+ * husband side
+ */
+ echo "<table cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td rowspan=\"2\">";
+ echo "<table border='0'><tr>";
+
+ if ($parid) {
+ if ($husb->getXref() == $parid) {
+ self::printSosaNumber($label);
+ } else {
+ self::printSosaNumber($label, "", "blank");
+ }
+ } elseif ($sosa) {
+ self::printSosaNumber($sosa * 2);
+ }
+ if ($husb->isPendingAddtion()) {
+ echo '<td valign="top" class="facts_value new">';
+ } elseif ($husb->isPendingDeletion()) {
+ echo '<td valign="top" class="facts_value old">';
+ } else {
+ echo '<td valign="top">';
+ }
+ FunctionsPrint::printPedigreePerson($husb, $show_full);
+ echo "</td></tr></table>";
+ echo "</td>";
+ // husband’s parents
+ $hfam = $husb->getPrimaryChildFamily();
+ if ($hfam) {
+ // remove the|| test for $sosa
+ echo "<td rowspan=\"2\"><img src=\"" . Theme::theme()->parameter('image-hline') . "\" alt=\"\"></td><td rowspan=\"2\"><img src=\"" . Theme::theme()->parameter('image-vline') . "\" width=\"3\" height=\"" . ($pbheight + 9) . "\" alt=\"\"></td>";
+ echo "<td><img class=\"line5\" src=\"" . Theme::theme()->parameter('image-hline') . "\" alt=\"\"></td><td>";
+ // husband’s father
+ if ($hfam && $hfam->getHusband()) {
+ echo "<table border='0'><tr>";
+ if ($sosa > 0) {
+ self::printSosaNumber($sosa * 4, $hfam->getHusband()->getXref(), "down");
+ }
+ if (!empty($gparid) && $hfam->getHusband()->getXref() == $gparid) {
+ self::printSosaNumber(trim(substr($label, 0, -3), ".") . ".");
+ }
+ echo "<td valign=\"top\">";
+ FunctionsPrint::printPedigreePerson($hfam->getHusband(), $show_full);
+ echo "</td></tr></table>";
+ } elseif ($hfam && !$hfam->getHusband()) {
+ // Empty box for grandfather
+ echo "<table border='0'><tr>";
+ echo '<td valign="top">';
+ FunctionsPrint::printPedigreePerson($hfam->getHusband(), $show_full);
+ echo '</td></tr></table>';
+ }
+ echo "</td>";
+ }
+ if ($hfam && ($sosa != -1)) {
+ echo '<td valign="middle" rowspan="2">';
+ self::printUrlArrow(($sosa == 0 ? '?famid=' . $hfam->getXref() . '&amp;ged=' . $hfam->getTree()->getNameUrl() : '#' . $hfam->getXref()), $hfam->getXref(), 1);
+ echo '</td>';
+ }
+ if ($hfam) {
+ // husband’s mother
+ echo "</tr><tr><td><img src=\"" . Theme::theme()->parameter('image-hline') . "\" alt=\"\"></td><td>";
+ if ($hfam && $hfam->getWife()) {
+ echo "<table border='0'><tr>";
+ if ($sosa > 0) {
+ self::printSosaNumber($sosa * 4 + 1, $hfam->getWife()->getXref(), "down");
+ }
+ if (!empty($gparid) && $hfam->getWife()->getXref() == $gparid) {
+ self::printSosaNumber(trim(substr($label, 0, -3), ".") . ".");
+ }
+ echo '<td valign="top">';
+ FunctionsPrint::printPedigreePerson($hfam->getWife(), $show_full);
+ echo '</td></tr></table>';
+ } elseif ($hfam && !$hfam->getWife()) {
+ // Empty box for grandmother
+ echo "<table border='0'><tr>";
+ echo '<td valign="top">';
+ FunctionsPrint::printPedigreePerson($hfam->getWife(), $show_full);
+ echo '</td></tr></table>';
+ }
+ echo '</td>';
+ }
+ echo '</tr></table>';
+ if ($sosa && $family->canShow()) {
+ foreach ($family->getFacts(WT_EVENTS_MARR) as $fact) {
+ echo '<a href="', $family->getHtmlUrl(), '" class="details1">';
+ echo str_repeat('&nbsp;', 10);
+ echo $fact->summary();
+ echo '</a>';
+ }
+ } else {
+ echo '<br>';
+ }
+
+ /**
+ * wife side
+ */
+ echo "<table cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td rowspan=\"2\">";
+ echo "<table><tr>";
+ if ($parid) {
+ if ($wife->getXref() == $parid) {
+ self::printSosaNumber($label);
+ } else {
+ self::printSosaNumber($label, "", "blank");
+ }
+ } elseif ($sosa) {
+ self::printSosaNumber($sosa * 2 + 1);
+ }
+ if ($wife->isPendingAddtion()) {
+ echo '<td valign="top" class="facts_value new">';
+ } elseif ($wife->isPendingDeletion()) {
+ echo '<td valign="top" class="facts_value old">';
+ } else {
+ echo '<td valign="top">';
+ }
+ FunctionsPrint::printPedigreePerson($wife, $show_full);
+ echo "</td></tr></table>";
+ echo "</td>";
+ // wife’s parents
+ $hfam = $wife->getPrimaryChildFamily();
+
+ if ($hfam) {
+ echo "<td rowspan=\"2\"><img src=\"" . Theme::theme()->parameter('image-hline') . "\" alt=\"\"></td><td rowspan=\"2\"><img src=\"" . Theme::theme()->parameter('image-vline') . "\" width=\"3\" height=\"" . ($pbheight + 9) . "\" alt=\"\"></td>";
+ echo "<td><img class=\"line5\" src=\"" . Theme::theme()->parameter('image-hline') . "\" alt=\"\"></td><td>";
+ // wife’s father
+ if ($hfam && $hfam->getHusband()) {
+ echo "<table><tr>";
+ if ($sosa > 0) {
+ self::printSosaNumber($sosa * 4 + 2, $hfam->getHusband()->getXref(), "down");
+ }
+ if (!empty($gparid) && $hfam->getHusband()->getXref() == $gparid) {
+ self::printSosaNumber(trim(substr($label, 0, -3), ".") . ".");
+ }
+ echo "<td valign=\"top\">";
+ FunctionsPrint::printPedigreePerson($hfam->getHusband(), $show_full);
+ echo "</td></tr></table>";
+ } elseif ($hfam && !$hfam->getHusband()) {
+ // Empty box for grandfather
+ echo "<table border='0'><tr>";
+ echo '<td valign="top">';
+ FunctionsPrint::printPedigreePerson($hfam->getHusband(), $show_full);
+ echo '</td></tr></table>';
+ }
+ echo "</td>";
+ }
+ if ($hfam && ($sosa != -1)) {
+ echo '<td valign="middle" rowspan="2">';
+ self::printUrlArrow(($sosa == 0 ? '?famid=' . $hfam->getXref() . '&amp;ged=' . $hfam->getTree()->getNameUrl() : '#' . $hfam->getXref()), $hfam->getXref(), 1);
+ echo '</td>';
+ }
+ if ($hfam) {
+ // wife’s mother
+ echo "</tr><tr><td><img src=\"" . Theme::theme()->parameter('image-hline') . "\" alt=\"\"></td><td>";
+ if ($hfam && $hfam->getWife()) {
+ echo "<table><tr>";
+ if ($sosa > 0) {
+ self::printSosaNumber($sosa * 4 + 3, $hfam->getWife()->getXref(), "down");
+ }
+ if (!empty($gparid) && $hfam->getWife()->getXref() == $gparid) {
+ self::printSosaNumber(trim(substr($label, 0, -3), ".") . ".");
+ }
+ echo "<td valign=\"top\">";
+ FunctionsPrint::printPedigreePerson($hfam->getWife(), $show_full);
+ echo "</td></tr></table>";
+ } elseif ($hfam && !$hfam->getWife()) {
+ // Empty box for grandmother
+ echo "<table border='0'><tr>";
+ echo '<td valign="top">';
+ FunctionsPrint::printPedigreePerson($hfam->getWife(), $show_full);
+ echo '</td></tr></table>';
+ }
+ echo '</td>';
+ }
+ echo "</tr></table>";
+ }
+
+ /**
+ * print the children table for a family
+ *
+ * @param Family $family family
+ * @param string $childid child ID
+ * @param int $sosa child sosa number
+ * @param string $label indi label (descendancy booklet)
+ * @param int $show_cousins display cousins on chart
+ * @param int $show_full large or small box
+ */
+ public static function printFamilyChildren(Family $family, $childid = '', $sosa = 0, $label = '', $show_cousins = 0, $show_full = 1) {
+
+ if ($show_full) {
+ $bheight = Theme::theme()->parameter('chart-box-y');
+ } else {
+ $bheight = Theme::theme()->parameter('compact-chart-box-y');
+ }
+
+ $pbheight = $bheight + 14;
+
+ $children = $family->getChildren();
+ $numchil = count($children);
+
+ echo '<table border="0" cellpadding="0" cellspacing="2"><tr>';
+ if ($sosa > 0) {
+ echo '<td></td>';
+ }
+ echo '<td><span class="subheaders">';
+ if ($numchil == 0) {
+ echo I18N::translate('No children');
+ } else {
+ echo I18N::plural('%s child', '%s children', $numchil, $numchil);
+ }
+ echo '</span>';
+
+ if ($sosa == 0 && Auth::isEditor($family->getTree())) {
+ echo '<br>';
+ echo "<a href=\"#\" onclick=\"return add_child_to_family('", $family->getXref(), "', 'U');\">" . I18N::translate('Add a child to this family') . "</a>";
+ echo ' <a class="icon-sex_m_15x15" href="#" onclick="return add_child_to_family(\'', $family->getXref(), '\', \'M\');" title="', I18N::translate('son'), '"></a>';
+ echo ' <a class="icon-sex_f_15x15" href="#" onclick="return add_child_to_family(\'', $family->getXref(), '\', \'F\');" title="', I18N::translate('daughter'), '"></a>';
+ echo '<br><br>';
+ }
+ echo '</td>';
+ if ($sosa > 0) {
+ echo '<td></td><td></td>';
+ }
+ echo '</tr>';
+
+ $nchi = 1;
+ if ($children) {
+ foreach ($children as $child) {
+ echo '<tr>';
+ if ($sosa != 0) {
+ if ($child->getXref() == $childid) {
+ self::printSosaNumber($sosa, $childid);
+ } elseif (empty($label)) {
+ self::printSosaNumber("");
+ } else {
+ self::printSosaNumber($label . ($nchi++) . ".");
+ }
+ }
+ if ($child->isPendingAddtion()) {
+ echo '<td valign="middle" class="new">';
+ } elseif ($child->isPendingDeletion()) {
+ echo '<td valign="middle" class="old">';
+ } else {
+ echo '<td valign="middle">';
+ }
+ FunctionsPrint::printPedigreePerson($child, $show_full);
+ echo "</td>";
+ if ($sosa != 0) {
+ // loop for all families where current child is a spouse
+ $famids = $child->getSpouseFamilies();
+
+ $maxfam = count($famids) - 1;
+ for ($f = 0; $f <= $maxfam; $f++) {
+ $famid_child = $famids[$f]->getXref();
+ // multiple marriages
+ if ($f > 0) {
+ echo '</tr><tr><td></td>';
+ echo '<td valign="top"';
+ if (I18N::direction() === 'rtl') {
+ echo ' align="left">';
+ } else {
+ echo ' align="right">';
+ }
+
+ //find out how many cousins there are to establish vertical line on second families
+ $fchildren = $famids[$f]->getChildren();
+ $kids = count($fchildren);
+ $Pheader = ($bheight - 1) * $kids;
+ $PBadj = 6; // default
+ if ($show_cousins > 0) {
+ if ($kids) {
+ $PBadj = max(0, $Pheader / 2 + $kids * 4.5);
+ }
+ }
+
+ if ($f == $maxfam) {
+ echo "<img height=\"" . ((($bheight / 2)) + $PBadj) . "px\"";
+ } else {
+ echo "<img height=\"" . $pbheight . "px\"";
+ }
+ echo " width=\"3\" src=\"" . Theme::theme()->parameter('image-vline') . "\" alt=\"\">";
+ echo "</td>";
+ }
+ echo "<td class=\"details1\" valign=\"middle\" align=\"center\">";
+ $spouse = $famids[$f]->getSpouse($child);
+
+ $marr = $famids[$f]->getFirstFact('MARR');
+ $div = $famids[$f]->getFirstFact('DIV');
+ if ($marr) {
+ // marriage date
+ echo $marr->getDate()->minimumDate()->format('%Y');
+ // divorce date
+ if ($div) {
+ echo '–', $div->getDate()->minimumDate()->format('%Y');
+ }
+ }
+ echo "<br><img width=\"100%\" class=\"line5\" height=\"3\" src=\"" . Theme::theme()->parameter('image-hline') . "\" alt=\"\">";
+ echo "</td>";
+ // spouse information
+ echo "<td style=\"vertical-align: center;";
+ if (!empty($divrec)) {
+ echo " filter:alpha(opacity=40);opacity:0.4;\">";
+ } else {
+ echo "\">";
+ }
+ FunctionsPrint::printPedigreePerson($spouse, $show_full);
+ echo "</td>";
+ // cousins
+ if ($show_cousins) {
+ print_cousins($famid_child, $show_full);
+ }
+ }
+ }
+ echo "</tr>";
+ }
+ } elseif ($sosa < 1) {
+ // message 'no children' except for sosa
+ if (preg_match('/\n1 NCHI (\d+)/', $family->getGedcom(), $match) && $match[1] == 0) {
+ echo '<tr><td><i class="icon-childless"></i> ' . I18N::translate('This family remained childless') . '</td></tr>';
+ }
+ }
+ echo "</table><br>";
+ }
+
+ /**
+ * print a family with Sosa-Stradonitz numbering system
+ * ($rootid=1, father=2, mother=3 ...)
+ *
+ * @param string $famid family gedcom ID
+ * @param string $childid tree root ID
+ * @param int $sosa starting sosa number
+ * @param string $label indi label (descendancy booklet)
+ * @param string $parid parent ID (descendancy booklet)
+ * @param string $gparid gd-parent ID (descendancy booklet)
+ * @param int $show_cousins display cousins on chart
+ * @param int $show_full large or small box
+ */
+ public static function printSosaFamily($famid, $childid, $sosa, $label = '', $parid = '', $gparid = '', $show_cousins = 0, $show_full = 1) {
+ global $WT_TREE;
+
+ echo '<hr>';
+ echo '<p style="page-break-before: always;">';
+ if (!empty($famid)) {
+ echo '<a name="', $famid, '"></a>';
+ }
+ self::printFamilyParents(Family::getInstance($famid, $WT_TREE), $sosa, $label, $parid, $gparid, $show_full);
+ echo '<br>';
+ echo '<table><tr><td valign="top">';
+ self::printFamilyChildren(Family::getInstance($famid, $WT_TREE), $childid, $sosa, $label, $show_cousins, $show_full);
+ echo '</td></tr></table>';
+ echo '<br>';
+ }
+
+ /**
+ * print an arrow to a new url
+ *
+ * @param string $url target url
+ * @param string $label arrow label
+ * @param int $dir arrow direction 0=left 1=right 2=up 3=down (default=2)
+ */
+ public static function printUrlArrow($url, $label, $dir = 2) {
+ if ($url === '') {
+ return;
+ }
+
+ // arrow direction
+ $adir = $dir;
+ if (I18N::direction() === 'rtl' && $dir === 0) {
+ $adir = 1;
+ }
+ if (I18N::direction() === 'rtl' && $dir === 1) {
+ $adir = 0;
+ }
+
+ // arrow style 0 1 2 3
+ $array_style = array('icon-larrow', 'icon-rarrow', 'icon-uarrow', 'icon-darrow');
+ $astyle = $array_style[$adir];
+
+ // Labels include people’s names, which may contain markup
+ echo '<a href="' . $url . '" title="' . strip_tags($label) . '" class="' . $astyle . '"></a>';
+ }
+
+ /**
+ * builds and returns sosa relationship name in the active language
+ *
+ * @param string $sosa sosa number
+ *
+ * @return string
+ */
+ public static function getSosaName($sosa) {
+ $path = '';
+ while ($sosa > 1) {
+ if ($sosa % 2 == 1) {
+ $sosa -= 1;
+ $path = 'mot' . $path;
+ } else {
+ $path = 'fat' . $path;
+ }
+ $sosa /= 2;
+ }
+
+ return Functions::getRelationshipNameFromPath($path, null, null);
+ }
+
+ /**
+ * print cousins list
+ *
+ * @param string $famid family ID
+ * @param int $show_full large or small box
+ */
+ public static function printCousins($famid, $show_full = 1) {
+ global $WT_TREE;
+
+ if ($show_full) {
+ $bheight = Theme::theme()->parameter('chart-box-y');
+ } else {
+ $bheight = Theme::theme()->parameter('compact-chart-box-y');
+ }
+
+ $family = Family::getInstance($famid, $WT_TREE);
+ $fchildren = $family->getChildren();
+
+ $kids = count($fchildren);
+
+ echo '<td valign="middle" height="100%">';
+ if ($kids) {
+ echo '<table cellspacing="0" cellpadding="0" border="0" ><tr valign="middle">';
+ if ($kids > 1) {
+ echo '<td rowspan="', $kids, '" valign="middle" align="right"><img width="3px" height="', (($bheight + 9) * ($kids - 1)), 'px" src="', Theme::theme()->parameter('image-vline'), '" alt=""></td>';
+ }
+ $ctkids = count($fchildren);
+ $i = 1;
+ foreach ($fchildren as $fchil) {
+ if ($i == 1) {
+ echo '<td><img width="10px" height="3px" align="top"';
+ } else {
+ echo '<td><img width="10px" height="3px"';
+ }
+ if (I18N::direction() === 'ltr') {
+ echo ' style="padding-right: 2px;"';
+ } else {
+ echo ' style="padding-left: 2px;"';
+ }
+ echo ' src="', Theme::theme()->parameter('image-hline'), '" alt=""></td><td>';
+ FunctionsPrint::printPedigreePerson($fchil, $show_full);
+ echo '</td></tr>';
+ if ($i < $ctkids) {
+ echo '<tr>';
+ $i++;
+ }
+ }
+ echo '</table>';
+ } else {
+ // If there is known that there are no children (as opposed to no known children)
+ if (preg_match('/\n1 NCHI (\d+)/', $family->getGedcom(), $match) && $match[1] == 0) {
+ echo ' <i class="icon-childless" title="', I18N::translate('This family remained childless'), '"></i>';
+ }
+ }
+ echo '</td>';
+ }
+}
diff --git a/app/Functions/FunctionsDate.php b/app/Functions/FunctionsDate.php
new file mode 100644
index 0000000000..1a6cf66ded
--- /dev/null
+++ b/app/Functions/FunctionsDate.php
@@ -0,0 +1,130 @@
+<?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\Date;
+use Fisharebest\Webtrees\I18N;
+
+/**
+ * Class FunctionsDate - common functions
+ */
+class FunctionsDate {
+ /**
+ * @param string $age_string
+ * @param bool $show_years
+ *
+ * @return string
+ */
+ public static function getAgeAtEvent($age_string, $show_years) {
+ switch (strtoupper($age_string)) {
+ case 'CHILD':
+ return I18N::translate('Child');
+ case 'INFANT':
+ return I18N::translate('Infant');
+ case 'STILLBORN':
+ return I18N::translate('Stillborn');
+ default:
+ return preg_replace_callback(
+ array(
+ '/(\d+)([ymwd])/',
+ ),
+ function ($match) use ($age_string, $show_years) {
+ switch ($match[2]) {
+ case 'y':
+ if ($show_years || preg_match('/[dm]/', $age_string)) {
+ return I18N::plural('%s year', '%s years', $match[1], I18N::digits($match[1]));
+ } else {
+ return I18N::digits($match[1]);
+ }
+ case 'm':
+ return I18N::plural('%s month', '%s months', $match[1], I18N::digits($match[1]));
+ case 'w':
+ return I18N::plural('%s week', '%s weeks', $match[1], I18N::digits($match[1]));
+ case 'd':
+ return I18N::plural('%s day', '%s days', $match[1], I18N::digits($match[1]));
+ }
+ },
+ $age_string
+ );
+ }
+ }
+
+ /**
+ * Convert a unix timestamp into a formatted date-time value, for logs, etc.
+ * Don't attempt to convert into other calendars, as not all days start at
+ * midnight, and we can only get it wrong.
+ *
+ * @param int $time
+ *
+ * @return string
+ */
+ public static function formatTimestamp($time) {
+ $time_fmt = I18N::timeFormat();
+ // PHP::date() doesn't do I18N. Do it ourselves....
+ preg_match_all('/%[^%]/', $time_fmt, $matches);
+ foreach ($matches[0] as $match) {
+ switch ($match) {
+ case '%a':
+ $t = gmdate('His', $time);
+ if ($t == '000000') {
+ $time_fmt = str_replace($match, /* I18N: time format “%a” - exactly 00:00:00 */
+ I18N::translate('midnight'), $time_fmt);
+ } elseif ($t < '120000') {
+ $time_fmt = str_replace($match, /* I18N: time format “%a” - between 00:00:01 and 11:59:59 */
+ I18N::translate('a.m.'), $time_fmt);
+ } elseif ($t == '120000') {
+ $time_fmt = str_replace($match, /* I18N: time format “%a” - exactly 12:00:00 */
+ I18N::translate('noon'), $time_fmt);
+ } else {
+ $time_fmt = str_replace($match, /* I18N: time format “%a” - between 12:00:01 and 23:59:59 */
+ I18N::translate('p.m.'), $time_fmt);
+ }
+ break;
+ case '%A':
+ $t = gmdate('His', $time);
+ if ($t == '000000') {
+ $time_fmt = str_replace($match, /* I18N: time format “%A” - exactly 00:00:00 */
+ I18N::translate('Midnight'), $time_fmt);
+ } elseif ($t < '120000') {
+ $time_fmt = str_replace($match, /* I18N: time format “%A” - between 00:00:01 and 11:59:59 */
+ I18N::translate('A.M.'), $time_fmt);
+ } elseif ($t == '120000') {
+ $time_fmt = str_replace($match, /* I18N: time format “%A” - exactly 12:00:00 */
+ I18N::translate('Noon'), $time_fmt);
+ } else {
+ $time_fmt = str_replace($match, /* I18N: time format “%A” - between 12:00:01 and 23:59:59 */
+ I18N::translate('P.M.'), $time_fmt);
+ }
+ break;
+ default:
+ $time_fmt = str_replace($match, I18N::digits(gmdate(substr($match, -1), $time)), $time_fmt);
+ }
+ }
+
+ return self::timestampToGedcomDate($time)->display() . '<span class="date"> - ' . $time_fmt . '</span>';
+ }
+
+ /**
+ * Convert a unix-style timestamp into a Date object
+ *
+ * @param int $time
+ *
+ * @return Date
+ */
+ public static function timestampToGedcomDate($time) {
+ return new Date(strtoupper(gmdate('j M Y', $time)));
+ }
+}
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();
+ }
+}
diff --git a/app/Functions/FunctionsEdit.php b/app/Functions/FunctionsEdit.php
new file mode 100644
index 0000000000..b395f25037
--- /dev/null
+++ b/app/Functions/FunctionsEdit.php
@@ -0,0 +1,1715 @@
+<?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\ConfigData;
+use Fisharebest\Webtrees\Database;
+use Fisharebest\Webtrees\Date;
+use Fisharebest\Webtrees\Fact;
+use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeAdop;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeName;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodePedi;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeQuay;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeRela;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeStat;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeTemp;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\GedcomTag;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Media;
+use Fisharebest\Webtrees\Module;
+use Fisharebest\Webtrees\Module\CensusAssistantModule;
+use Fisharebest\Webtrees\Note;
+use Fisharebest\Webtrees\Repository;
+use Fisharebest\Webtrees\Source;
+use Rhumsaa\Uuid\Uuid;
+
+/**
+ * Class FunctionsEdit - common functions
+ */
+class FunctionsEdit {
+ /**
+ * Create a <select> control for a form.
+ *
+ * @param string $name
+ * @param string[] $values
+ * @param string|null $empty
+ * @param string $selected
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function selectEditControl($name, $values, $empty, $selected, $extra = '') {
+ if (is_null($empty)) {
+ $html = '';
+ } else {
+ if (empty($selected)) {
+ $html = '<option value="" selected>' . Filter::escapeHtml($empty) . '</option>';
+ } else {
+ $html = '<option value="">' . Filter::escapeHtml($empty) . '</option>';
+ }
+ }
+ // A completely empty list would be invalid, and break various things
+ if (empty($values) && empty($html)) {
+ $html = '<option value=""></option>';
+ }
+ foreach ($values as $key => $value) {
+ // PHP array keys are cast to integers! Cast them back
+ if ((string) $key === (string) $selected) {
+ $html .= '<option value="' . Filter::escapeHtml($key) . '" selected dir="auto">' . Filter::escapeHtml($value) . '</option>';
+ } else {
+ $html .= '<option value="' . Filter::escapeHtml($key) . '" dir="auto">' . Filter::escapeHtml($value) . '</option>';
+ }
+ }
+ if (substr($name, -2) === '[]') {
+ // id attribute is not used for arrays
+ return '<select name="' . $name . '" ' . $extra . '>' . $html . '</select>';
+ } else {
+ return '<select id="' . $name . '" name="' . $name . '" ' . $extra . '>' . $html . '</select>';
+ }
+ }
+
+ /**
+ * Create a set of radio buttons for a form
+ *
+ * @param string $name The ID for the form element
+ * @param string[] $values Array of value=>display items
+ * @param string $selected The currently selected item
+ * @param string $extra Additional markup for the label
+ *
+ * @return string
+ */
+ public static function radioButtons($name, $values, $selected, $extra = '') {
+ $html = '';
+ foreach ($values as $key => $value) {
+ $html .=
+ '<label ' . $extra . '>' .
+ '<input type="radio" name="' . $name . '" value="' . Filter::escapeHtml($key) . '"';
+ // PHP array keys are cast to integers! Cast them back
+ if ((string) $key === (string) $selected) {
+ $html .= ' checked';
+ }
+ $html .= '>' . Filter::escapeHtml($value) . '</label>';
+ }
+
+ return $html;
+ }
+
+ /**
+ * Print an edit control for a Yes/No field
+ *
+ * @param string $name
+ * @param bool $selected
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function editFieldYesNo($name, $selected = false, $extra = '') {
+ return self::radioButtons(
+ $name, array(I18N::translate('no'), I18N::translate('yes')), $selected, $extra
+ );
+ }
+
+ /**
+ * Print an edit control for a checkbox.
+ *
+ * @param string $name
+ * @param bool $is_checked
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function checkbox($name, $is_checked = false, $extra = '') {
+ return '<input type="checkbox" name="' . $name . '" value="1" ' . ($is_checked ? 'checked ' : '') . $extra . '>';
+ }
+
+ /**
+ * Print an edit control for a checkbox, with a hidden field to store one of the two states.
+ * By default, a checkbox is either set, or not sent.
+ * This function gives us a three options, set, unset or not sent.
+ * Useful for dynamically generated forms where we don't know what elements are present.
+ *
+ * @param string $name
+ * @param int $is_checked 0 or 1
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function twoStateCheckbox($name, $is_checked = 0, $extra = '') {
+ return
+ '<input type="hidden" id="' . $name . '" name="' . $name . '" value="' . ($is_checked ? 1 : 0) . '">' .
+ '<input type="checkbox" name="' . $name . '-GUI-ONLY" value="1"' .
+ ($is_checked ? ' checked' : '') .
+ ' onclick="document.getElementById(\'' . $name . '\').value=(this.checked?1:0);" ' . $extra . '>';
+ }
+
+ /**
+ * Function edit_language_checkboxes
+ *
+ * @param string $parameter_name
+ * @param array $accepted_languages
+ *
+ * @return string
+ */
+ public static function editLanguageCheckboxes($parameter_name, $accepted_languages) {
+ $html = '';
+ foreach (I18N::activeLocales() as $locale) {
+ $html .= '<div class="checkbox">';
+ $html .= '<label title="' . $locale->languageTag() . '">';
+ $html .= '<input type="checkbox" name="' . $parameter_name . '[]" value="' . $locale->languageTag() . '"';
+ $html .= in_array($locale->languageTag(), $accepted_languages) ? ' checked>' : '>';
+ $html .= $locale->endonym();
+ $html .= '</label>';
+ $html .= '</div>';
+ }
+
+ return $html;
+ }
+
+ /**
+ * Print an edit control for access level.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function editFieldAccessLevel($name, $selected = '', $extra = '') {
+ $ACCESS_LEVEL = array(
+ Auth::PRIV_PRIVATE => I18N::translate('Show to visitors'),
+ Auth::PRIV_USER => I18N::translate('Show to members'),
+ Auth::PRIV_NONE => I18N::translate('Show to managers'),
+ Auth::PRIV_HIDE => I18N::translate('Hide from everyone'),
+ );
+
+ return self::selectEditControl($name, $ACCESS_LEVEL, null, $selected, $extra);
+ }
+
+ /**
+ * Print an edit control for a RESN field.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function editFieldRestriction($name, $selected = '', $extra = '') {
+ $RESN = array(
+ '' => '',
+ 'none' => I18N::translate('Show to visitors'), // Not valid GEDCOM, but very useful
+ 'privacy' => I18N::translate('Show to members'),
+ 'confidential' => I18N::translate('Show to managers'),
+ 'locked' => I18N::translate('Only managers can edit'),
+ );
+
+ return self::selectEditControl($name, $RESN, null, $selected, $extra);
+ }
+
+ /**
+ * Print an edit control for a contact method field.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function editFieldContact($name, $selected = '', $extra = '') {
+ // Different ways to contact the users
+ $CONTACT_METHODS = array(
+ 'messaging' => I18N::translate('Internal messaging'),
+ 'messaging2' => I18N::translate('Internal messaging with emails'),
+ 'messaging3' => I18N::translate('webtrees sends emails with no storage'),
+ 'mailto' => I18N::translate('Mailto link'),
+ 'none' => I18N::translate('No contact'),
+ );
+
+ return self::selectEditControl($name, $CONTACT_METHODS, null, $selected, $extra);
+ }
+
+ /**
+ * Print an edit control for a language field.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function editFieldLanguage($name, $selected = '', $extra = '') {
+ $languages = array();
+ foreach (I18N::activeLocales() as $locale) {
+ $languages[$locale->languageTag()] = $locale->endonym();
+ }
+
+ return self::selectEditControl($name, $languages, null, $selected, $extra);
+ }
+
+ /**
+ * Print an edit control for a range of integers.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param int $min
+ * @param int $max
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function editFieldInteger($name, $selected = '', $min, $max, $extra = '') {
+ $array = array();
+ for ($i = $min; $i <= $max; ++$i) {
+ $array[$i] = I18N::number($i);
+ }
+
+ return self::selectEditControl($name, $array, null, $selected, $extra);
+ }
+
+ /**
+ * Print an edit control for a username.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function editFieldUsername($name, $selected = '', $extra = '') {
+ $all_users = Database::prepare(
+ "SELECT user_name, CONCAT_WS(' ', real_name, '-', user_name) FROM `##user` ORDER BY real_name"
+ )->fetchAssoc();
+ // The currently selected user may not exist
+ if ($selected && !array_key_exists($selected, $all_users)) {
+ $all_users[$selected] = $selected;
+ }
+
+ return self::selectEditControl($name, $all_users, '-', $selected, $extra);
+ }
+
+ /**
+ * Print an edit control for a ADOP field.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param string $extra
+ * @param Individual $individual
+ *
+ * @return string
+ */
+ public static function editFieldAdoption($name, $selected = '', $extra = '', Individual $individual = null) {
+ return self::selectEditControl($name, GedcomCodeAdop::getValues($individual), null, $selected, $extra);
+ }
+
+ /**
+ * Print an edit control for a PEDI field.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param string $extra
+ * @param Individual $individual
+ *
+ * @return string
+ */
+ public static function editFieldPedigree($name, $selected = '', $extra = '', Individual $individual = null) {
+ return self::selectEditControl($name, GedcomCodePedi::getValues($individual), '', $selected, $extra);
+ }
+
+ /**
+ * Print an edit control for a NAME TYPE field.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param string $extra
+ * @param Individual $individual
+ *
+ * @return string
+ */
+ public static function editFieldNameType($name, $selected = '', $extra = '', Individual $individual = null) {
+ return self::selectEditControl($name, GedcomCodeName::getValues($individual), '', $selected, $extra);
+ }
+
+ /**
+ * Print an edit control for a RELA field.
+ *
+ * @param string $name
+ * @param string $selected
+ * @param string $extra
+ *
+ * @return string
+ */
+ public static function editFieldRelationship($name, $selected = '', $extra = '') {
+ $rela_codes = GedcomCodeRela::getValues();
+ // The user is allowed to specify values that aren't in the list.
+ if (!array_key_exists($selected, $rela_codes)) {
+ $rela_codes[$selected] = $selected;
+ }
+
+ return self::selectEditControl($name, $rela_codes, '', $selected, $extra);
+ }
+
+ /**
+ * Remove all links from $gedrec to $xref, and any sub-tags.
+ *
+ * @param string $gedrec
+ * @param string $xref
+ *
+ * @return string
+ */
+ public static function removeLinks($gedrec, $xref) {
+ $gedrec = preg_replace('/\n1 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[2-9].*)*/', '', $gedrec);
+ $gedrec = preg_replace('/\n2 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[3-9].*)*/', '', $gedrec);
+ $gedrec = preg_replace('/\n3 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[4-9].*)*/', '', $gedrec);
+ $gedrec = preg_replace('/\n4 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[5-9].*)*/', '', $gedrec);
+ $gedrec = preg_replace('/\n5 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[6-9].*)*/', '', $gedrec);
+
+ return $gedrec;
+ }
+
+ /**
+ * Generates javascript code for calendar popup in user’s language.
+ *
+ * @param string $id
+ *
+ * @return string
+ */
+ public static function printCalendarPopup($id) {
+ return
+ ' <a href="#" onclick="cal_toggleDate(\'caldiv' . $id . '\', \'' . $id . '\'); return false;" class="icon-button_calendar" title="' . I18N::translate('Select a date') . '"></a>' .
+ '<div id="caldiv' . $id . '" style="position:absolute;visibility:hidden;background-color:white;z-index:1000;"></div>';
+ }
+
+ /**
+ * @param string $element_id
+ *
+ * @return string
+ */
+ public static function printAddNewMediaLink($element_id) {
+ return '<a href="#" onclick="pastefield=document.getElementById(\'' . $element_id . '\'); window.open(\'addmedia.php?action=showmediaform\', \'_blank\', edit_window_specs); return false;" class="icon-button_addmedia" title="' . I18N::translate('Create a new media object') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ *
+ * @return string
+ */
+ public static function printAddNewRepositoryLink($element_id) {
+ return '<a href="#" onclick="addnewrepository(document.getElementById(\'' . $element_id . '\')); return false;" class="icon-button_addrepository" title="' . I18N::translate('Create a new repository') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ *
+ * @return string
+ */
+ public static function printAddNewNoteLink($element_id) {
+ return '<a href="#" onclick="addnewnote(document.getElementById(\'' . $element_id . '\')); return false;" class="icon-button_addnote" title="' . I18N::translate('Create a new shared note') . '"></a>';
+ }
+
+ /**
+ * @param string $note_id
+ *
+ * @return string
+ */
+ public static function printEditNoteLink($note_id) {
+ return '<a href="#" onclick="edit_note(\'' . $note_id . '\'); return false;" class="icon-button_note" title="' . I18N::translate('Edit shared note') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ *
+ * @return string
+ */
+ public static function printAddNewSourceLink($element_id) {
+ return '<a href="#" onclick="addnewsource(document.getElementById(\'' . $element_id . '\')); return false;" class="icon-button_addsource" title="' . I18N::translate('Create a new source') . '"></a>';
+ }
+
+ /**
+ * add a new tag input field
+ *
+ * called for each fact to be edited on a form.
+ * Fact level=0 means a new empty form : data are POSTed by name
+ * else data are POSTed using arrays :
+ * glevels[] : tag level
+ * islink[] : tag is a link
+ * tag[] : tag name
+ * text[] : tag value
+ *
+ * @param string $tag fact record to edit (eg 2 DATE xxxxx)
+ * @param string $upperlevel optional upper level tag (eg BIRT)
+ * @param string $label An optional label to echo instead of the default
+ * @param string $extra optional text to display after the input field
+ * @param Individual $person For male/female translations
+ *
+ * @return string
+ */
+ public static function addSimpleTag($tag, $upperlevel = '', $label = '', $extra = null, Individual $person = null) {
+ global $tags, $main_fact, $xref, $bdm, $action, $WT_TREE;
+
+ // Keep track of SOUR fields, so we can reference them in subsequent PAGE fields.
+ static $source_element_id;
+
+ $subnamefacts = array('NPFX', 'GIVN', 'SPFX', 'SURN', 'NSFX', '_MARNM_SURN');
+ preg_match('/^(?:(\d+) (' . WT_REGEX_TAG . ') ?(.*))/', $tag, $match);
+ list(, $level, $fact, $value) = $match;
+ $level = (int) $level;
+
+ // element name : used to POST data
+ if ($level === 0) {
+ if ($upperlevel) {
+ $element_name = $upperlevel . '_' . $fact;
+ } else {
+ $element_name = $fact;
+ }
+ } else {
+ $element_name = 'text[]';
+ }
+ if ($level === 1) {
+ $main_fact = $fact;
+ }
+
+ // element id : used by javascript functions
+ if ($level === 0) {
+ $element_id = $fact;
+ } else {
+ $element_id = $fact . Uuid::uuid4();
+ }
+ if ($upperlevel) {
+ $element_id = $upperlevel . '_' . $fact . Uuid::uuid4();
+ }
+
+ // field value
+ $islink = (substr($value, 0, 1) === '@' && substr($value, 0, 2) !== '@#');
+ if ($islink) {
+ $value = trim(substr($tag, strlen($fact) + 3), ' @\r');
+ } else {
+ $value = (string) substr($tag, strlen($fact) + 3);
+ }
+ if ($fact === 'REPO' || $fact === 'SOUR' || $fact === 'OBJE' || $fact === 'FAMC') {
+ $islink = true;
+ }
+
+ if ($fact === 'SHARED_NOTE_EDIT' || $fact === 'SHARED_NOTE') {
+ $islink = true;
+ $fact = 'NOTE';
+ }
+
+ // label
+ echo '<tr id="', $element_id, '_tr"';
+ if ($fact === 'MAP' || ($fact === 'LATI' || $fact === 'LONG') && $value === '') {
+ echo ' style="display:none;"';
+ }
+ echo '>';
+
+ if (in_array($fact, $subnamefacts) || $fact === 'LATI' || $fact === 'LONG') {
+ echo '<td class="optionbox wrap width25">';
+ } else {
+ echo '<td class="descriptionbox wrap width25">';
+ }
+
+ // tag name
+ if ($label) {
+ echo $label;
+ } elseif ($upperlevel) {
+ echo GedcomTag::getLabel($upperlevel . ':' . $fact);
+ } else {
+ echo GedcomTag::getLabel($fact);
+ }
+
+ // If using GEDFact-assistant window
+ if ($action === 'addnewnote_assisted') {
+ // Do not print on GEDFact Assistant window
+ } else {
+ // Not all facts have help text.
+ switch ($fact) {
+ case 'NAME':
+ if ($upperlevel !== 'REPO') {
+ echo FunctionsPrint::helpLink($fact);
+ }
+ break;
+ case 'DATE':
+ case 'PLAC':
+ case 'RESN':
+ case 'ROMN':
+ case 'SURN':
+ case '_HEB':
+ echo FunctionsPrint::helpLink($fact);
+ break;
+ }
+ }
+ // tag level
+ if ($level > 0) {
+ if ($fact === 'TEXT' && $level > 1) {
+ echo '<input type="hidden" name="glevels[]" value="', $level - 1, '">';
+ echo '<input type="hidden" name="islink[]" value="0">';
+ echo '<input type="hidden" name="tag[]" value="DATA">';
+ // leave data text[] value empty because the following TEXT line will cause the DATA to be added
+ echo '<input type="hidden" name="text[]" value="">';
+ }
+ echo '<input type="hidden" name="glevels[]" value="', $level, '">';
+ echo '<input type="hidden" name="islink[]" value="', $islink, '">';
+ echo '<input type="hidden" name="tag[]" value="', $fact, '">';
+ }
+ echo '</td>';
+
+ // value
+ echo '<td class="optionbox wrap">';
+
+ // retrieve linked NOTE
+ if ($fact === 'NOTE' && $islink) {
+ $note1 = Note::getInstance($value, $WT_TREE);
+ if ($note1) {
+ $noterec = $note1->getGedcom();
+ preg_match('/' . $value . '/i', $noterec, $notematch);
+ $value = $notematch[0];
+ }
+ }
+
+ if (in_array($fact, ConfigData::emptyFacts()) && ($value === '' || $value === 'Y' || $value === 'y')) {
+ echo '<input type="hidden" id="', $element_id, '" name="', $element_name, '" value="', $value, '">';
+ if ($level <= 1) {
+ echo '<input type="checkbox" ';
+ if ($value) {
+ echo 'checked';
+ }
+ echo ' onclick="if (this.checked) ', $element_id, '.value=\'Y\'; else ', $element_id, '.value=\'\';">';
+ echo I18N::translate('yes');
+ }
+
+ } elseif ($fact === 'TEMP') {
+ echo self::selectEditControl($element_name, GedcomCodeTemp::templeNames(), I18N::translate('No temple - living ordinance'), $value);
+ } elseif ($fact === 'ADOP') {
+ echo self::editFieldAdoption($element_name, $value, '', $person);
+ } elseif ($fact === 'PEDI') {
+ echo self::editFieldPedigree($element_name, $value, '', $person);
+ } elseif ($fact === 'STAT') {
+ echo self::selectEditControl($element_name, GedcomCodeStat::statusNames($upperlevel), '', $value);
+ } elseif ($fact === 'RELA') {
+ echo self::editFieldRelationship($element_name, strtolower($value));
+ } elseif ($fact === 'QUAY') {
+ echo self::selectEditControl($element_name, GedcomCodeQuay::getValues(), '', $value);
+ } elseif ($fact === '_WT_USER') {
+ echo self::editFieldUsername($element_name, $value);
+ } elseif ($fact === 'RESN') {
+ echo self::editFieldRestriction($element_name, $value);
+ } elseif ($fact === '_PRIM') {
+ echo '<select id="', $element_id, '" name="', $element_name, '" >';
+ echo '<option value=""></option>';
+ echo '<option value="Y" ';
+ if ($value === 'Y') {
+ echo ' selected';
+ }
+ echo '>', /* I18N: option in list box “always use this image” */
+ I18N::translate('always'), '</option>';
+ echo '<option value="N" ';
+ if ($value === 'N') {
+ echo 'selected';
+ }
+ echo '>', /* I18N: option in list box “never use this image” */
+ I18N::translate('never'), '</option>';
+ echo '</select>';
+ echo '<p class="small text-muted">', I18N::translate('Use this image for charts and on the individual’s page.'), '</p>';
+ } elseif ($fact === 'SEX') {
+ echo '<select id="', $element_id, '" name="', $element_name, '"><option value="M" ';
+ if ($value === 'M') {
+ echo 'selected';
+ }
+ echo '>', I18N::translate('Male'), '</option><option value="F" ';
+ if ($value === 'F') {
+ echo 'selected';
+ }
+ echo '>', I18N::translate('Female'), '</option><option value="U" ';
+ if ($value === 'U' || empty($value)) {
+ echo 'selected';
+ }
+ echo '>', I18N::translateContext('unknown gender', 'Unknown'), '</option></select>';
+ } elseif ($fact === 'TYPE' && $level === 3) {
+ //-- Build the selector for the Media 'TYPE' Fact
+ echo '<select name="text[]"><option selected value="" ></option>';
+ $selectedValue = strtolower($value);
+ if (!array_key_exists($selectedValue, GedcomTag::getFileFormTypes())) {
+ echo '<option selected value="', Filter::escapeHtml($value), '" >', Filter::escapeHtml($value), '</option>';
+ }
+ foreach (GedcomTag::getFileFormTypes() as $typeName => $typeValue) {
+ echo '<option value="', $typeName, '" ';
+ if ($selectedValue === $typeName) {
+ echo 'selected';
+ }
+ echo '>', $typeValue, '</option>';
+ }
+ echo '</select>';
+ } elseif (($fact === 'NAME' && $upperlevel !== 'REPO') || $fact === '_MARNM') {
+ // Populated in javascript from sub-tags
+ echo '<input type="hidden" id="', $element_id, '" name="', $element_name, '" onchange="updateTextName(\'', $element_id, '\');" value="', Filter::escapeHtml($value), '" class="', $fact, '">';
+ echo '<span id="', $element_id, '_display" dir="auto">', Filter::escapeHtml($value), '</span>';
+ echo ' <a href="#edit_name" onclick="convertHidden(\'', $element_id, '\'); return false;" class="icon-edit_indi" title="' . I18N::translate('Edit name') . '"></a>';
+ } else {
+ // textarea
+ if ($fact === 'TEXT' || $fact === 'ADDR' || ($fact === 'NOTE' && !$islink)) {
+ echo '<textarea id="', $element_id, '" name="', $element_name, '" dir="auto">', Filter::escapeHtml($value), '</textarea><br>';
+ } else {
+ // text
+ // If using GEDFact-assistant window
+ if ($action === 'addnewnote_assisted') {
+ echo '<input type="text" id="', $element_id, '" name="', $element_name, '" value="', Filter::escapeHtml($value), '" style="width:4.1em;" dir="ltr"';
+ } else {
+ echo '<input type="text" id="', $element_id, '" name="', $element_name, '" value="', Filter::escapeHtml($value), '" dir="ltr"';
+ }
+ echo ' class="', $fact, '"';
+ if (in_array($fact, $subnamefacts)) {
+ echo ' onblur="updatewholename();" onkeyup="updatewholename();"';
+ }
+
+ // Extra markup for specific fact types
+ switch ($fact) {
+ case 'ALIA':
+ case 'ASSO':
+ echo ' data-autocomplete-type="INDI"';
+ break;
+ case '_ASSO':
+ echo ' data-autocomplete-type="ASSO"';
+ break;
+ case 'DATE':
+ echo ' onblur="valid_date(this);" onmouseout="valid_date(this);"';
+ break;
+ case 'GIVN':
+ echo ' autofocus data-autocomplete-type="GIVN"';
+ break;
+ case 'LATI':
+ echo ' onblur="valid_lati_long(this, \'N\', \'S\');" onmouseout="valid_lati_long(this, \'N\', \'S\');"';
+ break;
+ case 'LONG':
+ echo ' onblur="valid_lati_long(this, \'E\', \'W\');" onmouseout="valid_lati_long(this, \'E\', \'W\');"';
+ break;
+ case 'NOTE':
+ // Shared notes. Inline notes are handled elsewhere.
+ echo ' data-autocomplete-type="NOTE"';
+ break;
+ case 'OBJE':
+ echo ' data-autocomplete-type="OBJE"';
+ break;
+ case 'PAGE':
+ echo ' data-autocomplete-type="PAGE" data-autocomplete-extra="' . $source_element_id . '"';
+ break;
+ case 'PLAC':
+ echo ' data-autocomplete-type="PLAC"';
+ break;
+ case 'REPO':
+ echo ' data-autocomplete-type="REPO"';
+ break;
+ case 'SOUR':
+ $source_element_id = $element_id;
+ echo ' data-autocomplete-type="SOUR"';
+ break;
+ case 'SURN':
+ case '_MARNM_SURN':
+ echo ' data-autocomplete-type="SURN"';
+ break;
+ case 'TIME':
+ echo ' pattern="([0-1][0-9]|2[0-3]):[0-5][0-9](:[0-5]0-9])?" dir="ltr" placeholder="' . /* I18N: Examples of valid time formats (hours:minutes:seconds) */
+ I18N::translate('hh:mm or hh:mm:ss') . '"';
+ break;
+ }
+ echo '>';
+ }
+
+ $tmp_array = array('TYPE', 'TIME', 'NOTE', 'SOUR', 'REPO', 'OBJE', 'ASSO', '_ASSO', 'AGE');
+
+ // split PLAC
+ if ($fact === 'PLAC') {
+ echo '<div id="', $element_id, '_pop" style="display: inline;">';
+ echo FunctionsPrint::printSpecialCharacterLink($element_id), ' ', self::print_findplace_link($element_id);
+ echo '<span onclick="jQuery(\'tr[id^=', $upperlevel, '_LATI],tr[id^=', $upperlevel, '_LONG],tr[id^=LATI],tr[id^=LONG]\').toggle(\'fast\'); return false;" class="icon-target" title="', GedcomTag::getLabel('LATI'), ' / ', GedcomTag::getLabel('LONG'), '"></span>';
+ echo '</div>';
+ if (Module::getModuleByName('places_assistant')) {
+ \PlacesAssistantModule::setup_place_subfields($element_id);
+ \PlacesAssistantModule::print_place_subfields($element_id);
+ }
+ } elseif (!in_array($fact, $tmp_array)) {
+ echo FunctionsPrint::printSpecialCharacterLink($element_id);
+ }
+ }
+ // MARRiage TYPE : hide text field and show a selection list
+ if ($fact === 'TYPE' && $level === 2 && $tags[0] === 'MARR') {
+ echo '<script>';
+ echo 'document.getElementById(\'', $element_id, '\').style.display=\'none\'';
+ echo '</script>';
+ echo '<select id="', $element_id, '_sel" onchange="document.getElementById(\'', $element_id, '\').value=this.value;" >';
+ foreach (array('Unknown', 'Civil', 'Religious', 'Partners') as $key) {
+ if ($key === 'Unknown') {
+ echo '<option value="" ';
+ } else {
+ echo '<option value="', $key, '" ';
+ }
+ $a = strtolower($key);
+ $b = strtolower($value);
+ if ($b !== '' && strpos($a, $b) !== false || strpos($b, $a) !== false) {
+ echo 'selected';
+ }
+ echo '>', GedcomTag::getLabel('MARR_' . strtoupper($key)), '</option>';
+ }
+ echo '</select>';
+ } elseif ($fact === 'TYPE' && $level === 0) {
+ // NAME TYPE : hide text field and show a selection list
+ $onchange = 'onchange="document.getElementById(\'' . $element_id . '\').value=this.value;"';
+ echo self::editFieldNameType($element_name, $value, $onchange, $person);
+ echo '<script>document.getElementById("', $element_id, '").style.display="none";</script>';
+ }
+
+ // popup links
+ switch ($fact) {
+ case 'DATE':
+ echo self::printCalendarPopup($element_id);
+
+ // Allow the GEDFact_assistant module to show a census-date selector
+ if (Module::getModuleByName('GEDFact_assistant')) {
+ echo CensusAssistantModule::censusDateSelector($action, $upperlevel, $element_id);
+ }
+ break;
+ case 'FAMC':
+ case 'FAMS':
+ echo FunctionsPrint::printFindFamilyLink($element_id);
+ break;
+ case 'ALIA':
+ case 'ASSO':
+ case '_ASSO':
+ echo FunctionsPrint::printFindIndividualLink($element_id, $element_id . '_description');
+ break;
+ case 'FILE':
+ FunctionsPrint::printFindMediaLink($element_id, '0file');
+ break;
+ case 'SOUR':
+ echo FunctionsPrint::printFindSourceLink($element_id, $element_id . '_description'), ' ', self::printAddNewSourceLink($element_id);
+ //-- checkboxes to apply '1 SOUR' to BIRT/MARR/DEAT as '2 SOUR'
+ if ($level === 1) {
+ echo '<br>';
+ switch ($WT_TREE->getPreference('PREFER_LEVEL2_SOURCES')) {
+ case '2': // records
+ $level1_checked = 'checked';
+ $level2_checked = '';
+ break;
+ case '1': // facts
+ $level1_checked = '';
+ $level2_checked = 'checked';
+ break;
+ case '0': // none
+ default:
+ $level1_checked = '';
+ $level2_checked = '';
+ break;
+ }
+ if (strpos($bdm, 'B') !== false) {
+ echo '&nbsp;<input type="checkbox" name="SOUR_INDI" ', $level1_checked, ' value="1">';
+ echo I18N::translate('Individual');
+ if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('QUICK_REQUIRED_FACTS'), $matches)) {
+ foreach ($matches[1] as $match) {
+ if (!in_array($match, explode('|', WT_EVENTS_DEAT))) {
+ echo '&nbsp;<input type="checkbox" name="SOUR_', $match, '" ', $level2_checked, ' value="1">';
+ echo GedcomTag::getLabel($match);
+ }
+ }
+ }
+ }
+ if (strpos($bdm, 'D') !== false) {
+ if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('QUICK_REQUIRED_FACTS'), $matches)) {
+ foreach ($matches[1] as $match) {
+ if (in_array($match, explode('|', WT_EVENTS_DEAT))) {
+ echo '&nbsp;<input type="checkbox" name="SOUR_', $match, '"', $level2_checked, ' value="1">';
+ echo GedcomTag::getLabel($match);
+ }
+ }
+ }
+ }
+ if (strpos($bdm, 'M') !== false) {
+ echo '&nbsp;<input type="checkbox" name="SOUR_FAM" ', $level1_checked, ' value="1">';
+ echo I18N::translate('Family');
+ if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('QUICK_REQUIRED_FAMFACTS'), $matches)) {
+ foreach ($matches[1] as $match) {
+ echo '&nbsp;<input type="checkbox" name="SOUR_', $match, '"', $level2_checked, ' value="1">';
+ echo GedcomTag::getLabel($match);
+ }
+ }
+ }
+ }
+ break;
+ case 'REPO':
+ echo FunctionsPrint::printFindRepositoryLink($element_id), ' ', self::printAddNewRepositoryLink($element_id);
+ break;
+ case 'NOTE':
+ // Shared Notes Icons ========================================
+ if ($islink) {
+ // Print regular Shared Note icons ---------------------------
+ echo ' ', FunctionsPrint::printFindNoteLink($element_id, $element_id . '_description'), ' ', self::printAddNewNoteLink($element_id);
+ if ($value) {
+ echo ' ', self::printEditNoteLink($value);
+ }
+
+ // Allow the GEDFact_assistant module to create a formatted shared note.
+ if ($upperlevel === 'CENS' && Module::getModuleByName('GEDFact_assistant')) {
+ echo CensusAssistantModule::addNoteWithAssistantLink($element_id, $xref, $action);
+ }
+ }
+ break;
+ case 'OBJE':
+ echo FunctionsPrint::printFindMediaLink($element_id, '1media');
+ if (!$value) {
+ echo ' ', self::printAddNewMediaLink($element_id);
+ $value = 'new';
+ }
+ break;
+ }
+
+ echo '<div id="' . $element_id . '_description">';
+
+ // current value
+ if ($fact === 'DATE') {
+ $date = new Date($value);
+ echo $date->display();
+ }
+ if (($fact === 'ASSO' || $fact === '_ASSO') && $value === '') {
+ if ($level === 1) {
+ echo '<p class="small text-muted">' . I18N::translate('An associate is another individual who was involved with this individual, such as a friend or an employer.') . '</p>';
+ } else {
+ echo '<p class="small text-muted">' . I18N::translate('An associate is another individual who was involved with this fact or event, such as a witness or a priest.') . '</p>';
+ }
+ }
+
+ if ($value && $value !== 'new' && $islink) {
+ switch ($fact) {
+ case 'ALIA':
+ case 'ASSO':
+ case '_ASSO':
+ $tmp = Individual::getInstance($value, $WT_TREE);
+ if ($tmp) {
+ echo ' ', $tmp->getFullname();
+ }
+ break;
+ case 'SOUR':
+ $tmp = Source::getInstance($value, $WT_TREE);
+ if ($tmp) {
+ echo ' ', $tmp->getFullname();
+ }
+ break;
+ case 'NOTE':
+ $tmp = Note::getInstance($value, $WT_TREE);
+ if ($tmp) {
+ echo ' ', $tmp->getFullname();
+ }
+ break;
+ case 'OBJE':
+ $tmp = Media::getInstance($value, $WT_TREE);
+ if ($tmp) {
+ echo ' ', $tmp->getFullname();
+ }
+ break;
+ case 'REPO':
+ $tmp = Repository::getInstance($value, $WT_TREE);
+ if ($tmp) {
+ echo ' ', $tmp->getFullname();
+ }
+ break;
+ }
+ }
+
+ // pastable values
+ if ($fact === 'FORM' && $upperlevel === 'OBJE') {
+ FunctionsPrint::printAutoPasteLink($element_id, ConfigData::fileFormats());
+ }
+ echo '</div>', $extra, '</td></tr>';
+
+ return $element_id;
+ }
+
+ /**
+ * Prints collapsable fields to add ASSO/RELA, SOUR, OBJE, etc.
+ *
+ * @param string $tag
+ * @param int $level
+ * @param string $parent_tag
+ */
+ public static function printAddLayer($tag, $level = 2, $parent_tag = '') {
+ global $WT_TREE;
+
+ switch ($tag) {
+ case 'SOUR':
+ echo '<a href="#" onclick="return expand_layer(\'newsource\');"><i id="newsource_img" class="icon-plus"></i> ', I18N::translate('Add a new source citation'), '</a>';
+ echo '<br>';
+ echo '<div id="newsource" style="display: none;">';
+ echo '<table class="facts_table">';
+ // 2 SOUR
+ self::addSimpleTag($level . ' SOUR @');
+ // 3 PAGE
+ self::addSimpleTag(($level + 1) . ' PAGE');
+ // 3 DATA
+ // 4 TEXT
+ self::addSimpleTag(($level + 2) . ' TEXT');
+ if ($WT_TREE->getPreference('FULL_SOURCES')) {
+ // 4 DATE
+ self::addSimpleTag(($level + 2) . ' DATE', '', GedcomTag::getLabel('DATA:DATE'));
+ // 3 QUAY
+ self::addSimpleTag(($level + 1) . ' QUAY');
+ }
+ // 3 OBJE
+ self::addSimpleTag(($level + 1) . ' OBJE');
+ // 3 SHARED_NOTE
+ self::addSimpleTag(($level + 1) . ' SHARED_NOTE');
+ echo '</table></div>';
+ break;
+
+ case 'ASSO':
+ case 'ASSO2':
+ //-- Add a new ASSOciate
+ if ($tag === 'ASSO') {
+ echo "<a href=\"#\" onclick=\"return expand_layer('newasso');\"><i id=\"newasso_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a new associate'), '</a>';
+ echo '<br>';
+ echo '<div id="newasso" style="display: none;">';
+ } else {
+ echo "<a href=\"#\" onclick=\"return expand_layer('newasso2');\"><i id=\"newasso2_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a new associate'), '</a>';
+ echo '<br>';
+ echo '<div id="newasso2" style="display: none;">';
+ }
+ echo '<table class="facts_table">';
+ // 2 ASSO
+ self::addSimpleTag($level . ' ASSO @');
+ // 3 RELA
+ self::addSimpleTag(($level + 1) . ' RELA');
+ // 3 NOTE
+ self::addSimpleTag(($level + 1) . ' NOTE');
+ // 3 SHARED_NOTE
+ self::addSimpleTag(($level + 1) . ' SHARED_NOTE');
+ echo '</table></div>';
+ break;
+
+ case 'NOTE':
+ //-- Retrieve existing note or add new note to fact
+ echo "<a href=\"#\" onclick=\"return expand_layer('newnote');\"><i id=\"newnote_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a new note'), '</a>';
+ echo '<br>';
+ echo '<div id="newnote" style="display: none;">';
+ echo '<table class="facts_table">';
+ // 2 NOTE
+ self::addSimpleTag($level . ' NOTE');
+ echo '</table></div>';
+ break;
+
+ case 'SHARED_NOTE':
+ echo "<a href=\"#\" onclick=\"return expand_layer('newshared_note');\"><i id=\"newshared_note_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a new shared note'), '</a>';
+ echo '<br>';
+ echo '<div id="newshared_note" style="display: none;">';
+ echo '<table class="facts_table">';
+ // 2 SHARED NOTE
+ self::addSimpleTag($level . ' SHARED_NOTE', $parent_tag);
+ echo '</table></div>';
+ break;
+
+ case 'OBJE':
+ if ($WT_TREE->getPreference('MEDIA_UPLOAD') >= Auth::accessLevel($WT_TREE)) {
+ echo "<a href=\"#\" onclick=\"return expand_layer('newobje');\"><i id=\"newobje_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a new media object'), '</a>';
+ echo '<br>';
+ echo '<div id="newobje" style="display: none;">';
+ echo '<table class="facts_table">';
+ self::addSimpleTag($level . ' OBJE');
+ echo '</table></div>';
+ }
+ break;
+
+ case 'RESN':
+ echo "<a href=\"#\" onclick=\"return expand_layer('newresn');\"><i id=\"newresn_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a new restriction'), '</a>';
+ echo '<br>';
+ echo '<div id="newresn" style="display: none;">';
+ echo '<table class="facts_table">';
+ // 2 RESN
+ self::addSimpleTag($level . ' RESN');
+ echo '</table></div>';
+ break;
+ }
+ }
+
+ /**
+ * Add some empty tags to create a new fact.
+ *
+ * @param string $fact
+ */
+ public static function addSimpleTags($fact) {
+ global $WT_TREE;
+
+ // For new individuals, these facts default to "Y"
+ if ($fact === 'MARR') {
+ self::addSimpleTag('0 ' . $fact . ' Y');
+ } else {
+ self::addSimpleTag('0 ' . $fact);
+ }
+
+ if (!in_array($fact, ConfigData::nonDateFacts())) {
+ self::addSimpleTag('0 DATE', $fact, GedcomTag::getLabel($fact . ':DATE'));
+ }
+
+ if (!in_array($fact, ConfigData::nonPlaceFacts())) {
+ self::addSimpleTag('0 PLAC', $fact, GedcomTag::getLabel($fact . ':PLAC'));
+
+ if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
+ foreach ($match[1] as $tag) {
+ self::addSimpleTag('0 ' . $tag, $fact, GedcomTag::getLabel($fact . ':PLAC:' . $tag));
+ }
+ }
+ self::addSimpleTag('0 MAP', $fact);
+ self::addSimpleTag('0 LATI', $fact);
+ self::addSimpleTag('0 LONG', $fact);
+ }
+ }
+
+ /**
+ * Assemble the pieces of a newly created record into gedcom
+ *
+ * @return string
+ */
+ public static function addNewName() {
+ global $WT_TREE;
+
+ $gedrec = "\n1 NAME " . Filter::post('NAME');
+
+ $tags = array('NPFX', 'GIVN', 'SPFX', 'SURN', 'NSFX');
+
+ if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_NAME_FACTS'), $match)) {
+ $tags = array_merge($tags, $match[1]);
+ }
+
+ // Paternal and Polish and Lithuanian surname traditions can also create a _MARNM
+ $SURNAME_TRADITION = $WT_TREE->getPreference('SURNAME_TRADITION');
+ if ($SURNAME_TRADITION === 'paternal' || $SURNAME_TRADITION === 'polish' || $SURNAME_TRADITION === 'lithuanian') {
+ $tags[] = '_MARNM';
+ }
+
+ foreach (array_unique($tags) as $tag) {
+ $TAG = Filter::post($tag);
+ if ($TAG) {
+ $gedrec .= "\n2 {$tag} {$TAG}";
+ }
+ }
+
+ return $gedrec;
+ }
+
+ /**
+ * @return string
+ */
+ public static function addNewSex() {
+ switch (Filter::post('SEX', '[MF]', 'U')) {
+ case 'M':
+ return "\n1 SEX M";
+ case 'F':
+ return "\n1 SEX F";
+ default:
+ return "\n1 SEX U";
+ }
+ }
+
+ /**
+ * @param string $fact
+ *
+ * @return string
+ */
+ public static function addNewFact($fact) {
+ global $WT_TREE;
+
+ $FACT = Filter::post($fact);
+ $DATE = Filter::post($fact . '_DATE');
+ $PLAC = Filter::post($fact . '_PLAC');
+ if ($DATE || $PLAC || $FACT && $FACT !== 'Y') {
+ if ($FACT && $FACT !== 'Y') {
+ $gedrec = "\n1 " . $fact . ' ' . $FACT;
+ } else {
+ $gedrec = "\n1 " . $fact;
+ }
+ if ($DATE) {
+ $gedrec .= "\n2 DATE " . $DATE;
+ }
+ if ($PLAC) {
+ $gedrec .= "\n2 PLAC " . $PLAC;
+
+ if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
+ foreach ($match[1] as $tag) {
+ $TAG = Filter::post($fact . '_' . $tag);
+ if ($TAG) {
+ $gedrec .= "\n3 " . $tag . ' ' . $TAG;
+ }
+ }
+ }
+ $LATI = Filter::post($fact . '_LATI');
+ $LONG = Filter::post($fact . '_LONG');
+ if ($LATI || $LONG) {
+ $gedrec .= "\n3 MAP\n4 LATI " . $LATI . "\n4 LONG " . $LONG;
+ }
+ }
+ if (Filter::postBool('SOUR_' . $fact)) {
+ return self::updateSource($gedrec, 2);
+ } else {
+ return $gedrec;
+ }
+ } elseif ($FACT === 'Y') {
+ if (Filter::postBool('SOUR_' . $fact)) {
+ return self::updateSource("\n1 " . $fact . ' Y', 2);
+ } else {
+ return "\n1 " . $fact . ' Y';
+ }
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * This function splits the $glevels, $tag, $islink, and $text arrays so that the
+ * entries associated with a SOUR record are separate from everything else.
+ *
+ * Input arrays:
+ * - $glevels[] - an array of the gedcom level for each line that was edited
+ * - $tag[] - an array of the tags for each gedcom line that was edited
+ * - $islink[] - an array of 1 or 0 values to indicate when the text is a link element
+ * - $text[] - an array of the text data for each line
+ *
+ * Output arrays:
+ * ** For the SOUR record:
+ * - $glevelsSOUR[] - an array of the gedcom level for each line that was edited
+ * - $tagSOUR[] - an array of the tags for each gedcom line that was edited
+ * - $islinkSOUR[] - an array of 1 or 0 values to indicate when the text is a link element
+ * - $textSOUR[] - an array of the text data for each line
+ * ** For the remaining records:
+ * - $glevelsRest[] - an array of the gedcom level for each line that was edited
+ * - $tagRest[] - an array of the tags for each gedcom line that was edited
+ * - $islinkRest[] - an array of 1 or 0 values to indicate when the text is a link element
+ * - $textRest[] - an array of the text data for each line
+ */
+ public static function splitSource() {
+ global $glevels, $tag, $islink, $text;
+ global $glevelsSOUR, $tagSOUR, $islinkSOUR, $textSOUR;
+ global $glevelsRest, $tagRest, $islinkRest, $textRest;
+
+ $glevelsSOUR = array();
+ $tagSOUR = array();
+ $islinkSOUR = array();
+ $textSOUR = array();
+
+ $glevelsRest = array();
+ $tagRest = array();
+ $islinkRest = array();
+ $textRest = array();
+
+ $inSOUR = false;
+
+ for ($i = 0; $i < count($glevels); $i++) {
+ if ($inSOUR) {
+ if ($levelSOUR < $glevels[$i]) {
+ $dest = 'S';
+ } else {
+ $inSOUR = false;
+ $dest = 'R';
+ }
+ } else {
+ if ($tag[$i] === 'SOUR') {
+ $inSOUR = true;
+ $levelSOUR = $glevels[$i];
+ $dest = 'S';
+ } else {
+ $dest = 'R';
+ }
+ }
+ if ($dest === 'S') {
+ $glevelsSOUR[] = $glevels[$i];
+ $tagSOUR[] = $tag[$i];
+ $islinkSOUR[] = $islink[$i];
+ $textSOUR[] = $text[$i];
+ } else {
+ $glevelsRest[] = $glevels[$i];
+ $tagRest[] = $tag[$i];
+ $islinkRest[] = $islink[$i];
+ $textRest[] = $text[$i];
+ }
+ }
+ }
+
+ /**
+ * Add new GEDCOM lines from the $xxxSOUR interface update arrays, which
+ * were produced by the splitSOUR() function.
+ * See the FunctionsEdit::handle_updatesges() function for details.
+ *
+ * @param string $inputRec
+ * @param string $levelOverride
+ *
+ * @return string
+ */
+ public static function updateSource($inputRec, $levelOverride = 'no') {
+ global $glevels, $tag, $islink, $text;
+ global $glevelsSOUR, $tagSOUR, $islinkSOUR, $textSOUR;
+
+ if (count($tagSOUR) === 0) {
+ return $inputRec; // No update required
+ }
+
+ // Save original interface update arrays before replacing them with the xxxSOUR ones
+ $glevelsSave = $glevels;
+ $tagSave = $tag;
+ $islinkSave = $islink;
+ $textSave = $text;
+
+ $glevels = $glevelsSOUR;
+ $tag = $tagSOUR;
+ $islink = $islinkSOUR;
+ $text = $textSOUR;
+
+ $myRecord = self::handleUpdates($inputRec, $levelOverride); // Now do the update
+
+ // Restore the original interface update arrays (just in case ...)
+ $glevels = $glevelsSave;
+ $tag = $tagSave;
+ $islink = $islinkSave;
+ $text = $textSave;
+
+ return $myRecord;
+ }
+
+ /**
+ * Add new GEDCOM lines from the $xxxRest interface update arrays, which
+ * were produced by the splitSOUR() function.
+ * See the FunctionsEdit::handle_updatesges() function for details.
+ *
+ * @param string $inputRec
+ * @param string $levelOverride
+ *
+ * @return string
+ */
+ public static function updateRest($inputRec, $levelOverride = 'no') {
+ global $glevels, $tag, $islink, $text;
+ global $glevelsRest, $tagRest, $islinkRest, $textRest;
+
+ if (count($tagRest) === 0) {
+ return $inputRec; // No update required
+ }
+
+ // Save original interface update arrays before replacing them with the xxxRest ones
+ $glevelsSave = $glevels;
+ $tagSave = $tag;
+ $islinkSave = $islink;
+ $textSave = $text;
+
+ $glevels = $glevelsRest;
+ $tag = $tagRest;
+ $islink = $islinkRest;
+ $text = $textRest;
+
+ $myRecord = self::handleUpdates($inputRec, $levelOverride); // Now do the update
+
+ // Restore the original interface update arrays (just in case ...)
+ $glevels = $glevelsSave;
+ $tag = $tagSave;
+ $islink = $islinkSave;
+ $text = $textSave;
+
+ return $myRecord;
+ }
+
+ /**
+ * Add new gedcom lines from interface update arrays
+ * The edit_interface and FunctionsEdit::add_simple_tag function produce the following
+ * arrays incoming from the $_POST form
+ * - $glevels[] - an array of the gedcom level for each line that was edited
+ * - $tag[] - an array of the tags for each gedcom line that was edited
+ * - $islink[] - an array of 1 or 0 values to tell whether the text is a link element and should be surrounded by @@
+ * - $text[] - an array of the text data for each line
+ * With these arrays you can recreate the gedcom lines like this
+ * <code>$glevel[0].' '.$tag[0].' '.$text[0]</code>
+ * There will be an index in each of these arrays for each line of the gedcom
+ * fact that is being edited.
+ * If the $text[] array is empty for the given line, then it means that the
+ * user removed that line during editing or that the line is supposed to be
+ * empty (1 DEAT, 1 BIRT) for example. To know if the line should be removed
+ * there is a section of code that looks ahead to the next lines to see if there
+ * are sub lines. For example we don't want to remove the 1 DEAT line if it has
+ * a 2 PLAC or 2 DATE line following it. If there are no sub lines, then the line
+ * can be safely removed.
+ *
+ * @param string $newged the new gedcom record to add the lines to
+ * @param string $levelOverride Override GEDCOM level specified in $glevels[0]
+ *
+ * @return string The updated gedcom record
+ */
+ public static function handleUpdates($newged, $levelOverride = 'no') {
+ global $glevels, $islink, $tag, $uploaded_files, $text;
+
+ if ($levelOverride === 'no' || count($glevels) === 0) {
+ $levelAdjust = 0;
+ } else {
+ $levelAdjust = $levelOverride - $glevels[0];
+ }
+
+ for ($j = 0; $j < count($glevels); $j++) {
+
+ // Look for empty SOUR reference with non-empty sub-records.
+ // This can happen when the SOUR entry is deleted but its sub-records
+ // were incorrectly left intact.
+ // The sub-records should be deleted.
+ if ($tag[$j] === 'SOUR' && ($text[$j] === '@@' || $text[$j] === '')) {
+ $text[$j] = '';
+ $k = $j + 1;
+ while (($k < count($glevels)) && ($glevels[$k] > $glevels[$j])) {
+ $text[$k] = '';
+ $k++;
+ }
+ }
+
+ if (trim($text[$j]) !== '') {
+ $pass = true;
+ } else {
+ //-- for facts with empty values they must have sub records
+ //-- this section checks if they have subrecords
+ $k = $j + 1;
+ $pass = false;
+ while (($k < count($glevels)) && ($glevels[$k] > $glevels[$j])) {
+ if ($text[$k] !== '') {
+ if (($tag[$j] !== 'OBJE') || ($tag[$k] === 'FILE')) {
+ $pass = true;
+ break;
+ }
+ }
+ if (($tag[$k] === 'FILE') && (count($uploaded_files) > 0)) {
+ $filename = array_shift($uploaded_files);
+ if (!empty($filename)) {
+ $text[$k] = $filename;
+ $pass = true;
+ break;
+ }
+ }
+ $k++;
+ }
+ }
+
+ //-- if the value is not empty or it has sub lines
+ //--- then write the line to the gedcom record
+ //if ((($text[trim($j)]!='')||($pass==true)) && (strlen($text[$j]) > 0)) {
+ //-- we have to let some emtpy text lines pass through... (DEAT, BIRT, etc)
+ if ($pass) {
+ $newline = $glevels[$j] + $levelAdjust . ' ' . $tag[$j];
+ //-- check and translate the incoming dates
+ if ($tag[$j] === 'DATE' && $text[$j] !== '') {
+ }
+ if ($text[$j] !== '') {
+ if ($islink[$j]) {
+ $newline .= ' @' . $text[$j] . '@';
+ } else {
+ $newline .= ' ' . $text[$j];
+ }
+ }
+ $newged .= "\n" . str_replace("\n", "\n" . (1 + substr($newline, 0, 1)) . ' CONT ', $newline);
+ }
+ }
+
+ return $newged;
+ }
+
+ /**
+ * builds the form for adding new facts
+ *
+ * @param string $fact the new fact we are adding
+ */
+ public static function createAddForm($fact) {
+ global $tags, $WT_TREE;
+
+ $tags = array();
+
+ // handle MARRiage TYPE
+ if (substr($fact, 0, 5) === 'MARR_') {
+ $tags[0] = 'MARR';
+ self::addSimpleTag('1 MARR');
+ self::insertMissingSubtags($fact);
+ } else {
+ $tags[0] = $fact;
+ if ($fact === '_UID') {
+ $fact .= ' ' . GedcomTag::createUid();
+ }
+ // These new level 1 tags need to be turned into links
+ if (in_array($fact, array('ALIA', 'ASSO'))) {
+ $fact .= ' @';
+ }
+ if (in_array($fact, ConfigData::emptyFacts())) {
+ self::addSimpleTag('1 ' . $fact . ' Y');
+ } else {
+ self::addSimpleTag('1 ' . $fact);
+ }
+ self::insertMissingSubtags($tags[0]);
+ //-- handle the special SOURce case for level 1 sources [ 1759246 ]
+ if ($fact === 'SOUR') {
+ self::addSimpleTag('2 PAGE');
+ self::addSimpleTag('3 TEXT');
+ if ($WT_TREE->getPreference('FULL_SOURCES')) {
+ self::addSimpleTag('3 DATE', '', GedcomTag::getLabel('DATA:DATE'));
+ self::addSimpleTag('2 QUAY');
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a form to edit a Fact object.
+ *
+ * @param GedcomRecord $record
+ * @param Fact $fact
+ *
+ * @return string
+ */
+ public static function createEditForm(GedcomRecord $record, Fact $fact) {
+ global $tags, $WT_TREE;
+
+ $pid = $record->getXref();
+
+ $tags = array();
+ $gedlines = explode("\n", $fact->getGedcom());
+
+ $linenum = 0;
+ $fields = explode(' ', $gedlines[$linenum]);
+ $glevel = $fields[0];
+ $level = $glevel;
+
+ $type = $fact->getTag();
+ $parent = $fact->getParent();
+ $level0type = $parent::RECORD_TYPE;
+ $level1type = $type;
+
+ $i = $linenum;
+ $inSource = false;
+ $levelSource = 0;
+ $add_date = true;
+ // List of tags we would expect at the next level
+ // NB add_missing_subtags() already takes care of the simple cases
+ // where a level 1 tag is missing a level 2 tag. Here we only need to
+ // handle the more complicated cases.
+ $expected_subtags = array(
+ 'SOUR' => array('PAGE', 'DATA'),
+ 'DATA' => array('TEXT'),
+ 'PLAC' => array('MAP'),
+ 'MAP' => array('LATI', 'LONG'),
+ );
+ if ($record->getTree()->getPreference('FULL_SOURCES')) {
+ $expected_subtags['SOUR'][] = 'QUAY';
+ $expected_subtags['DATA'][] = 'DATE';
+ }
+ if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $record->getTree()->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
+ $expected_subtags['PLAC'] = array_merge($match[1], $expected_subtags['PLAC']);
+ }
+
+ $stack = array(0 => $level0type);
+ // Loop on existing tags :
+ while (true) {
+ // Keep track of our hierarchy, e.g. 1=>BIRT, 2=>PLAC, 3=>FONE
+ $stack[(int) $level] = $type;
+ // Merge them together, e.g. BIRT:PLAC:FONE
+ $label = implode(':', array_slice($stack, 1, $level));
+
+ $text = '';
+ for ($j = 2; $j < count($fields); $j++) {
+ if ($j > 2) {
+ $text .= ' ';
+ }
+ $text .= $fields[$j];
+ }
+ $text = rtrim($text);
+ while (($i + 1 < count($gedlines)) && (preg_match("/" . ($level + 1) . ' CONT ?(.*)/', $gedlines[$i + 1], $cmatch) > 0)) {
+ $text .= "\n" . $cmatch[1];
+ $i++;
+ }
+
+ if ($type === 'SOUR') {
+ $inSource = true;
+ $levelSource = $level;
+ } elseif ($levelSource >= $level) {
+ $inSource = false;
+ }
+
+ if ($type !== 'DATA' && $type !== 'CONT') {
+ $tags[] = $type;
+ $person = Individual::getInstance($pid, $WT_TREE);
+ $subrecord = $level . ' ' . $type . ' ' . $text;
+ if ($inSource && $type === 'DATE') {
+ self::addSimpleTag($subrecord, '', GedcomTag::getLabel($label, $person));
+ } elseif (!$inSource && $type === 'DATE') {
+ self::addSimpleTag($subrecord, $level1type, GedcomTag::getLabel($label, $person));
+ $add_date = false;
+ } elseif ($type === 'STAT') {
+ self::addSimpleTag($subrecord, $level1type, GedcomTag::getLabel($label, $person));
+ } elseif ($level0type === 'REPO') {
+ $repo = Repository::getInstance($pid, $WT_TREE);
+ self::addSimpleTag($subrecord, $level0type, GedcomTag::getLabel($label, $repo));
+ } else {
+ self::addSimpleTag($subrecord, $level0type, GedcomTag::getLabel($label, $person));
+ }
+ }
+
+ // Get a list of tags present at the next level
+ $subtags = array();
+ for ($ii = $i + 1; isset($gedlines[$ii]) && preg_match('/^\s*(\d+)\s+(\S+)/', $gedlines[$ii], $mm) && $mm[1] > $level; ++$ii) {
+ if ($mm[1] == $level + 1) {
+ $subtags[] = $mm[2];
+ }
+ }
+
+ // Insert missing tags
+ if (!empty($expected_subtags[$type])) {
+ foreach ($expected_subtags[$type] as $subtag) {
+ if (!in_array($subtag, $subtags)) {
+ if (!$inSource || $subtag !== 'DATA') {
+ self::addSimpleTag(($level + 1) . ' ' . $subtag, '', GedcomTag::getLabel($label . ':' . $subtag));
+ }
+ if (!empty($expected_subtags[$subtag])) {
+ foreach ($expected_subtags[$subtag] as $subsubtag) {
+ self::addSimpleTag(($level + 2) . ' ' . $subsubtag, '', GedcomTag::getLabel($label . ':' . $subtag . ':' . $subsubtag));
+ }
+ }
+ }
+ }
+ }
+
+ // Awkward special cases
+ if ($level == 2 && $type === 'DATE' && in_array($level1type, ConfigData::dateAndTime()) && !in_array('TIME', $subtags)) {
+ self::addSimpleTag('3 TIME'); // TIME is NOT a valid 5.5.1 tag
+ }
+ if ($level == 2 && $type === 'STAT' && GedcomCodeTemp::isTagLDS($level1type) && !in_array('DATE', $subtags)) {
+ self::addSimpleTag('3 DATE', '', GedcomTag::getLabel('STAT:DATE'));
+ }
+
+ $i++;
+ if (isset($gedlines[$i])) {
+ $fields = explode(' ', $gedlines[$i]);
+ $level = $fields[0];
+ if (isset($fields[1])) {
+ $type = trim($fields[1]);
+ } else {
+ $level = 0;
+ }
+ } else {
+ $level = 0;
+ }
+ if ($level <= $glevel) {
+ break;
+ }
+ }
+
+ if ($level1type !== '_PRIM') {
+ self::insertMissingSubtags($level1type, $add_date);
+ }
+
+ return $level1type;
+ }
+
+ /**
+ * Populates the global $tags array with any missing sub-tags.
+ *
+ * @param string $level1tag the type of the level 1 gedcom record
+ * @param bool $add_date
+ */
+ public static function insertMissingSubtags($level1tag, $add_date = false) {
+ global $tags, $WT_TREE;
+
+ // handle MARRiage TYPE
+ $type_val = '';
+ if (substr($level1tag, 0, 5) === 'MARR_') {
+ $type_val = substr($level1tag, 5);
+ $level1tag = 'MARR';
+ }
+
+ foreach (ConfigData::levelTwoTags() as $key => $value) {
+ if ($key === 'DATE' && in_array($level1tag, ConfigData::nonDateFacts()) || $key === 'PLAC' && in_array($level1tag, ConfigData::nonPlaceFacts())) {
+ continue;
+ }
+ if (in_array($level1tag, $value) && !in_array($key, $tags)) {
+ if ($key === 'TYPE') {
+ self::addSimpleTag('2 TYPE ' . $type_val, $level1tag);
+ } elseif ($level1tag === '_TODO' && $key === 'DATE') {
+ self::addSimpleTag('2 ' . $key . ' ' . strtoupper(date('d M Y')), $level1tag);
+ } elseif ($level1tag === '_TODO' && $key === '_WT_USER') {
+ self::addSimpleTag('2 ' . $key . ' ' . Auth::user()->getUserName(), $level1tag);
+ } elseif ($level1tag === 'TITL' && strstr($WT_TREE->getPreference('ADVANCED_NAME_FACTS'), $key) !== false) {
+ self::addSimpleTag('2 ' . $key, $level1tag);
+ } elseif ($level1tag === 'NAME' && strstr($WT_TREE->getPreference('ADVANCED_NAME_FACTS'), $key) !== false) {
+ self::addSimpleTag('2 ' . $key, $level1tag);
+ } elseif ($level1tag !== 'TITL' && $level1tag !== 'NAME') {
+ self::addSimpleTag('2 ' . $key, $level1tag);
+ }
+ // Add level 3/4 tags as appropriate
+ switch ($key) {
+ case 'PLAC':
+ if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
+ foreach ($match[1] as $tag) {
+ self::addSimpleTag('3 ' . $tag, '', GedcomTag::getLabel($level1tag . ':PLAC:' . $tag));
+ }
+ }
+ self::addSimpleTag('3 MAP');
+ self::addSimpleTag('4 LATI');
+ self::addSimpleTag('4 LONG');
+ break;
+ case 'FILE':
+ self::addSimpleTag('3 FORM');
+ break;
+ case 'EVEN':
+ self::addSimpleTag('3 DATE');
+ self::addSimpleTag('3 PLAC');
+ break;
+ case 'STAT':
+ if (GedcomCodeTemp::isTagLDS($level1tag)) {
+ self::addSimpleTag('3 DATE', '', GedcomTag::getLabel('STAT:DATE'));
+ }
+ break;
+ case 'DATE':
+ // TIME is NOT a valid 5.5.1 tag
+ if (in_array($level1tag, ConfigData::dateAndTime())) {
+ self::addSimpleTag('3 TIME');
+ }
+ break;
+ case 'HUSB':
+ case 'WIFE':
+ self::addSimpleTag('3 AGE');
+ break;
+ case 'FAMC':
+ if ($level1tag === 'ADOP') {
+ self::addSimpleTag('3 ADOP BOTH');
+ }
+ break;
+ }
+ } elseif ($key === 'DATE' && $add_date) {
+ self::addSimpleTag('2 DATE', $level1tag, GedcomTag::getLabel($level1tag . ':DATE'));
+ }
+ }
+ // Do something (anything!) with unrecognized custom tags
+ if (substr($level1tag, 0, 1) === '_' && $level1tag !== '_UID' && $level1tag !== '_TODO') {
+ foreach (array('DATE', 'PLAC', 'ADDR', 'AGNC', 'TYPE', 'AGE') as $tag) {
+ if (!in_array($tag, $tags)) {
+ self::addSimpleTag('2 ' . $tag);
+ if ($tag === 'PLAC') {
+ if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
+ foreach ($match[1] as $ptag) {
+ self::addSimpleTag('3 ' . $ptag, '', GedcomTag::getLabel($level1tag . ':PLAC:' . $ptag));
+ }
+ }
+ self::addSimpleTag('3 MAP');
+ self::addSimpleTag('4 LATI');
+ self::addSimpleTag('4 LONG');
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/Functions/FunctionsExport.php b/app/Functions/FunctionsExport.php
new file mode 100644
index 0000000000..03b0c63ad2
--- /dev/null
+++ b/app/Functions/FunctionsExport.php
@@ -0,0 +1,314 @@
+<?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\Family;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Media;
+use Fisharebest\Webtrees\Note;
+use Fisharebest\Webtrees\Repository;
+use Fisharebest\Webtrees\Source;
+use Fisharebest\Webtrees\Tree;
+
+/**
+ * Class FunctionsExport - common functions
+ */
+class FunctionsExport {
+ /**
+ * Tidy up a gedcom record on export, for compatibility/portability.
+ *
+ * @param string $rec
+ *
+ * @return string
+ */
+ public static function reformatRecord($rec) {
+ global $WT_TREE;
+
+ $newrec = '';
+ foreach (preg_split('/[\r\n]+/', $rec, -1, PREG_SPLIT_NO_EMPTY) as $line) {
+ // Split long lines
+ // The total length of a GEDCOM line, including level number, cross-reference number,
+ // tag, value, delimiters, and terminator, must not exceed 255 (wide) characters.
+ if (mb_strlen($line) > WT_GEDCOM_LINE_LENGTH) {
+ list($level, $tag) = explode(' ', $line, 3);
+ if ($tag != 'CONT' && $tag != 'CONC') {
+ $level++;
+ }
+ do {
+ // Split after $pos chars
+ $pos = WT_GEDCOM_LINE_LENGTH;
+ if ($WT_TREE->getPreference('WORD_WRAPPED_NOTES')) {
+ // Split on a space, and remove it (for compatibility with some desktop apps)
+ while ($pos && mb_substr($line, $pos - 1, 1) != ' ') {
+ --$pos;
+ }
+ if ($pos == strpos($line, ' ', 3) + 1) {
+ // No spaces in the data! Can’t split it :-(
+ break;
+ } else {
+ $newrec .= mb_substr($line, 0, $pos - 1) . WT_EOL;
+ $line = $level . ' CONC ' . mb_substr($line, $pos);
+ }
+ } else {
+ // Split on a non-space (standard gedcom behaviour)
+ while ($pos && mb_substr($line, $pos - 1, 1) == ' ') {
+ --$pos;
+ }
+ if ($pos == strpos($line, ' ', 3)) {
+ // No non-spaces in the data! Can’t split it :-(
+ break;
+ }
+ $newrec .= mb_substr($line, 0, $pos) . WT_EOL;
+ $line = $level . ' CONC ' . mb_substr($line, $pos);
+ }
+ } while (mb_strlen($line) > WT_GEDCOM_LINE_LENGTH);
+ }
+ $newrec .= $line . WT_EOL;
+ }
+
+ return $newrec;
+ }
+
+ /**
+ * Create a header for a (newly-created or already-imported) gedcom file.
+ *
+ * @param Tree $tree
+ *
+ * @return string
+ */
+ public static function gedcomHeader(Tree $tree) {
+ // Default values for a new header
+ $HEAD = "0 HEAD";
+ $SOUR = "\n1 SOUR " . WT_WEBTREES . "\n2 NAME " . WT_WEBTREES . "\n2 VERS " . WT_VERSION;
+ $DEST = "\n1 DEST DISKETTE";
+ $DATE = "\n1 DATE " . strtoupper(date("d M Y")) . "\n2 TIME " . date("H:i:s");
+ $GEDC = "\n1 GEDC\n2 VERS 5.5.1\n2 FORM Lineage-Linked";
+ $CHAR = "\n1 CHAR UTF-8";
+ $FILE = "\n1 FILE " . $tree->getName();
+ $LANG = "";
+ $PLAC = "\n1 PLAC\n2 FORM City, County, State/Province, Country";
+ $COPR = "";
+ $SUBN = "";
+ $SUBM = "\n1 SUBM @SUBM@\n0 @SUBM@ SUBM\n1 NAME " . Auth::user()->getUserName(); // The SUBM record is mandatory
+
+ // Preserve some values from the original header
+ $record = GedcomRecord::getInstance('HEAD', $tree);
+ if ($fact = $record->getFirstFact('PLAC')) {
+ $PLAC = "\n1 PLAC\n2 FORM " . $fact->getAttribute('FORM');
+ }
+ if ($fact = $record->getFirstFact('LANG')) {
+ $LANG = $fact->getValue();
+ }
+ if ($fact = $record->getFirstFact('SUBN')) {
+ $SUBN = $fact->getValue();
+ }
+ if ($fact = $record->getFirstFact('COPR')) {
+ $COPR = $fact->getValue();
+ }
+ // Link to actual SUBM/SUBN records, if they exist
+ $subn =
+ Database::prepare("SELECT o_id FROM `##other` WHERE o_type=? AND o_file=?")
+ ->execute(array('SUBN', $tree->getTreeId()))
+ ->fetchOne();
+ if ($subn) {
+ $SUBN = "\n1 SUBN @{$subn}@";
+ }
+ $subm =
+ Database::prepare("SELECT o_id FROM `##other` WHERE o_type=? AND o_file=?")
+ ->execute(array('SUBM', $tree->getTreeId()))
+ ->fetchOne();
+ if ($subm) {
+ $SUBM = "\n1 SUBM @{$subm}@";
+ }
+
+ return $HEAD . $SOUR . $DEST . $DATE . $GEDC . $CHAR . $FILE . $COPR . $LANG . $PLAC . $SUBN . $SUBM . "\n";
+ }
+
+ /**
+ * Prepend the GEDCOM_MEDIA_PATH to media filenames.
+ *
+ * @param string $rec
+ * @param string $path
+ *
+ * @return string
+ */
+ public static function convertMediaPath($rec, $path) {
+ if ($path && preg_match('/\n1 FILE (.+)/', $rec, $match)) {
+ $old_file_name = $match[1];
+ // Don’t modify external links
+ if (!preg_match('~^(https?|ftp):~', $old_file_name)) {
+ // Adding a windows path? Convert the slashes.
+ if (strpos($path, '\\') !== false) {
+ $new_file_name = preg_replace('~/+~', '\\', $old_file_name);
+ } else {
+ $new_file_name = $old_file_name;
+ }
+ // Path not present - add it.
+ if (strpos($new_file_name, $path) === false) {
+ $new_file_name = $path . $new_file_name;
+ }
+ $rec = str_replace("\n1 FILE " . $old_file_name, "\n1 FILE " . $new_file_name, $rec);
+ }
+ }
+
+ return $rec;
+ }
+
+ /**
+ * Export the database in GEDCOM format
+ *
+ * @param Tree $tree Which tree to export
+ * @param resource $gedout Handle to a writable stream
+ * @param string[] $exportOptions Export options are as follows:
+ * 'privatize': which Privacy rules apply? (none, visitor, user, manager)
+ * 'toANSI': should the output be produced in ISO-8859-1 instead of UTF-8? (yes, no)
+ * 'path': what constant should prefix all media file paths? (eg: media/ or c:\my pictures\my family
+ * 'slashes': what folder separators apply to media file paths? (forward, backward)
+ */
+ public static function exportGedcom(Tree $tree, $gedout, $exportOptions) {
+ switch ($exportOptions['privatize']) {
+ case 'gedadmin':
+ $access_level = Auth::PRIV_NONE;
+ break;
+ case 'user':
+ $access_level = Auth::PRIV_USER;
+ break;
+ case 'visitor':
+ $access_level = Auth::PRIV_PRIVATE;
+ break;
+ case 'none':
+ $access_level = Auth::PRIV_HIDE;
+ break;
+ }
+
+ $head = self::gedcomHeader($tree);
+ if ($exportOptions['toANSI'] == 'yes') {
+ $head = str_replace('UTF-8', 'ANSI', $head);
+ $head = utf8_decode($head);
+ }
+ $head = self::reformatRecord($head);
+ fwrite($gedout, $head);
+
+ // Buffer the output. Lots of small fwrite() calls can be very slow when writing large gedcoms.
+ $buffer = '';
+
+ // Generate the OBJE/SOUR/REPO/NOTE records first, as their privacy calcualations involve
+ // database queries, and we wish to avoid large gaps between queries due to MySQL connection timeouts.
+ $tmp_gedcom = '';
+ $rows = Database::prepare(
+ "SELECT m_id AS xref, m_gedcom AS gedcom" .
+ " FROM `##media` WHERE m_file = :tree_id ORDER BY m_id"
+ )->execute(array(
+ 'tree_id' => $tree->getTreeId(),
+ ))->fetchAll();
+
+ foreach ($rows as $row) {
+ $rec = Media::getInstance($row->xref, $tree, $row->gedcom)->privatizeGedcom($access_level);
+ $rec = self::convertMediaPath($rec, $exportOptions['path']);
+ if ($exportOptions['toANSI'] === 'yes') {
+ $rec = utf8_decode($rec);
+ }
+ $tmp_gedcom .= self::reformatRecord($rec);
+ }
+
+ $rows = Database::prepare(
+ "SELECT s_id AS xref, s_file AS gedcom_id, s_gedcom AS gedcom" .
+ " FROM `##sources` WHERE s_file = :tree_id ORDER BY s_id"
+ )->execute(array(
+ 'tree_id' => $tree->getTreeId(),
+ ))->fetchAll();
+
+ foreach ($rows as $row) {
+ $rec = Source::getInstance($row->xref, $tree, $row->gedcom)->privatizeGedcom($access_level);
+ if ($exportOptions['toANSI'] === 'yes') {
+ $rec = utf8_decode($rec);
+ }
+ $tmp_gedcom .= self::reformatRecord($rec);
+ }
+
+ $rows = Database::prepare(
+ "SELECT o_type AS type, o_id AS xref, o_gedcom AS gedcom" .
+ " FROM `##other` WHERE o_file = :tree_id AND o_type NOT IN ('HEAD', 'TRLR') ORDER BY o_id"
+ )->execute(array(
+ 'tree_id' => $tree->getTreeId(),
+ ))->fetchAll();
+
+ foreach ($rows as $row) {
+ switch ($row->type) {
+ case 'NOTE':
+ $record = Note::getInstance($row->xref, $tree, $row->gedcom);
+ break;
+ case 'REPO':
+ $record = Repository::getInstance($row->xref, $tree, $row->gedcom);
+ break;
+ default:
+ $record = GedcomRecord::getInstance($row->xref, $tree, $row->gedcom);
+ break;
+ }
+
+ $rec = $record->privatizeGedcom($access_level);
+ if ($exportOptions['toANSI'] === 'yes') {
+ $rec = utf8_decode($rec);
+ }
+ $tmp_gedcom .= self::reformatRecord($rec);
+ }
+
+ $rows = Database::prepare(
+ "SELECT i_id AS xref, i_gedcom AS gedcom" .
+ " FROM `##individuals` WHERE i_file = :tree_id ORDER BY i_id"
+ )->execute(array(
+ 'tree_id' => $tree->getTreeId(),
+ ))->fetchAll();
+
+ foreach ($rows as $row) {
+ $rec = Individual::getInstance($row->xref, $tree, $row->gedcom)->privatizeGedcom($access_level);
+ if ($exportOptions['toANSI'] === 'yes') {
+ $rec = utf8_decode($rec);
+ }
+ $buffer .= self::reformatRecord($rec);
+ if (strlen($buffer) > 65536) {
+ fwrite($gedout, $buffer);
+ $buffer = '';
+ }
+ }
+
+ $rows = Database::prepare(
+ "SELECT f_id AS xref, f_gedcom AS gedcom" .
+ " FROM `##families` WHERE f_file = :tree_id ORDER BY f_id"
+ )->execute(array(
+ 'tree_id' => $tree->getTreeId(),
+ ))->fetchAll();
+
+ foreach ($rows as $row) {
+ $rec = Family::getInstance($row->xref, $tree, $row->gedcom)->privatizeGedcom($access_level);
+ if ($exportOptions['toANSI'] === 'yes') {
+ $rec = utf8_decode($rec);
+ }
+ $buffer .= self::reformatRecord($rec);
+ if (strlen($buffer) > 65536) {
+ fwrite($gedout, $buffer);
+ $buffer = '';
+ }
+ }
+
+ fwrite($gedout, $buffer);
+ fwrite($gedout, $tmp_gedcom);
+ fwrite($gedout, '0 TRLR' . WT_EOL);
+ }
+}
diff --git a/app/Functions/FunctionsImport.php b/app/Functions/FunctionsImport.php
new file mode 100644
index 0000000000..c9959635fd
--- /dev/null
+++ b/app/Functions/FunctionsImport.php
@@ -0,0 +1,1162 @@
+<?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\Database;
+use Fisharebest\Webtrees\Date;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\GedcomTag;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Log;
+use Fisharebest\Webtrees\Media;
+use Fisharebest\Webtrees\Note;
+use Fisharebest\Webtrees\Repository;
+use Fisharebest\Webtrees\Soundex;
+use Fisharebest\Webtrees\Source;
+use Fisharebest\Webtrees\Tree;
+use PDOException;
+
+/**
+ * Class FunctionsImport - common functions
+ */
+class FunctionsImport {
+ /**
+ * Tidy up a gedcom record on import, so that we can access it consistently/efficiently.
+ *
+ * @param string $rec
+ * @param Tree $tree
+ *
+ * @return string
+ */
+ public static function reformatRecord($rec, Tree $tree) {
+ // Strip out UTF8 formatting characters
+ $rec = str_replace(array(WT_UTF8_BOM, WT_UTF8_LRM, WT_UTF8_RLM), '', $rec);
+
+ // Strip out mac/msdos line endings
+ $rec = preg_replace("/[\r\n]+/", "\n", $rec);
+
+ // Extract lines from the record; lines consist of: level + optional xref + tag + optional data
+ $num_matches = preg_match_all('/^[ \t]*(\d+)[ \t]*(@[^@]*@)?[ \t]*(\w+)[ \t]?(.*)$/m', $rec, $matches, PREG_SET_ORDER);
+
+ // Process the record line-by-line
+ $newrec = '';
+ foreach ($matches as $n => $match) {
+ list(, $level, $xref, $tag, $data) = $match;
+ $tag = strtoupper($tag); // Tags should always be upper case
+ switch ($tag) {
+ // Convert PhpGedView tags to WT
+ case '_PGVU':
+ $tag = '_WT_USER';
+ break;
+ case '_PGV_OBJS':
+ $tag = '_WT_OBJE_SORT';
+ break;
+ // Convert FTM-style "TAG_FORMAL_NAME" into "TAG".
+ case 'ABBREVIATION':
+ $tag = 'ABBR';
+ break;
+ case 'ADDRESS':
+ $tag = 'ADDR';
+ break;
+ case 'ADDRESS1':
+ $tag = 'ADR1';
+ break;
+ case 'ADDRESS2':
+ $tag = 'ADR2';
+ break;
+ case 'ADDRESS3':
+ $tag = 'ADR3';
+ break;
+ case 'ADOPTION':
+ $tag = 'ADOP';
+ break;
+ case 'ADULT_CHRISTENING':
+ $tag = 'CHRA';
+ break;
+ case 'AFN':
+ // AFN values are upper case
+ $data = strtoupper($data);
+ break;
+ case 'AGENCY':
+ $tag = 'AGNC';
+ break;
+ case 'ALIAS':
+ $tag = 'ALIA';
+ break;
+ case 'ANCESTORS':
+ $tag = 'ANCE';
+ break;
+ case 'ANCES_INTEREST':
+ $tag = 'ANCI';
+ break;
+ case 'ANNULMENT':
+ $tag = 'ANUL';
+ break;
+ case 'ASSOCIATES':
+ $tag = 'ASSO';
+ break;
+ case 'AUTHOR':
+ $tag = 'AUTH';
+ break;
+ case 'BAPTISM':
+ $tag = 'BAPM';
+ break;
+ case 'BAPTISM_LDS':
+ $tag = 'BAPL';
+ break;
+ case 'BAR_MITZVAH':
+ $tag = 'BARM';
+ break;
+ case 'BAS_MITZVAH':
+ $tag = 'BASM';
+ break;
+ case 'BIRTH':
+ $tag = 'BIRT';
+ break;
+ case 'BLESSING':
+ $tag = 'BLES';
+ break;
+ case 'BURIAL':
+ $tag = 'BURI';
+ break;
+ case 'CALL_NUMBER':
+ $tag = 'CALN';
+ break;
+ case 'CASTE':
+ $tag = 'CAST';
+ break;
+ case 'CAUSE':
+ $tag = 'CAUS';
+ break;
+ case 'CENSUS':
+ $tag = 'CENS';
+ break;
+ case 'CHANGE':
+ $tag = 'CHAN';
+ break;
+ case 'CHARACTER':
+ $tag = 'CHAR';
+ break;
+ case 'CHILD':
+ $tag = 'CHIL';
+ break;
+ case 'CHILDREN_COUNT':
+ $tag = 'NCHI';
+ break;
+ case 'CHRISTENING':
+ $tag = 'CHR';
+ break;
+ case 'CONCATENATION':
+ $tag = 'CONC';
+ break;
+ case 'CONFIRMATION':
+ $tag = 'CONF';
+ break;
+ case 'CONFIRMATION_LDS':
+ $tag = 'CONL';
+ break;
+ case 'CONTINUED':
+ $tag = 'CONT';
+ break;
+ case 'COPYRIGHT':
+ $tag = 'COPR';
+ break;
+ case 'CORPORATE':
+ $tag = 'CORP';
+ break;
+ case 'COUNTRY':
+ $tag = 'CTRY';
+ break;
+ case 'CREMATION':
+ $tag = 'CREM';
+ break;
+ case 'DATE':
+ // Preserve text from INT dates
+ if (strpos($data, '(') !== false) {
+ list($date, $text) = explode('(', $data, 2);
+ $text = ' (' . $text;
+ } else {
+ $date = $data;
+ $text = '';
+ }
+ // Capitals
+ $date = strtoupper($date);
+ // Temporarily add leading/trailing spaces, to allow efficient matching below
+ $date = " {$date} ";
+ // Ensure space digits and letters
+ $date = preg_replace('/([A-Z])(\d)/', '$1 $2', $date);
+ $date = preg_replace('/(\d)([A-Z])/', '$1 $2', $date);
+ // Ensure space before/after calendar escapes
+ $date = preg_replace('/@#[^@]+@/', ' $0 ', $date);
+ // "BET." => "BET"
+ $date = preg_replace('/(\w\w)\./', '$1', $date);
+ // "CIR" => "ABT"
+ $date = str_replace(' CIR ', ' ABT ', $date);
+ $date = str_replace(' APX ', ' ABT ', $date);
+ // B.C. => BC (temporarily, to allow easier handling of ".")
+ $date = str_replace(' B.C. ', ' BC ', $date);
+ // "BET X - Y " => "BET X AND Y"
+ $date = preg_replace('/^(.* BET .+) - (.+)/', '$1 AND $2', $date);
+ $date = preg_replace('/^(.* FROM .+) - (.+)/', '$1 TO $2', $date);
+ // "@#ESC@ FROM X TO Y" => "FROM @#ESC@ X TO @#ESC@ Y"
+ $date = preg_replace('/^ +(@#[^@]+@) +FROM +(.+) +TO +(.+)/', ' FROM $1 $2 TO $1 $3', $date);
+ $date = preg_replace('/^ +(@#[^@]+@) +BET +(.+) +AND +(.+)/', ' BET $1 $2 AND $1 $3', $date);
+ // "@#ESC@ AFT X" => "AFT @#ESC@ X"
+ $date = preg_replace('/^ +(@#[^@]+@) +(FROM|BET|TO|AND|BEF|AFT|CAL|EST|INT|ABT) +(.+)/', ' $2 $1 $3', $date);
+ // Ignore any remaining punctuation, e.g. "14-MAY, 1900" => "14 MAY 1900"
+ // (don't change "/" - it is used in NS/OS dates)
+ $date = preg_replace('/[.,:;-]/', ' ', $date);
+ // BC => B.C.
+ $date = str_replace(' BC ', ' B.C. ', $date);
+ // Append the "INT" text
+ $data = $date . $text;
+ break;
+ case 'DEATH':
+ $tag = 'DEAT';
+ break;
+ case '_DEATH_OF_SPOUSE':
+ $tag = '_DETS';
+ break;
+ case '_DEGREE':
+ $tag = '_DEG';
+ break;
+ case 'DESCENDANTS':
+ $tag = 'DESC';
+ break;
+ case 'DESCENDANT_INT':
+ $tag = 'DESI';
+ break;
+ case 'DESTINATION':
+ $tag = 'DEST';
+ break;
+ case 'DIVORCE':
+ $tag = 'DIV';
+ break;
+ case 'DIVORCE_FILED':
+ $tag = 'DIVF';
+ break;
+ case 'EDUCATION':
+ $tag = 'EDUC';
+ break;
+ case 'EMIGRATION':
+ $tag = 'EMIG';
+ break;
+ case 'ENDOWMENT':
+ $tag = 'ENDL';
+ break;
+ case 'ENGAGEMENT':
+ $tag = 'ENGA';
+ break;
+ case 'EVENT':
+ $tag = 'EVEN';
+ break;
+ case 'FACSIMILE':
+ $tag = 'FAX';
+ break;
+ case 'FAMILY':
+ $tag = 'FAM';
+ break;
+ case 'FAMILY_CHILD':
+ $tag = 'FAMC';
+ break;
+ case 'FAMILY_FILE':
+ $tag = 'FAMF';
+ break;
+ case 'FAMILY_SPOUSE':
+ $tag = 'FAMS';
+ break;
+ case 'FIRST_COMMUNION':
+ $tag = 'FCOM';
+ break;
+ case '_FILE':
+ $tag = 'FILE';
+ break;
+ case 'FORMAT':
+ $tag = 'FORM';
+ case 'FORM':
+ // Consistent commas
+ $data = preg_replace('/ *, */', ', ', $data);
+ break;
+ case 'GEDCOM':
+ $tag = 'GEDC';
+ break;
+ case 'GIVEN_NAME':
+ $tag = 'GIVN';
+ break;
+ case 'GRADUATION':
+ $tag = 'GRAD';
+ break;
+ case 'HEADER':
+ $tag = 'HEAD';
+ case 'HEAD':
+ // HEAD records don't have an XREF or DATA
+ if ($level == '0') {
+ $xref = '';
+ $data = '';
+ }
+ break;
+ case 'HUSBAND':
+ $tag = 'HUSB';
+ break;
+ case 'IDENT_NUMBER':
+ $tag = 'IDNO';
+ break;
+ case 'IMMIGRATION':
+ $tag = 'IMMI';
+ break;
+ case 'INDIVIDUAL':
+ $tag = 'INDI';
+ break;
+ case 'LANGUAGE':
+ $tag = 'LANG';
+ break;
+ case 'LATITUDE':
+ $tag = 'LATI';
+ break;
+ case 'LONGITUDE':
+ $tag = 'LONG';
+ break;
+ case 'MARRIAGE':
+ $tag = 'MARR';
+ break;
+ case 'MARRIAGE_BANN':
+ $tag = 'MARB';
+ break;
+ case 'MARRIAGE_COUNT':
+ $tag = 'NMR';
+ break;
+ case 'MARRIAGE_CONTRACT':
+ $tag = 'MARC';
+ break;
+ case 'MARRIAGE_LICENSE':
+ $tag = 'MARL';
+ break;
+ case 'MARRIAGE_SETTLEMENT':
+ $tag = 'MARS';
+ break;
+ case 'MEDIA':
+ $tag = 'MEDI';
+ break;
+ case '_MEDICAL':
+ $tag = '_MDCL';
+ break;
+ case '_MILITARY_SERVICE':
+ $tag = '_MILT';
+ break;
+ case 'NAME':
+ // Tidy up whitespace
+ $data = preg_replace('/ +/', ' ', trim($data));
+ break;
+ case 'NAME_PREFIX':
+ $tag = 'NPFX';
+ break;
+ case 'NAME_SUFFIX':
+ $tag = 'NSFX';
+ break;
+ case 'NATIONALITY':
+ $tag = 'NATI';
+ break;
+ case 'NATURALIZATION':
+ $tag = 'NATU';
+ break;
+ case 'NICKNAME':
+ $tag = 'NICK';
+ break;
+ case 'OBJECT':
+ $tag = 'OBJE';
+ break;
+ case 'OCCUPATION':
+ $tag = 'OCCU';
+ break;
+ case 'ORDINANCE':
+ $tag = 'ORDI';
+ break;
+ case 'ORDINATION':
+ $tag = 'ORDN';
+ break;
+ case 'PEDIGREE':
+ $tag = 'PEDI';
+ case 'PEDI':
+ // PEDI values are lower case
+ $data = strtolower($data);
+ break;
+ case 'PHONE':
+ $tag = 'PHON';
+ break;
+ case 'PHONETIC':
+ $tag = 'FONE';
+ break;
+ case 'PHY_DESCRIPTION':
+ $tag = 'DSCR';
+ break;
+ case 'PLACE':
+ $tag = 'PLAC';
+ case 'PLAC':
+ // Consistent commas
+ $data = preg_replace('/ *(،|,) */', ', ', $data);
+ // The Master Genealogist stores LAT/LONG data in the PLAC field, e.g. Pennsylvania, USA, 395945N0751013W
+ if (preg_match('/(.*), (\d\d)(\d\d)(\d\d)([NS])(\d\d\d)(\d\d)(\d\d)([EW])$/', $data, $match)) {
+ $data =
+ $match[1] . "\n" .
+ ($level + 1) . " MAP\n" .
+ ($level + 2) . " LATI " . ($match[5] . (round($match[2] + ($match[3] / 60) + ($match[4] / 3600), 4))) . "\n" .
+ ($level + 2) . " LONG " . ($match[9] . (round($match[6] + ($match[7] / 60) + ($match[8] / 3600), 4)));
+ }
+ break;
+ case 'POSTAL_CODE':
+ $tag = 'POST';
+ break;
+ case 'PROBATE':
+ $tag = 'PROB';
+ break;
+ case 'PROPERTY':
+ $tag = 'PROP';
+ break;
+ case 'PUBLICATION':
+ $tag = 'PUBL';
+ break;
+ case 'QUALITY_OF_DATA':
+ $tag = 'QUAL';
+ break;
+ case 'REC_FILE_NUMBER':
+ $tag = 'RFN';
+ break;
+ case 'REC_ID_NUMBER':
+ $tag = 'RIN';
+ break;
+ case 'REFERENCE':
+ $tag = 'REFN';
+ break;
+ case 'RELATIONSHIP':
+ $tag = 'RELA';
+ break;
+ case 'RELIGION':
+ $tag = 'RELI';
+ break;
+ case 'REPOSITORY':
+ $tag = 'REPO';
+ break;
+ case 'RESIDENCE':
+ $tag = 'RESI';
+ break;
+ case 'RESTRICTION':
+ $tag = 'RESN';
+ case 'RESN':
+ // RESN values are lower case (confidential, privacy, locked, none)
+ $data = strtolower($data);
+ if ($data == 'invisible') {
+ $data = 'confidential'; // From old versions of Legacy.
+ }
+ break;
+ case 'RETIREMENT':
+ $tag = 'RETI';
+ break;
+ case 'ROMANIZED':
+ $tag = 'ROMN';
+ break;
+ case 'SEALING_CHILD':
+ $tag = 'SLGC';
+ break;
+ case 'SEALING_SPOUSE':
+ $tag = 'SLGS';
+ break;
+ case 'SOC_SEC_NUMBER':
+ $tag = 'SSN';
+ break;
+ case 'SEX':
+ $data = strtoupper($data);
+ break;
+ case 'SOURCE':
+ $tag = 'SOUR';
+ break;
+ case 'STATE':
+ $tag = 'STAE';
+ break;
+ case 'STATUS':
+ $tag = 'STAT';
+ case 'STAT':
+ if ($data == 'CANCELLED') {
+ // PhpGedView mis-spells this tag - correct it.
+ $data = 'CANCELED';
+ }
+ break;
+ case 'SUBMISSION':
+ $tag = 'SUBN';
+ break;
+ case 'SUBMITTER':
+ $tag = 'SUBM';
+ break;
+ case 'SURNAME':
+ $tag = 'SURN';
+ break;
+ case 'SURN_PREFIX':
+ $tag = 'SPFX';
+ break;
+ case 'TEMPLE':
+ $tag = 'TEMP';
+ case 'TEMP':
+ // Temple codes are upper case
+ $data = strtoupper($data);
+ break;
+ case 'TITLE':
+ $tag = 'TITL';
+ break;
+ case 'TRAILER':
+ $tag = 'TRLR';
+ case 'TRLR':
+ // TRLR records don't have an XREF or DATA
+ if ($level == '0') {
+ $xref = '';
+ $data = '';
+ }
+ break;
+ case 'VERSION':
+ $tag = 'VERS';
+ break;
+ case 'WEB':
+ $tag = 'WWW';
+ break;
+ }
+ // Suppress "Y", for facts/events with a DATE or PLAC
+ if ($data == 'y') {
+ $data = 'Y';
+ }
+ if ($level == '1' && $data == 'Y') {
+ for ($i = $n + 1; $i < $num_matches - 1 && $matches[$i][1] != '1'; ++$i) {
+ if ($matches[$i][3] == 'DATE' || $matches[$i][3] == 'PLAC') {
+ $data = '';
+ break;
+ }
+ }
+ }
+ // Reassemble components back into a single line
+ switch ($tag) {
+ default:
+ // Remove tabs and multiple/leading/trailing spaces
+ if (strpos($data, "\t") !== false) {
+ $data = str_replace("\t", ' ', $data);
+ }
+ if (substr($data, 0, 1) == ' ' || substr($data, -1, 1) == ' ') {
+ $data = trim($data);
+ }
+ while (strpos($data, ' ')) {
+ $data = str_replace(' ', ' ', $data);
+ }
+ $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level == '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag != "NOTE" ? '' : ' ' . $data);
+ break;
+ case 'NOTE':
+ case 'TEXT':
+ case 'DATA':
+ case 'CONT':
+ $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level == '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag != "NOTE" ? '' : ' ' . $data);
+ break;
+ case 'FILE':
+ // Strip off the user-defined path prefix
+ $GEDCOM_MEDIA_PATH = $tree->getPreference('GEDCOM_MEDIA_PATH');
+ if ($GEDCOM_MEDIA_PATH && strpos($data, $GEDCOM_MEDIA_PATH) === 0) {
+ $data = substr($data, strlen($GEDCOM_MEDIA_PATH));
+ }
+ // convert backslashes in filenames to forward slashes
+ $data = preg_replace("/\\\/", "/", $data);
+
+ $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level == '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag != "NOTE" ? '' : ' ' . $data);
+ break;
+ case 'CONC':
+ // Merge CONC lines, to simplify access later on.
+ $newrec .= ($tree->getPreference('WORD_WRAPPED_NOTES') ? ' ' : '') . $data;
+ break;
+ }
+ }
+
+ return $newrec;
+ }
+
+ /**
+ * import record into database
+ *
+ * this function will parse the given gedcom record and add it to the database
+ *
+ * @param string $gedrec the raw gedcom record to parse
+ * @param Tree $tree import the record into this tree
+ * @param bool $update whether or not this is an updated record that has been accepted
+ */
+ public static function importRecord($gedrec, Tree $tree, $update) {
+ $tree_id = $tree->getTreeId();
+
+ // Escaped @ signs (only if importing from file)
+ if (!$update) {
+ $gedrec = str_replace('@@', '@', $gedrec);
+ }
+
+ // Standardise gedcom format
+ $gedrec = self::reformatRecord($gedrec, $tree);
+
+ // import different types of records
+ if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ (' . WT_REGEX_TAG . ')/', $gedrec, $match)) {
+ list(, $xref, $type) = $match;
+ // check for a _UID, if the record doesn't have one, add one
+ if ($tree->getPreference('GENERATE_UIDS') && !strpos($gedrec, "\n1 _UID ")) {
+ $gedrec .= "\n1 _UID " . GedcomTag::createUid();
+ }
+ } elseif (preg_match('/0 (HEAD|TRLR)/', $gedrec, $match)) {
+ $type = $match[1];
+ $xref = $type; // For HEAD/TRLR, use type as pseudo XREF.
+ } else {
+ echo I18N::translate('Invalid GEDCOM format'), '<br><pre>', $gedrec, '</pre>';
+
+ return;
+ }
+
+ // If the user has downloaded their GEDCOM data (containing media objects) and edited it
+ // using an application which does not support (and deletes) media objects, then add them
+ // back in.
+ if ($tree->getPreference('keep_media') && $xref) {
+ $old_linked_media =
+ Database::prepare("SELECT l_to FROM `##link` WHERE l_from=? AND l_file=? AND l_type='OBJE'")
+ ->execute(array($xref, $tree_id))
+ ->fetchOneColumn();
+ foreach ($old_linked_media as $media_id) {
+ $gedrec .= "\n1 OBJE @" . $media_id . "@";
+ }
+ }
+
+ switch ($type) {
+ case 'INDI':
+ // Convert inline media into media objects
+ $gedrec = self::convertInlineMedia($tree, $gedrec);
+
+ $record = new Individual($xref, $gedrec, null, $tree);
+ if ($tree->getPreference('USE_RIN') && preg_match('/\n1 RIN (.+)/', $gedrec, $match)) {
+ $rin = $match[1];
+ } else {
+ $rin = $xref;
+ }
+ Database::prepare(
+ "INSERT INTO `##individuals` (i_id, i_file, i_rin, i_sex, i_gedcom) VALUES (?, ?, ?, ?, ?)"
+ )->execute(array(
+ $xref, $tree_id, $rin, $record->getSex(), $gedrec,
+ ));
+ // Update the cross-reference/index tables.
+ self::updatePlaces($xref, $tree_id, $gedrec);
+ self::updateDates($xref, $tree_id, $gedrec);
+ self::updateLinks($xref, $tree_id, $gedrec);
+ self::updateNames($xref, $tree_id, $record);
+ break;
+ case 'FAM':
+ // Convert inline media into media objects
+ $gedrec = self::convertInlineMedia($tree, $gedrec);
+
+ if (preg_match('/\n1 HUSB @(' . WT_REGEX_XREF . ')@/', $gedrec, $match)) {
+ $husb = $match[1];
+ } else {
+ $husb = '';
+ }
+ if (preg_match('/\n1 WIFE @(' . WT_REGEX_XREF . ')@/', $gedrec, $match)) {
+ $wife = $match[1];
+ } else {
+ $wife = '';
+ }
+ $nchi = preg_match_all('/\n1 CHIL @(' . WT_REGEX_XREF . ')@/', $gedrec, $match);
+ if (preg_match('/\n1 NCHI (\d+)/', $gedrec, $match)) {
+ $nchi = max($nchi, $match[1]);
+ }
+ Database::prepare(
+ "INSERT INTO `##families` (f_id, f_file, f_husb, f_wife, f_gedcom, f_numchil) VALUES (?, ?, ?, ?, ?, ?)"
+ )->execute(array(
+ $xref, $tree_id, $husb, $wife, $gedrec, $nchi,
+ ));
+ // Update the cross-reference/index tables.
+ self::updatePlaces($xref, $tree_id, $gedrec);
+ self::updateDates($xref, $tree_id, $gedrec);
+ self::updateLinks($xref, $tree_id, $gedrec);
+ break;
+ case 'SOUR':
+ // Convert inline media into media objects
+ $gedrec = self::convertInlineMedia($tree, $gedrec);
+
+ $record = new Source($xref, $gedrec, null, $tree);
+ if (preg_match('/\n1 TITL (.+)/', $gedrec, $match)) {
+ $name = $match[1];
+ } elseif (preg_match('/\n1 ABBR (.+)/', $gedrec, $match)) {
+ $name = $match[1];
+ } else {
+ $name = $xref;
+ }
+ Database::prepare(
+ "INSERT INTO `##sources` (s_id, s_file, s_name, s_gedcom) VALUES (?, ?, LEFT(?, 255), ?)"
+ )->execute(array(
+ $xref, $tree_id, $name, $gedrec,
+ ));
+ // Update the cross-reference/index tables.
+ self::updateLinks($xref, $tree_id, $gedrec);
+ self::updateNames($xref, $tree_id, $record);
+ break;
+ case 'REPO':
+ // Convert inline media into media objects
+ $gedrec = self::convertInlineMedia($tree, $gedrec);
+
+ $record = new Repository($xref, $gedrec, null, $tree);
+ Database::prepare(
+ "INSERT INTO `##other` (o_id, o_file, o_type, o_gedcom) VALUES (?, ?, 'REPO', ?)"
+ )->execute(array(
+ $xref, $tree_id, $gedrec,
+ ));
+ // Update the cross-reference/index tables.
+ self::updateLinks($xref, $tree_id, $gedrec);
+ self::updateNames($xref, $tree_id, $record);
+ break;
+ case 'NOTE':
+ $record = new Note($xref, $gedrec, null, $tree);
+ Database::prepare(
+ "INSERT INTO `##other` (o_id, o_file, o_type, o_gedcom) VALUES (?, ?, 'NOTE', ?)"
+ )->execute(array(
+ $xref, $tree_id, $gedrec,
+ ));
+ // Update the cross-reference/index tables.
+ self::updateLinks($xref, $tree_id, $gedrec);
+ self::updateNames($xref, $tree_id, $record);
+ break;
+ case 'OBJE':
+ $record = new Media($xref, $gedrec, null, $tree);
+ Database::prepare(
+ "INSERT INTO `##media` (m_id, m_ext, m_type, m_titl, m_filename, m_file, m_gedcom) VALUES (?, ?, ?, ?, ?, ?, ?)"
+ )->execute(array(
+ $xref, $record->extension(), $record->getMediaType(), $record->getTitle(), $record->getFilename(), $tree_id, $gedrec,
+ ));
+ // Update the cross-reference/index tables.
+ self::updateLinks($xref, $tree_id, $gedrec);
+ self::updateNames($xref, $tree_id, $record);
+ break;
+ case 'HEAD':
+ // Force HEAD records to have a creation date.
+ if (!strpos($gedrec, "\n1 DATE ")) {
+ $gedrec .= "\n1 DATE " . date('j M Y');
+ }
+ // No break;
+ case 'TRLR':
+ case 'SUBM':
+ case 'SUBN':
+ Database::prepare(
+ "INSERT INTO `##other` (o_id, o_file, o_type, o_gedcom) VALUES (?, ?, ?, ?)"
+ )->execute(array($xref, $tree_id, $type, $gedrec));
+ // Update the cross-reference/index tables.
+ self::updateLinks($xref, $tree_id, $gedrec);
+ break;
+ default:
+ $record = new GedcomRecord($xref, $gedrec, null, $tree);
+ Database::prepare(
+ "INSERT INTO `##other` (o_id, o_file, o_type, o_gedcom) VALUES (?, ?, ?, ?)"
+ )->execute(array($xref, $tree_id, $type, $gedrec));
+ // Update the cross-reference/index tables.
+ self::updateLinks($xref, $tree_id, $gedrec);
+ self::updateNames($xref, $tree_id, $record);
+ break;
+ }
+ }
+
+ /**
+ * extract all places from the given record and insert them into the places table
+ *
+ * @param string $gid
+ * @param int $ged_id
+ * @param string $gedrec
+ */
+ public static function updatePlaces($gid, $ged_id, $gedrec) {
+ global $placecache;
+
+ if (!isset($placecache)) {
+ $placecache = array();
+ }
+ $personplace = array();
+ // import all place locations, but not control info such as
+ // 0 HEAD/1 PLAC or 0 _EVDEF/1 PLAC
+ $pt = preg_match_all("/^[2-9] PLAC (.+)/m", $gedrec, $match, PREG_SET_ORDER);
+ for ($i = 0; $i < $pt; $i++) {
+ $place = trim($match[$i][1]);
+ $lowplace = I18N::strtolower($place);
+ //-- if we have already visited this place for this person then we don't need to again
+ if (isset($personplace[$lowplace])) {
+ continue;
+ }
+ $personplace[$lowplace] = 1;
+ $places = explode(',', $place);
+ //-- reverse the array to start at the highest level
+ $secalp = array_reverse($places);
+ $parent_id = 0;
+ $search = true;
+
+ foreach ($secalp as $place) {
+ $place = trim($place);
+ $key = strtolower(mb_substr($place, 0, 150) . "_" . $parent_id);
+ //-- if this place has already been added then we don't need to add it again
+ if (isset($placecache[$key])) {
+ $parent_id = $placecache[$key];
+ if (!isset($personplace[$key])) {
+ $personplace[$key] = 1;
+ // Use INSERT IGNORE as a (temporary) fix for https://bugs.launchpad.net/webtrees/+bug/582226
+ // It ignores places that utf8_unicode_ci consider to be the same (i.e. accents).
+ // For example Québec and Quebec
+ // We need a better solution that attaches multiple names to single places
+ Database::prepare(
+ "INSERT IGNORE INTO `##placelinks` (pl_p_id, pl_gid, pl_file) VALUES (?, ?, ?)"
+ )->execute(array(
+ $parent_id, $gid, $ged_id,
+ ));
+ }
+ continue;
+ }
+
+ //-- only search the database while we are finding places in it
+ if ($search) {
+ //-- check if this place and level has already been added
+ $tmp = Database::prepare(
+ "SELECT p_id FROM `##places` WHERE p_file = ? AND p_parent_id = ? AND p_place = LEFT(?, 150)"
+ )->execute(array(
+ $ged_id, $parent_id, $place,
+ ))->fetchOne();
+ if ($tmp) {
+ $p_id = $tmp;
+ } else {
+ $search = false;
+ }
+ }
+
+ //-- if we are not searching then we have to insert the place into the db
+ if (!$search) {
+ $std_soundex = Soundex::russell($place);
+ $dm_soundex = Soundex::daitchMokotoff($place);
+ Database::prepare(
+ "INSERT INTO `##places` (p_place, p_parent_id, p_file, p_std_soundex, p_dm_soundex) VALUES (LEFT(?, 150), ?, ?, ?, ?)"
+ )->execute(array(
+ $place, $parent_id, $ged_id, $std_soundex, $dm_soundex,
+ ));
+ $p_id = Database::getInstance()->lastInsertId();
+ }
+
+ Database::prepare(
+ "INSERT IGNORE INTO `##placelinks` (pl_p_id, pl_gid, pl_file) VALUES (?, ?, ?)"
+ )->execute(array(
+ $p_id, $gid, $ged_id,
+ ));
+ //-- increment the level and assign the parent id for the next place level
+ $parent_id = $p_id;
+ $placecache[$key] = $p_id;
+ $personplace[$key] = 1;
+ }
+ }
+ }
+
+ /**
+ * Extract all the dates from the given record and insert them into the database.
+ *
+ * @param string $xref
+ * @param int $ged_id
+ * @param string $gedrec
+ */
+ public static function updateDates($xref, $ged_id, $gedrec) {
+ if (strpos($gedrec, '2 DATE ') && preg_match_all("/\n1 (\w+).*(?:\n[2-9].*)*(?:\n2 DATE (.+))(?:\n[2-9].*)*/", $gedrec, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $fact = $match[1];
+ if (($fact == 'FACT' || $fact == 'EVEN') && preg_match("/\n2 TYPE ([A-Z]{3,5})/", $match[0], $tmatch)) {
+ $fact = $tmatch[1];
+ }
+ $date = new Date($match[2]);
+ Database::prepare(
+ "INSERT INTO `##dates` (d_day,d_month,d_mon,d_year,d_julianday1,d_julianday2,d_fact,d_gid,d_file,d_type) VALUES (?,?,?,?,?,?,?,?,?,?)"
+ )->execute(array(
+ $date->minimumDate()->d,
+ $date->minimumDate()->format('%O'),
+ $date->minimumDate()->m,
+ $date->minimumDate()->y,
+ $date->minimumDate()->minJD,
+ $date->minimumDate()->maxJD,
+ $fact,
+ $xref,
+ $ged_id,
+ $date->minimumDate()->format('%@'),
+ ));
+ if ($date->minimumDate() !== $date->maximumDate()) {
+ Database::prepare(
+ "INSERT INTO `##dates` (d_day,d_month,d_mon,d_year,d_julianday1,d_julianday2,d_fact,d_gid,d_file,d_type) VALUES (?,?,?,?,?,?,?,?,?,?)"
+ )->execute(array(
+ $date->maximumDate()->d,
+ $date->maximumDate()->format('%O'),
+ $date->maximumDate()->m,
+ $date->maximumDate()->y,
+ $date->maximumDate()->minJD,
+ $date->maximumDate()->maxJD,
+ $fact,
+ $xref,
+ $ged_id,
+ $date->maximumDate()->format('%@'),
+ ));
+ }
+ }
+ }
+ }
+
+ /**
+ * Extract all the links from the given record and insert them into the database
+ *
+ * @param string $xref
+ * @param int $ged_id
+ * @param string $gedrec
+ */
+ public static function updateLinks($xref, $ged_id, $gedrec) {
+ if (preg_match_all('/^\d+ (' . WT_REGEX_TAG . ') @(' . WT_REGEX_XREF . ')@/m', $gedrec, $matches, PREG_SET_ORDER)) {
+ $data = array();
+ foreach ($matches as $match) {
+ // Include each link once only.
+ if (!in_array($match[1] . $match[2], $data)) {
+ $data[] = $match[1] . $match[2];
+ // Ignore any errors, which may be caused by "duplicates" that differ on case/collation, e.g. "S1" and "s1"
+ try {
+ Database::prepare(
+ "INSERT INTO `##link` (l_from, l_to, l_type, l_file) VALUES (?, ?, ?, ?)"
+ )->execute(array(
+ $xref, $match[2], $match[1], $ged_id,
+ ));
+ } catch (PDOException $e) {
+ // We could display a warning here....
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Extract all the names from the given record and insert them into the database.
+ *
+ * @param string $xref
+ * @param int $ged_id
+ * @param GedcomRecord $record
+ */
+ public static function updateNames($xref, $ged_id, GedcomRecord $record) {
+ foreach ($record->getAllNames() as $n => $name) {
+ if ($record instanceof Individual) {
+ if ($name['givn'] === '@P.N.') {
+ $soundex_givn_std = null;
+ $soundex_givn_dm = null;
+ } else {
+ $soundex_givn_std = Soundex::russell($name['givn']);
+ $soundex_givn_dm = Soundex::daitchMokotoff($name['givn']);
+ }
+ if ($name['surn'] === '@N.N.') {
+ $soundex_surn_std = null;
+ $soundex_surn_dm = null;
+ } else {
+ $soundex_surn_std = Soundex::russell($name['surname']);
+ $soundex_surn_dm = Soundex::daitchMokotoff($name['surname']);
+ }
+ Database::prepare(
+ "INSERT INTO `##name` (n_file,n_id,n_num,n_type,n_sort,n_full,n_surname,n_surn,n_givn,n_soundex_givn_std,n_soundex_surn_std,n_soundex_givn_dm,n_soundex_surn_dm) VALUES (?, ?, ?, ?, LEFT(?, 255), LEFT(?, 255), LEFT(?, 255), LEFT(?, 255), ?, ?, ?, ?, ?)"
+ )->execute(array(
+ $ged_id, $xref, $n, $name['type'], $name['sort'], $name['fullNN'], $name['surname'], $name['surn'], $name['givn'], $soundex_givn_std, $soundex_surn_std, $soundex_givn_dm, $soundex_surn_dm,
+ ));
+ } else {
+ Database::prepare(
+ "INSERT INTO `##name` (n_file,n_id,n_num,n_type,n_sort,n_full) VALUES (?, ?, ?, ?, LEFT(?, 255), LEFT(?, 255))"
+ )->execute(array(
+ $ged_id, $xref, $n, $name['type'], $name['sort'], $name['fullNN'],
+ ));
+ }
+ }
+ }
+
+ /**
+ * Extract inline media data, and convert to media objects.
+ *
+ * @param Tree $tree
+ * @param string $gedrec
+ *
+ * @return string
+ */
+ public static function convertInlineMedia(Tree $tree, $gedrec) {
+ while (preg_match('/\n1 OBJE(?:\n[2-9].+)+/', $gedrec, $match)) {
+ $gedrec = str_replace($match[0], self::createMediaObject(1, $match[0], $tree), $gedrec);
+ }
+ while (preg_match('/\n2 OBJE(?:\n[3-9].+)+/', $gedrec, $match)) {
+ $gedrec = str_replace($match[0], self::createMediaObject(2, $match[0], $tree), $gedrec);
+ }
+ while (preg_match('/\n3 OBJE(?:\n[4-9].+)+/', $gedrec, $match)) {
+ $gedrec = str_replace($match[0], self::createMediaObject(3, $match[0], $tree), $gedrec);
+ }
+
+ return $gedrec;
+ }
+
+ /**
+ * Create a new media object, from inline media data.
+ *
+ * @param int $level
+ * @param string $gedrec
+ * @param Tree $tree
+ *
+ * @return string
+ */
+ public static function createMediaObject($level, $gedrec, Tree $tree) {
+ if (preg_match('/\n\d FILE (.+)/', $gedrec, $file_match)) {
+ $file = $file_match[1];
+ } else {
+ $file = '';
+ }
+
+ if (preg_match('/\n\d TITL (.+)/', $gedrec, $file_match)) {
+ $titl = $file_match[1];
+ } else {
+ $titl = $file;
+ }
+
+ // Have we already created a media object with the same title/filename?
+ $xref = Database::prepare(
+ "SELECT m_id FROM `##media` WHERE m_filename = ? AND m_titl = ? AND m_file = ?"
+ )->execute(array(
+ $file, $titl, $tree->getTreeId(),
+ ))->fetchOne();
+
+ if (!$xref) {
+ $xref = $tree->getNewXref('OBJE');
+ // renumber the lines
+ $gedrec = preg_replace_callback('/\n(\d+)/', function ($m) use ($level) {
+ return "\n" . ($m[1] - $level);
+ }, $gedrec);
+ // convert to an object
+ $gedrec = str_replace("\n0 OBJE\n", '0 @' . $xref . "@ OBJE\n", $gedrec);
+ // Fix Legacy GEDCOMS
+ $gedrec = preg_replace('/\n1 FORM (.+)\n1 FILE (.+)\n1 TITL (.+)/', "\n1 FILE $2\n2 FORM $1\n2 TITL $3", $gedrec);
+ // Create new record
+ $record = new Media($xref, $gedrec, null, $tree);
+ Database::prepare(
+ "INSERT INTO `##media` (m_id, m_ext, m_type, m_titl, m_filename, m_file, m_gedcom) VALUES (?, ?, ?, ?, ?, ?, ?)"
+ )->execute(array(
+ $xref, $record->extension(), $record->getMediaType(), $record->getTitle(), $record->getFilename(), $tree->getTreeId(), $gedrec,
+ ));
+ }
+
+ return "\n" . $level . ' OBJE @' . $xref . '@';
+ }
+
+ /**
+ * Accept all pending changes for a specified record.
+ *
+ * @param string $xref
+ * @param int $ged_id
+ */
+ public static function acceptAllChanges($xref, $ged_id) {
+ $changes = Database::prepare(
+ "SELECT change_id, gedcom_name, old_gedcom, new_gedcom" .
+ " FROM `##change` c" .
+ " JOIN `##gedcom` g USING (gedcom_id)" .
+ " WHERE c.status='pending' AND xref=? AND gedcom_id=?" .
+ " ORDER BY change_id"
+ )->execute(array($xref, $ged_id))->fetchAll();
+ foreach ($changes as $change) {
+ if (empty($change->new_gedcom)) {
+ // delete
+ self::updateRecord($change->old_gedcom, $ged_id, true);
+ } else {
+ // add/update
+ self::updateRecord($change->new_gedcom, $ged_id, false);
+ }
+ Database::prepare(
+ "UPDATE `##change`" .
+ " SET status='accepted'" .
+ " WHERE status='pending' AND xref=? AND gedcom_id=?"
+ )->execute(array($xref, $ged_id));
+ Log::addEditLog("Accepted change {$change->change_id} for {$xref} / {$change->gedcom_name} into database");
+ }
+ }
+
+ /**
+ * Accept all pending changes for a specified record.
+ *
+ * @param GedcomRecord $record
+ */
+ public static function rejectAllChanges(GedcomRecord $record) {
+ Database::prepare(
+ "UPDATE `##change`" .
+ " SET status = 'rejected'" .
+ " WHERE status = 'pending' AND xref = :xref AND gedcom_id = :tree_id"
+ )->execute(array(
+ 'xref' => $record->getXref(),
+ 'tree_id' => $record->getTree()->getTreeId(),
+ ));
+ }
+
+ /**
+ * update a record in the database
+ *
+ * @param string $gedrec
+ * @param int $ged_id
+ * @param bool $delete
+ */
+ public static function updateRecord($gedrec, $ged_id, $delete) {
+ if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ (' . WT_REGEX_TAG . ')/', $gedrec, $match)) {
+ list(, $gid, $type) = $match;
+ } else {
+ echo "ERROR: Invalid gedcom record.";
+
+ return;
+ }
+
+ // TODO deleting unlinked places can be done more efficiently in a single query
+ $placeids =
+ Database::prepare("SELECT pl_p_id FROM `##placelinks` WHERE pl_gid=? AND pl_file=?")
+ ->execute(array($gid, $ged_id))
+ ->fetchOneColumn();
+
+ Database::prepare("DELETE FROM `##placelinks` WHERE pl_gid=? AND pl_file=?")->execute(array($gid, $ged_id));
+ Database::prepare("DELETE FROM `##dates` WHERE d_gid =? AND d_file =?")->execute(array($gid, $ged_id));
+
+ //-- delete any unlinked places
+ foreach ($placeids as $p_id) {
+ $num =
+ Database::prepare("SELECT count(pl_p_id) FROM `##placelinks` WHERE pl_p_id=? AND pl_file=?")
+ ->execute(array($p_id, $ged_id))
+ ->fetchOne();
+ if ($num == 0) {
+ Database::prepare("DELETE FROM `##places` WHERE p_id=? AND p_file=?")->execute(array($p_id, $ged_id));
+ }
+ }
+
+ Database::prepare("DELETE FROM `##name` WHERE n_id=? AND n_file=?")->execute(array($gid, $ged_id));
+ Database::prepare("DELETE FROM `##link` WHERE l_from=? AND l_file=?")->execute(array($gid, $ged_id));
+
+ switch ($type) {
+ case 'INDI':
+ Database::prepare("DELETE FROM `##individuals` WHERE i_id=? AND i_file=?")->execute(array($gid, $ged_id));
+ break;
+ case 'FAM':
+ Database::prepare("DELETE FROM `##families` WHERE f_id=? AND f_file=?")->execute(array($gid, $ged_id));
+ break;
+ case 'SOUR':
+ Database::prepare("DELETE FROM `##sources` WHERE s_id=? AND s_file=?")->execute(array($gid, $ged_id));
+ break;
+ case 'OBJE':
+ Database::prepare("DELETE FROM `##media` WHERE m_id=? AND m_file=?")->execute(array($gid, $ged_id));
+ break;
+ default:
+ Database::prepare("DELETE FROM `##other` WHERE o_id=? AND o_file=?")->execute(array($gid, $ged_id));
+ break;
+ }
+
+ if (!$delete) {
+ self::importRecord($gedrec, Tree::FindById($ged_id), true);
+ }
+ }
+}
diff --git a/app/Functions/FunctionsMedia.php b/app/Functions/FunctionsMedia.php
new file mode 100644
index 0000000000..0b06852a61
--- /dev/null
+++ b/app/Functions/FunctionsMedia.php
@@ -0,0 +1,97 @@
+<?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\Log;
+
+/**
+ * Class FunctionsMedia - common functions
+ */
+class FunctionsMedia {
+ /**
+ * Convert raw values from php.ini file into bytes
+ *
+ * @param string $val
+ *
+ * @return int
+ */
+ public static function sizeToBytes($val) {
+ if (!$val) {
+ // no value was passed in, assume no limit and return -1
+ $val = -1;
+ }
+ switch (substr($val, -1)) {
+ case 'g':
+ case 'G':
+ return (int) $val * 1024 * 1024 * 1024;
+ case 'm':
+ case 'M':
+ return (int) $val * 1024 * 1024;
+ case 'k':
+ case 'K':
+ return (int) $val * 1024;
+ default:
+ return (int) $val;
+ }
+ }
+
+ /**
+ * Determine whether there is enough memory to load a particular image.
+ *
+ * @param string $serverFilename
+ *
+ * @return bool
+ */
+ public static function hasMemoryForImage($serverFilename) {
+ // find out how much total memory this script can access
+ $memoryAvailable = self::sizeToBytes(ini_get('memory_limit'));
+ // if memory is unlimited, it will return -1 and we don’t need to worry about it
+ if ($memoryAvailable == -1) {
+ return true;
+ }
+
+ // find out how much memory we are already using
+ $memoryUsed = memory_get_usage();
+
+ try {
+ $imgsize = getimagesize($serverFilename);
+ } catch (\ErrorException $ex) {
+ // Not an image, or not a valid image?
+ $imgsize = false;
+ }
+
+ // find out how much memory this image needs for processing, probably only works for jpegs
+ // from comments on http://www.php.net/imagecreatefromjpeg
+ if ($imgsize && isset($imgsize['bits']) && (isset($imgsize['channels']))) {
+ $memoryNeeded = round(($imgsize[0] * $imgsize[1] * $imgsize['bits'] * $imgsize['channels'] / 8 + Pow(2, 16)) * 1.65);
+ $memorySpare = $memoryAvailable - $memoryUsed - $memoryNeeded;
+ if ($memorySpare > 0) {
+ // we have enough memory to load this file
+ return true;
+ } else {
+ // not enough memory to load this file
+ $image_info = sprintf('%.2fKB, %d × %d %d bits %d channels', filesize($serverFilename) / 1024, $imgsize[0], $imgsize[1], $imgsize['bits'], $imgsize['channels']);
+ Log::addMediaLog('Cannot create thumbnail ' . $serverFilename . ' (' . $image_info . ') memory avail: ' . $memoryAvailable . ' used: ' . $memoryUsed . ' needed: ' . $memoryNeeded . ' spare: ' . $memorySpare);
+
+ return false;
+ }
+ } else {
+ // assume there is enough memory
+ // TODO find out how to check memory needs for gif and png
+ return true;
+ }
+ }
+}
diff --git a/app/Functions/FunctionsPrint.php b/app/Functions/FunctionsPrint.php
new file mode 100644
index 0000000000..b926ddabdc
--- /dev/null
+++ b/app/Functions/FunctionsPrint.php
@@ -0,0 +1,873 @@
+<?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\Controller\SearchController;
+use Fisharebest\Webtrees\Date;
+use Fisharebest\Webtrees\Fact;
+use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeRela;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeStat;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeTemp;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\GedcomTag;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Module;
+use Fisharebest\Webtrees\Module\CensusAssistantModule;
+use Fisharebest\Webtrees\Note;
+use Fisharebest\Webtrees\Place;
+use Fisharebest\Webtrees\Session;
+use Fisharebest\Webtrees\Theme;
+use Fisharebest\Webtrees\Tree;
+use Rhumsaa\Uuid\Uuid;
+
+/**
+ * Class FunctionsPrint - common functions
+ */
+class FunctionsPrint {
+ /**
+ * print the information for an individual chart box
+ *
+ * find and print a given individuals information for a pedigree chart
+ *
+ * @param Individual $person The person to print
+ * @param int $show_full The style to print the box in, 0 for smaller boxes, 1 for larger boxes
+ */
+ public static function printPedigreePerson(Individual $person = null, $show_full = 1) {
+
+ switch ($show_full) {
+ case 0:
+ if ($person) {
+ echo Theme::theme()->individualBoxSmall($person);
+ } else {
+ echo Theme::theme()->individualBoxSmallEmpty();
+ }
+ break;
+ case 1:
+ if ($person) {
+ echo Theme::theme()->individualBox($person);
+ } else {
+ echo Theme::theme()->individualBoxEmpty();
+ }
+ break;
+ }
+ }
+
+ /**
+ * print a note record
+ *
+ * @param string $text
+ * @param int $nlevel the level of the note record
+ * @param string $nrec the note record to print
+ * @param bool $textOnly Don't print the "Note: " introduction
+ *
+ * @return string
+ */
+ public static function printNoteRecord($text, $nlevel, $nrec, $textOnly = false) {
+ global $WT_TREE;
+
+ $text .= Functions::getCont($nlevel, $nrec);
+
+ // Check if shared note (we have already checked that it exists)
+ if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ NOTE/', $nrec, $match)) {
+ $note = Note::getInstance($match[1], $WT_TREE);
+ $label = 'SHARED_NOTE';
+ // If Census assistant installed, allow it to format the note
+ if (Module::getModuleByName('GEDFact_assistant')) {
+ $html = CensusAssistantModule::formatCensusNote($note);
+ } else {
+ $html = Filter::formatText($note->getNote(), $WT_TREE);
+ }
+ } else {
+ $note = null;
+ $label = 'NOTE';
+ $html = Filter::formatText($text, $WT_TREE);
+ }
+
+ if ($textOnly) {
+ return strip_tags($text);
+ }
+
+ if (strpos($text, "\n") === false) {
+ // A one-line note? strip the block-level tags, so it displays inline
+ return GedcomTag::getLabelValue($label, strip_tags($html, '<a><strong><em>'));
+ } elseif ($WT_TREE->getPreference('EXPAND_NOTES')) {
+ // A multi-line note, and we're expanding notes by default
+ return GedcomTag::getLabelValue($label, $html);
+ } else {
+ // A multi-line note, with an expand/collapse option
+ $element_id = Uuid::uuid4();
+ // NOTE: class "note-details" is (currently) used only by some third-party themes
+ if ($note) {
+ $first_line = '<a href="' . $note->getHtmlUrl() . '">' . $note->getFullName() . '</a>';
+ } else {
+ list($text) = explode("\n", strip_tags($html));
+ $first_line = strlen($text) > 100 ? mb_substr($text, 0, 100) . I18N::translate('…') : $text;
+ }
+
+ return
+ '<div class="fact_NOTE"><span class="label">' .
+ '<a href="#" onclick="expand_layer(\'' . $element_id . '\'); return false;"><i id="' . $element_id . '_img" class="icon-plus"></i></a> ' . GedcomTag::getLabel($label) . ':</span> ' . '<span id="' . $element_id . '-alt">' . $first_line . '</span>' .
+ '</div>' .
+ '<div class="note-details" id="' . $element_id . '" style="display:none">' . $html . '</div>';
+ }
+ }
+
+ /**
+ * Print all of the notes in this fact record
+ *
+ * @param string $factrec The factrecord to print the notes from
+ * @param int $level The level of the factrecord
+ * @param bool $textOnly Don't print the "Note: " introduction
+ *
+ * @return string HTML
+ */
+ public static function printFactNotes($factrec, $level, $textOnly = false) {
+ global $WT_TREE;
+
+ $data = '';
+ $previous_spos = 0;
+ $nlevel = $level + 1;
+ $ct = preg_match_all("/$level NOTE (.*)/", $factrec, $match, PREG_SET_ORDER);
+ for ($j = 0; $j < $ct; $j++) {
+ $spos1 = strpos($factrec, $match[$j][0], $previous_spos);
+ $spos2 = strpos($factrec . "\n$level", "\n$level", $spos1 + 1);
+ if (!$spos2) {
+ $spos2 = strlen($factrec);
+ }
+ $previous_spos = $spos2;
+ $nrec = substr($factrec, $spos1, $spos2 - $spos1);
+ if (!isset($match[$j][1])) {
+ $match[$j][1] = '';
+ }
+ if (!preg_match('/@(.*)@/', $match[$j][1], $nmatch)) {
+ $data .= self::printNoteRecord($match[$j][1], $nlevel, $nrec, $textOnly);
+ } else {
+ $note = Note::getInstance($nmatch[1], $WT_TREE);
+ if ($note) {
+ if ($note->canShow()) {
+ $noterec = $note->getGedcom();
+ $nt = preg_match("/0 @$nmatch[1]@ NOTE (.*)/", $noterec, $n1match);
+ $data .= self::printNoteRecord(($nt > 0) ? $n1match[1] : "", 1, $noterec, $textOnly);
+ if (!$textOnly) {
+ if (strpos($noterec, '1 SOUR') !== false) {
+ $data .= FunctionsPrintFacts::printFactSources($noterec, 1);
+ }
+ }
+ }
+ } else {
+ $data = '<div class="fact_NOTE"><span class="label">' . I18N::translate('Note') . '</span>: <span class="field error">' . $nmatch[1] . '</span></div>';
+ }
+ }
+ if (!$textOnly) {
+ if (strpos($factrec, "$nlevel SOUR") !== false) {
+ $data .= "<div class=\"indent\">";
+ $data .= FunctionsPrintFacts::printFactSources($nrec, $nlevel);
+ $data .= "</div>";
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Print a link for a popup help window.
+ *
+ * @param string $help_topic
+ * @param string $module
+ *
+ * @return string
+ */
+ public static function helpLink($help_topic, $module = '') {
+ return '<span class="icon-help" onclick="helpDialog(\'' . $help_topic . '\',\'' . $module . '\'); return false;">&nbsp;</span>';
+ }
+
+ /**
+ * Print an external help link to the wiki site, in a new window
+ *
+ * @param string $topic
+ *
+ * @return string
+ */
+ public static function wikiHelpLink($topic) {
+ return '<a class="help icon-wiki" href="' . WT_WEBTREES_WIKI . $topic . '" title="' . I18N::translate('webtrees wiki') . '" target="_blank">&nbsp;</a>';
+ }
+
+ /**
+ * When a user has searched for text, highlight any matches in
+ * the displayed string.
+ *
+ * @param string $string
+ *
+ * @return string
+ */
+ public static function highlightSearchHits($string) {
+ global $controller;
+
+ if ($controller instanceof SearchController && $controller->query) {
+ // TODO: when a search contains multiple words, we search independently.
+ // e.g. searching for "FOO BAR" will find records containing both FOO and BAR.
+ // However, we only highlight the original search string, not the search terms.
+ // The controller needs to provide its "query_terms" array.
+ $regex = array();
+ foreach (array($controller->query) as $search_term) {
+ $regex[] = preg_quote($search_term, '/');
+ }
+ // Match these strings, provided they do not occur inside HTML tags
+ $regex = '(' . implode('|', $regex) . ')(?![^<]*>)';
+
+ return preg_replace('/' . $regex . '/i', '<span class="search_hit">$1</span>', $string);
+ } else {
+ return $string;
+ }
+ }
+
+ /**
+ * Print the associations from the associated individuals in $event to the individuals in $record
+ *
+ * @param Fact $event
+ *
+ * @return string
+ */
+ public static function formatAssociateRelationship(Fact $event) {
+ $parent = $event->getParent();
+ // To whom is this record an assocate?
+ if ($parent instanceof Individual) {
+ // On an individual page, we just show links to the person
+ $associates = array($parent);
+ } elseif ($parent instanceof Family) {
+ // On a family page, we show links to both spouses
+ $associates = $parent->getSpouses();
+ } else {
+ // On other pages, it does not make sense to show associates
+ return '';
+ }
+
+ preg_match_all('/^1 ASSO @(' . WT_REGEX_XREF . ')@((\n[2-9].*)*)/', $event->getGedcom(), $amatches1, PREG_SET_ORDER);
+ preg_match_all('/\n2 _?ASSO @(' . WT_REGEX_XREF . ')@((\n[3-9].*)*)/', $event->getGedcom(), $amatches2, PREG_SET_ORDER);
+
+ $html = '';
+ // For each ASSO record
+ foreach (array_merge($amatches1, $amatches2) as $amatch) {
+ $person = Individual::getInstance($amatch[1], $event->getParent()->getTree());
+ if ($person && $person->canShowName()) {
+ // Is there a "RELA" tag
+ if (preg_match('/\n[23] RELA (.+)/', $amatch[2], $rmatch)) {
+ // Use the supplied relationship as a label
+ $label = GedcomCodeRela::getValue($rmatch[1], $person);
+ } else {
+ // Use a default label
+ $label = GedcomTag::getLabel('ASSO', $person);
+ }
+
+ $values = array('<a href="' . $person->getHtmlUrl() . '">' . $person->getFullName() . '</a>');
+ if (!Auth::isSearchEngine()) {
+ foreach ($associates as $associate) {
+ $relationship_name = Functions::getAssociateRelationshipName($associate, $person);
+ if (!$relationship_name) {
+ $relationship_name = GedcomTag::getLabel('RELA');
+ }
+
+ if ($parent instanceof Family) {
+ // For family ASSO records (e.g. MARR), identify the spouse with a sex icon
+ $relationship_name .= $associate->getSexImage();
+ }
+
+ $values[] = '<a href="relationship.php?pid1=' . $associate->getXref() . '&amp;pid2=' . $person->getXref() . '&amp;ged=' . $associate->getTree()->getNameUrl() . '">' . $relationship_name . '</a>';
+ }
+ }
+ $value = implode(' — ', $values);
+
+ // Use same markup as GedcomTag::getLabelValue()
+ $asso = I18N::translate('<span class="label">%1$s:</span> <span class="field" dir="auto">%2$s</span>', $label, $value);
+ } elseif (!$person && Auth::isEditor($event->getParent()->getTree())) {
+ $asso = GedcomTag::getLabelValue('ASSO', '<span class="error">' . $amatch[1] . '</span>');
+ } else {
+ $asso = '';
+ }
+ $html .= '<div class="fact_ASSO">' . $asso . '</div>';
+ }
+
+ return $html;
+ }
+
+ /**
+ * Format age of parents in HTML
+ *
+ * @param Individual $person child
+ * @param Date $birth_date
+ *
+ * @return string HTML
+ */
+ public static function formatParentsAges(Individual $person, Date $birth_date) {
+ $html = '';
+ $families = $person->getChildFamilies();
+ // Multiple sets of parents (e.g. adoption) cause complications, so ignore.
+ if ($birth_date->isOK() && count($families) == 1) {
+ $family = current($families);
+ foreach ($family->getSpouses() as $parent) {
+ if ($parent->getBirthDate()->isOK()) {
+ $sex = $parent->getSexImage();
+ $age = Date::getAge($parent->getBirthDate(), $birth_date, 2);
+ $deatdate = $parent->getDeathDate();
+ switch ($parent->getSex()) {
+ case 'F':
+ // Highlight mothers who die in childbirth or shortly afterwards
+ if ($deatdate->isOK() && $deatdate->maximumJulianDay() < $birth_date->minimumJulianDay() + 90) {
+ $html .= ' <span title="' . GedcomTag::getLabel('_DEAT_PARE', $parent) . '" class="parentdeath">' . $sex . $age . '</span>';
+ } else {
+ $html .= ' <span title="' . I18N::translate('Mother’s age') . '">' . $sex . $age . '</span>';
+ }
+ break;
+ case 'M':
+ // Highlight fathers who die before the birth
+ if ($deatdate->isOK() && $deatdate->maximumJulianDay() < $birth_date->minimumJulianDay()) {
+ $html .= ' <span title="' . GedcomTag::getLabel('_DEAT_PARE', $parent) . '" class="parentdeath">' . $sex . $age . '</span>';
+ } else {
+ $html .= ' <span title="' . I18N::translate('Father’s age') . '">' . $sex . $age . '</span>';
+ }
+ break;
+ default:
+ $html .= ' <span title="' . I18N::translate('Parent’s age') . '">' . $sex . $age . '</span>';
+ break;
+ }
+ }
+ }
+ if ($html) {
+ $html = '<span class="age">' . $html . '</span>';
+ }
+ }
+
+ return $html;
+ }
+
+ /**
+ * Print fact DATE/TIME
+ *
+ * @param Fact $event event containing the date/age
+ * @param GedcomRecord $record the person (or couple) whose ages should be printed
+ * @param bool $anchor option to print a link to calendar
+ * @param bool $time option to print TIME value
+ *
+ * @return string
+ */
+ public static function formatFactDate(Fact $event, GedcomRecord $record, $anchor, $time) {
+ global $pid;
+
+ $factrec = $event->getGedcom();
+ $html = '';
+ // Recorded age
+ if (preg_match('/\n2 AGE (.+)/', $factrec, $match)) {
+ $fact_age = $match[1];
+ } else {
+ $fact_age = '';
+ }
+ if (preg_match('/\n2 HUSB\n3 AGE (.+)/', $factrec, $match)) {
+ $husb_age = $match[1];
+ } else {
+ $husb_age = '';
+ }
+ if (preg_match('/\n2 WIFE\n3 AGE (.+)/', $factrec, $match)) {
+ $wife_age = $match[1];
+ } else {
+ $wife_age = '';
+ }
+
+ // Calculated age
+ if (preg_match('/\n2 DATE (.+)/', $factrec, $match)) {
+ $date = new Date($match[1]);
+ $html .= ' ' . $date->display($anchor && !Auth::isSearchEngine());
+ // time
+ if ($time && preg_match('/\n3 TIME (.+)/', $factrec, $match)) {
+ $html .= ' – <span class="date">' . $match[1] . '</span>';
+ }
+ $fact = $event->getTag();
+ if ($record instanceof Individual) {
+ if ($fact === 'BIRT' && $record->getTree()->getPreference('SHOW_PARENTS_AGE')) {
+ // age of parents at child birth
+ $html .= self::formatParentsAges($record, $date);
+ } elseif ($fact !== 'CHAN' && $fact !== '_TODO') {
+ // age at event
+ $birth_date = $record->getBirthDate();
+ // Can't use getDeathDate(), as this also gives BURI/CREM events, which
+ // wouldn't give the correct "days after death" result for people with
+ // no DEAT.
+ $death_event = $record->getFirstFact('DEAT');
+ if ($death_event) {
+ $death_date = $death_event->getDate();
+ } else {
+ $death_date = new Date('');
+ }
+ $ageText = '';
+ if ((Date::compare($date, $death_date) <= 0 || !$record->isDead()) || $fact == 'DEAT') {
+ // Before death, print age
+ $age = Date::getAgeGedcom($birth_date, $date);
+ // Only show calculated age if it differs from recorded age
+ if ($age != '') {
+ if (
+ $fact_age != '' && $fact_age != $age ||
+ $fact_age == '' && $husb_age == '' && $wife_age == '' ||
+ $husb_age != '' && $record->getSex() == 'M' && $husb_age != $age ||
+ $wife_age != '' && $record->getSex() == 'F' && $wife_age != $age
+ ) {
+ if ($age != "0d") {
+ $ageText = '(' . I18N::translate('Age') . ' ' . FunctionsDate::getAgeAtEvent($age, false) . ')';
+ }
+ }
+ }
+ }
+ if ($fact != 'DEAT' && Date::compare($date, $death_date) >= 0) {
+ // After death, print time since death
+ $age = FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($death_date, $date), true);
+ if ($age != '') {
+ if (Date::getAgeGedcom($death_date, $date) == "0d") {
+ $ageText = '(' . I18N::translate('on the date of death') . ')';
+ } else {
+ $ageText = '(' . $age . ' ' . I18N::translate('after death') . ')';
+ // Family events which occur after death are probably errors
+ if ($event->getParent() instanceof Family) {
+ $ageText .= '<i class="icon-warning"></i>';
+ }
+ }
+ }
+ }
+ if ($ageText) {
+ $html .= ' <span class="age">' . $ageText . '</span>';
+ }
+ }
+ } elseif ($record instanceof Family) {
+ $indi = Individual::getInstance($pid, $record->getTree());
+ if ($indi) {
+ $birth_date = $indi->getBirthDate();
+ $death_date = $indi->getDeathDate();
+ $ageText = '';
+ if (Date::compare($date, $death_date) <= 0) {
+ $age = Date::getAgeGedcom($birth_date, $date);
+ // Only show calculated age if it differs from recorded age
+ if ($age != '' && $age > 0) {
+ if (
+ $fact_age != '' && $fact_age != $age ||
+ $fact_age == '' && $husb_age == '' && $wife_age == '' ||
+ $husb_age != '' && $indi->getSex() == 'M' && $husb_age != $age ||
+ $wife_age != '' && $indi->getSex() == 'F' && $wife_age != $age
+ ) {
+ $ageText = '(' . I18N::translate('Age') . ' ' . FunctionsDate::getAgeAtEvent($age, false) . ')';
+ }
+ }
+ }
+ if ($ageText) {
+ $html .= ' <span class="age">' . $ageText . '</span>';
+ }
+ }
+ }
+ } else {
+ // 1 DEAT Y with no DATE => print YES
+ // 1 BIRT 2 SOUR @S1@ => print YES
+ // 1 DEAT N is not allowed
+ // It is not proper GEDCOM form to use a N(o) value with an event tag to infer that it did not happen.
+ $factdetail = explode(' ', trim($factrec));
+ if (isset($factdetail) && (count($factdetail) == 3 && strtoupper($factdetail[2]) == 'Y') || (count($factdetail) == 4 && $factdetail[2] == 'SOUR')) {
+ $html .= I18N::translate('yes');
+ }
+ }
+ // print gedcom ages
+ foreach (array(GedcomTag::getLabel('AGE') => $fact_age, GedcomTag::getLabel('HUSB') => $husb_age, GedcomTag::getLabel('WIFE') => $wife_age) as $label => $age) {
+ if ($age != '') {
+ $html .= ' <span class="label">' . $label . ':</span> <span class="age">' . FunctionsDate::getAgeAtEvent($age, false) . '</span>';
+ }
+ }
+
+ return $html;
+ }
+
+ /**
+ * print fact PLACe TEMPle STATus
+ *
+ * @param Fact $event gedcom fact record
+ * @param bool $anchor to print a link to placelist
+ * @param bool $sub_records to print place subrecords
+ * @param bool $lds to print LDS TEMPle and STATus
+ *
+ * @return string HTML
+ */
+ public static function formatFactPlace(Fact $event, $anchor = false, $sub_records = false, $lds = false) {
+ if ($anchor) {
+ // Show the full place name, for facts/events tab
+ $html = '<a href="' . $event->getPlace()->getURL() . '">' . $event->getPlace()->getFullName() . '</a>';
+ } else {
+ // Abbreviate the place name, for chart boxes
+ return ' - ' . $event->getPlace()->getShortName();
+ }
+
+ if ($sub_records) {
+ $placerec = Functions::getSubRecord(2, '2 PLAC', $event->getGedcom());
+ if (!empty($placerec)) {
+ if (preg_match_all('/\n3 (?:_HEB|ROMN) (.+)/', $placerec, $matches)) {
+ foreach ($matches[1] as $match) {
+ $wt_place = new Place($match, $event->getParent()->getTree());
+ $html .= ' - ' . $wt_place->getFullName();
+ }
+ }
+ $map_lati = "";
+ $cts = preg_match('/\d LATI (.*)/', $placerec, $match);
+ if ($cts > 0) {
+ $map_lati = $match[1];
+ $html .= '<br><span class="label">' . GedcomTag::getLabel('LATI') . ': </span>' . $map_lati;
+ }
+ $map_long = '';
+ $cts = preg_match('/\d LONG (.*)/', $placerec, $match);
+ if ($cts > 0) {
+ $map_long = $match[1];
+ $html .= ' <span class="label">' . GedcomTag::getLabel('LONG') . ': </span>' . $map_long;
+ }
+ if ($map_lati && $map_long) {
+ $map_lati = trim(strtr($map_lati, "NSEW,�", " - -. ")); // S5,6789 ==> -5.6789
+ $map_long = trim(strtr($map_long, "NSEW,�", " - -. ")); // E3.456� ==> 3.456
+ $html .= ' <a rel="nofollow" href="https://maps.google.com/maps?q=' . $map_lati . ',' . $map_long . '" class="icon-googlemaps" title="' . I18N::translate('Google Maps™') . '"></a>';
+ $html .= ' <a rel="nofollow" href="https://www.bing.com/maps/?lvl=15&cp=' . $map_lati . '~' . $map_long . '" class="icon-bing" title="' . I18N::translate('Bing Maps™') . '"></a>';
+ $html .= ' <a rel="nofollow" href="https://www.openstreetmap.org/#map=15/' . $map_lati . '/' . $map_long . '" class="icon-osm" title="' . I18N::translate('OpenStreetMap™') . '"></a>';
+ }
+ if (preg_match('/\d NOTE (.*)/', $placerec, $match)) {
+ $html .= '<br>' . self::printFactNotes($placerec, 3);
+ }
+ }
+ }
+ if ($lds) {
+ if (preg_match('/2 TEMP (.*)/', $event->getGedcom(), $match)) {
+ $html .= '<br>' . I18N::translate('LDS temple') . ': ' . GedcomCodeTemp::templeName($match[1]);
+ }
+ if (preg_match('/2 STAT (.*)/', $event->getGedcom(), $match)) {
+ $html .= '<br>' . I18N::translate('Status') . ': ' . GedcomCodeStat::statusName($match[1]);
+ if (preg_match('/3 DATE (.*)/', $event->getGedcom(), $match)) {
+ $date = new Date($match[1]);
+ $html .= ', ' . GedcomTag::getLabel('STAT:DATE') . ': ' . $date->display();
+ }
+ }
+ }
+
+ return $html;
+ }
+
+ /**
+ * Check for facts that may exist only once for a certain record type.
+ * If the fact already exists in the second array, delete it from the first one.
+ *
+ * @param string[] $uniquefacts
+ * @param Fact[] $recfacts
+ * @param string $type
+ *
+ * @return string[]
+ */
+ public static function checkFactUnique($uniquefacts, $recfacts, $type) {
+ foreach ($recfacts as $factarray) {
+ $fact = false;
+ if (is_object($factarray)) {
+ $fact = $factarray->getTag();
+ } else {
+ if ($type === 'SOUR' || $type === 'REPO') {
+ $factrec = $factarray[0];
+ }
+ if ($type === 'FAM' || $type === 'INDI') {
+ $factrec = $factarray[1];
+ }
+
+ $ft = preg_match("/1 (\w+)(.*)/", $factrec, $match);
+ if ($ft > 0) {
+ $fact = trim($match[1]);
+ }
+ }
+ if ($fact !== false) {
+ $key = array_search($fact, $uniquefacts);
+ if ($key !== false) {
+ unset($uniquefacts[$key]);
+ }
+ }
+ }
+
+ return $uniquefacts;
+ }
+
+ /**
+ * Print a new fact box on details pages
+ *
+ * @param string $id the id of the person, family, source etc the fact will be added to
+ * @param array $usedfacts an array of facts already used in this record
+ * @param string $type the type of record INDI, FAM, SOUR etc
+ */
+ public static function printAddNewFact($id, $usedfacts, $type) {
+ global $WT_TREE;
+
+ // -- Add from clipboard
+ if (is_array(Session::get('clipboard'))) {
+ $newRow = true;
+ foreach (array_reverse(Session::get('clipboard'), true) as $fact_id => $fact) {
+ if ($fact["type"] == $type || $fact["type"] == 'all') {
+ if ($newRow) {
+ $newRow = false;
+ echo '<tr><td class="descriptionbox">';
+ echo I18N::translate('Add from clipboard'), '</td>';
+ echo '<td class="optionbox wrap"><form method="get" name="newFromClipboard" action="?" onsubmit="return false;">';
+ echo '<select id="newClipboardFact">';
+ }
+ echo '<option value="', Filter::escapeHtml($fact_id), '">', GedcomTag::getLabel($fact['fact']);
+ // TODO use the event class to store/parse the clipboard events
+ if (preg_match('/^2 DATE (.+)/m', $fact['factrec'], $match)) {
+ $tmp = new Date($match[1]);
+ echo '; ', $tmp->minimumDate()->format('%Y');
+ }
+ if (preg_match('/^2 PLAC ([^,\n]+)/m', $fact['factrec'], $match)) {
+ echo '; ', $match[1];
+ }
+ echo '</option>';
+ }
+ }
+ if (!$newRow) {
+ echo '</select>';
+ echo '&nbsp;&nbsp;<input type="button" value="', I18N::translate('Add'), "\" onclick=\"return paste_fact('$id', '#newClipboardFact');\"> ";
+ echo '</form></td></tr>', "\n";
+ }
+ }
+
+ // -- Add from pick list
+ switch ($type) {
+ case "INDI":
+ $addfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('INDI_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
+ $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('INDI_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
+ $quickfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('INDI_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
+ break;
+ case "FAM":
+ $addfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('FAM_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
+ $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('FAM_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
+ $quickfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('FAM_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
+ break;
+ case "SOUR":
+ $addfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('SOUR_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
+ $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('SOUR_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
+ $quickfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('SOUR_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
+ break;
+ case "NOTE":
+ $addfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('NOTE_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
+ $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('NOTE_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
+ $quickfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('NOTE_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
+ break;
+ case "REPO":
+ $addfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('REPO_FACTS_ADD'), -1, PREG_SPLIT_NO_EMPTY);
+ $uniquefacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('REPO_FACTS_UNIQUE'), -1, PREG_SPLIT_NO_EMPTY);
+ $quickfacts = preg_split("/[, ;:]+/", $WT_TREE->getPreference('REPO_FACTS_QUICK'), -1, PREG_SPLIT_NO_EMPTY);
+ break;
+ default:
+ return;
+ }
+ $addfacts = array_merge(self::checkFactUnique($uniquefacts, $usedfacts, $type), $addfacts);
+ $quickfacts = array_intersect($quickfacts, $addfacts);
+ $translated_addfacts = array();
+ foreach ($addfacts as $addfact) {
+ $translated_addfacts[$addfact] = GedcomTag::getLabel($addfact);
+ }
+ uasort($translated_addfacts, function ($x, $y) {
+ return I18N::strcasecmp(I18N::translate($x), I18N::translate($y));
+ });
+ echo '<tr><td class="descriptionbox">';
+ echo I18N::translate('Fact or event');
+ echo '</td>';
+ echo '<td class="optionbox wrap">';
+ echo '<form method="get" name="newfactform" action="?" onsubmit="return false;">';
+ echo '<select id="newfact" name="newfact">';
+ echo '<option value="" disabled selected>' . I18N::translate('&lt;select&gt;') . '</option>';
+ foreach ($translated_addfacts as $fact => $fact_name) {
+ echo '<option value="', $fact, '">', $fact_name, '</option>';
+ }
+ if ($type == 'INDI' || $type == 'FAM') {
+ echo '<option value="FACT">', I18N::translate('Custom fact'), '</option>';
+ echo '<option value="EVEN">', I18N::translate('Custom event'), '</option>';
+ }
+ echo '</select>';
+ echo '<input type="button" value="', I18N::translate('Add'), '" onclick="add_record(\'' . $id . '\', \'newfact\');">';
+ echo '<span class="quickfacts">';
+ foreach ($quickfacts as $fact) {
+ echo '<a href="#" onclick="add_new_record(\'' . $id . '\', \'' . $fact . '\');return false;">', GedcomTag::getLabel($fact), '</a>';
+ }
+ echo '</span></form>';
+ echo '</td></tr>';
+ }
+
+ /**
+ * javascript declaration for calendar popup
+ */
+ public static function initializeCalendarPopup() {
+ global $controller;
+
+ $controller->addInlineJavascript('
+ cal_setMonthNames(
+ "' . I18N::translateContext('NOMINATIVE', 'January') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'February') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'March') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'April') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'May') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'June') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'July') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'August') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'September') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'October') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'November') . '",
+ "' . I18N::translateContext('NOMINATIVE', 'December') . '"
+ )
+ cal_setDayHeaders(
+ "' . I18N::translate('Sun') . '",
+ "' . I18N::translate('Mon') . '",
+ "' . I18N::translate('Tue') . '",
+ "' . I18N::translate('Wed') . '",
+ "' . I18N::translate('Thu') . '",
+ "' . I18N::translate('Fri') . '",
+ "' . I18N::translate('Sat') . '"
+ )
+ cal_setWeekStart(' . I18N::firstDay() . ');
+ ');
+ }
+
+ /**
+ * @param string $element_id
+ * @param string $indiname
+ * @param Tree $tree
+ *
+ * @return string
+ */
+ public static function printFindIndividualLink($element_id, $indiname = '', $tree = null) {
+ global $WT_TREE;
+
+ if ($tree === null) {
+ $tree = $WT_TREE;
+ }
+
+ return '<a href="#" onclick="findIndi(document.getElementById(\'' . $element_id . '\'), document.getElementById(\'' . $indiname . '\'), \'' . $tree->getNameHtml() . '\'); return false;" class="icon-button_indi" title="' . I18N::translate('Find an individual') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ *
+ * @return string
+ */
+ public static function printFindPlaceLink($element_id) {
+ return '<a href="#" onclick="findPlace(document.getElementById(\'' . $element_id . '\'), WT_GEDCOM); return false;" class="icon-button_place" title="' . I18N::translate('Find a place') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ *
+ * @return string
+ */
+ public static function printFindFamilyLink($element_id) {
+ return '<a href="#" onclick="findFamily(document.getElementById(\'' . $element_id . '\'), WT_GEDCOM); return false;" class="icon-button_family" title="' . I18N::translate('Find a family') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ *
+ * @return string
+ */
+ public static function printSpecialCharacterLink($element_id) {
+ return '<span onclick="findSpecialChar(document.getElementById(\'' . $element_id . '\')); if (window.updatewholename) { updatewholename(); } return false;" class="icon-button_keyboard" title="' . I18N::translate('Find a special character') . '"></span>';
+ }
+
+ /**
+ * @param string $element_id
+ * @param string[] $choices
+ */
+ public static function printAutoPasteLink($element_id, $choices) {
+ echo '<small>';
+ foreach ($choices as $choice) {
+ echo '<span onclick="document.getElementById(\'', $element_id, '\').value=';
+ echo '\'', $choice, '\';';
+ echo " return false;\">", $choice, '</span> ';
+ }
+ echo '</small>';
+ }
+
+ /**
+ * @param string $element_id
+ * @param string $sourcename
+ *
+ * @return string
+ */
+ public static function printFindSourceLink($element_id, $sourcename = '') {
+ return '<a href="#" onclick="findSource(document.getElementById(\'' . $element_id . '\'), document.getElementById(\'' . $sourcename . '\'), WT_GEDCOM); return false;" class="icon-button_source" title="' . I18N::translate('Find a source') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ * @param string $notename
+ *
+ * @return string
+ */
+ public static function printFindNoteLink($element_id, $notename = '') {
+ return '<a href="#" onclick="findnote(document.getElementById(\'' . $element_id . '\'), document.getElementById(\'' . $notename . '\'), \'WT_GEDCOM\'); return false;" class="icon-button_find" title="' . I18N::translate('Find a shared note') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ *
+ * @return string
+ */
+ public static function printFindRepositoryLink($element_id) {
+ return '<a href="#" onclick="findRepository(document.getElementById(\'' . $element_id . '\'), WT_GEDCOM); return false;" class="icon-button_repository" title="' . I18N::translate('Find a repository') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ * @param string $choose
+ *
+ * @return string
+ */
+ public static function printFindMediaLink($element_id, $choose = '') {
+ return '<a href="#" onclick="findMedia(document.getElementById(\'' . $element_id . '\'), \'' . $choose . '\', WT_GEDCOM); return false;" class="icon-button_media" title="' . I18N::translate('Find a media object') . '"></a>';
+ }
+
+ /**
+ * @param string $element_id
+ *
+ * @return string
+ */
+ public static function printFindFactLink($element_id) {
+ return '<a href="#" onclick="findFact(document.getElementById(\'' . $element_id . '\'), WT_GEDCOM); return false;" class="icon-button_find_facts" title="' . I18N::translate('Find a fact or event') . '"></a>';
+ }
+
+ /**
+ * Summary of LDS ordinances.
+ *
+ * @param Individual $individual
+ *
+ * @return string
+ */
+ public static function getLdsSummary(Individual $individual) {
+ $BAPL = $individual->getFacts('BAPL') ? 'B' : '_';
+ $ENDL = $individual->getFacts('ENDL') ? 'E' : '_';
+ $SLGC = $individual->getFacts('SLGC') ? 'C' : '_';
+ $SLGS = '_';
+
+ foreach ($individual->getSpouseFamilies() as $family) {
+ if ($family->getFacts('SLGS')) {
+ $SLGS = '';
+ }
+ }
+
+ return $BAPL . $ENDL . $SLGS . $SLGC;
+ }
+}
diff --git a/app/Functions/FunctionsPrintFacts.php b/app/Functions/FunctionsPrintFacts.php
new file mode 100644
index 0000000000..f8634859c2
--- /dev/null
+++ b/app/Functions/FunctionsPrintFacts.php
@@ -0,0 +1,1150 @@
+<?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\Date;
+use Fisharebest\Webtrees\Fact;
+use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeAdop;
+use Fisharebest\Webtrees\GedcomCode\GedcomCodeQuay;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\GedcomTag;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Media;
+use Fisharebest\Webtrees\Module;
+use Fisharebest\Webtrees\Module\CensusAssistantModule;
+use Fisharebest\Webtrees\Note;
+use Fisharebest\Webtrees\Repository;
+use Fisharebest\Webtrees\Source;
+use Fisharebest\Webtrees\Theme;
+use Fisharebest\Webtrees\User;
+use Rhumsaa\Uuid\Uuid;
+
+/**
+ * Class FunctionsPrintFacts - common functions
+ */
+class FunctionsPrintFacts {
+ /**
+ * Print a fact record, for the individual/family/source/repository/etc. pages.
+ *
+ * Although a Fact has a parent object, we also need to know
+ * the GedcomRecord for which we are printing it. For example,
+ * we can show the death of X on the page of Y, or the marriage
+ * of X+Y on the page of Z. We need to know both records to
+ * calculate ages, relationships, etc.
+ *
+ * @param Fact $fact
+ * @param GedcomRecord $record
+ */
+ public static function printFact(Fact $fact, GedcomRecord $record) {
+ static $n_chil = 0, $n_gchi = 0;
+
+ $parent = $fact->getParent();
+
+ // Some facts don't get printed here ...
+ switch ($fact->getTag()) {
+ case 'NOTE':
+ self::printMainNotes($fact, 1);
+
+ return;
+ case 'SOUR':
+ self::printMainSources($fact, 1);
+
+ return;
+ case 'OBJE':
+ self::printMainMedia($fact, 1);
+
+ return;
+ case 'FAMC':
+ case 'FAMS':
+ case 'CHIL':
+ case 'HUSB':
+ case 'WIFE':
+ // These are internal links, not facts
+ return;
+ case '_WT_OBJE_SORT':
+ // These links are used internally to record the sort order.
+ return;
+ default:
+ // Hide unrecognized/custom tags?
+ if ($fact->getParent()->getTree()->getPreference('HIDE_GEDCOM_ERRORS') && !GedcomTag::isTag($fact->getTag())) {
+ return;
+ }
+ break;
+ }
+
+ // Who is this fact about? Need it to translate fact label correctly
+ if ($parent instanceof Family && $record instanceof Individual) {
+ // Family event
+ $label_person = $fact->getParent()->getSpouse($record);
+ } else {
+ // Individual event
+ $label_person = $parent;
+ }
+
+ // New or deleted facts need different styling
+ $styleadd = '';
+ if ($fact->isPendingAddition()) {
+ $styleadd = 'new';
+ }
+ if ($fact->isPendingDeletion()) {
+ $styleadd = 'old';
+ }
+
+ // Event of close relative
+ if (preg_match('/^_[A-Z_]{3,5}_[A-Z0-9]{4}$/', $fact->getTag())) {
+ $styleadd = trim($styleadd . ' rela');
+ }
+
+ // Event of close associates
+ if ($fact->getFactId() == 'asso') {
+ $styleadd = trim($styleadd . ' rela');
+ }
+
+ // historical facts
+ if ($fact->getFactId() == 'histo') {
+ $styleadd = trim($styleadd . ' histo');
+ }
+
+ // Does this fact have a type?
+ if (preg_match('/\n2 TYPE (.+)/', $fact->getGedcom(), $match)) {
+ $type = $match[1];
+ } else {
+ $type = '';
+ }
+
+ switch ($fact->getTag()) {
+ case 'EVEN':
+ case 'FACT':
+ if (GedcomTag::isTag($type)) {
+ // Some users (just Meliza?) use "1 EVEN/2 TYPE BIRT". Translate the TYPE.
+ $label = GedcomTag::getLabel($type, $label_person);
+ $type = ''; // Do not print this again
+ } elseif ($type) {
+ // We don't have a translation for $type - but a custom translation might exist.
+ $label = I18N::translate(Filter::escapeHtml($type));
+ $type = ''; // Do not print this again
+ } else {
+ // An unspecified fact/event
+ $label = $fact->getLabel();
+ }
+ break;
+ case 'MARR':
+ // This is a hack for a proprietory extension. Is it still used/needed?
+ $utype = strtoupper($type);
+ if ($utype == 'CIVIL' || $utype == 'PARTNERS' || $utype == 'RELIGIOUS') {
+ $label = GedcomTag::getLabel('MARR_' . $utype, $label_person);
+ $type = ''; // Do not print this again
+ } else {
+ $label = $fact->getLabel();
+ }
+ break;
+ default:
+ // Normal fact/event
+ $label = $fact->getLabel();
+ break;
+ }
+
+ echo '<tr class="', $styleadd, '">';
+ echo '<td class="descriptionbox width20">';
+
+ if ($fact->getParent()->getTree()->getPreference('SHOW_FACT_ICONS')) {
+ echo Theme::theme()->icon($fact), ' ';
+ }
+
+ if ($fact->getFactId() != 'histo' && $fact->canEdit()) {
+ ?>
+ <a
+ href="#"
+ title="<?php echo I18N::translate('Edit'); ?>"
+ onclick="return edit_record('<?php echo $parent->getXref(); ?>', '<?php echo $fact->getFactId(); ?>');"
+ ><?php echo $label; ?></a>
+ <div class="editfacts">
+ <div class="editlink">
+ <a
+ href="#"
+ title="<?php echo I18N::translate('Edit'); ?>"
+ class="editicon"
+ onclick="return edit_record('<?php echo $parent->getXref(); ?>', '<?php echo $fact->getFactId(); ?>');"
+ ><span class="link_text"><?php echo I18N::translate('Edit'); ?></span></a>
+ </div>
+ <div class="copylink">
+ <a
+ href="#"
+ title="<?php echo I18N::translate('Copy'); ?>"
+ class="copyicon"
+ onclick="return copy_fact('<?php echo $parent->getXref(); ?>', '<?php echo $fact->getFactId(); ?>');"
+ ><span class="link_text"><?php echo I18N::translate('Copy'); ?></span></a>
+ </div>
+ <div class="deletelink">
+ <a
+ href="#"
+ title="<?php echo I18N::translate('Delete'); ?>"
+ class="deleteicon"
+ onclick="return delete_fact('<?php echo I18N::translate('Are you sure you want to delete this fact?'); ?>', '<?php echo $parent->getXref(); ?>', '<?php echo $fact->getFactId(); ?>');"
+ ><span class="link_text"><?php echo I18N::translate('Delete'); ?></span></a>
+ </div>
+ </div>
+ <?php
+ } else {
+ echo $label;
+ }
+
+ switch ($fact->getTag()) {
+ case '_BIRT_CHIL':
+ echo '<br>', /* I18N: Abbreviation for "number %s" */
+ I18N::translate('#%s', ++$n_chil);
+ break;
+ case '_BIRT_GCHI':
+ case '_BIRT_GCH1':
+ case '_BIRT_GCH2':
+ echo '<br>', I18N::translate('#%s', ++$n_gchi);
+ break;
+ }
+
+ echo '</td><td class="optionbox ', $styleadd, ' wrap">';
+
+ // Event from another record?
+ if ($parent !== $record) {
+ if ($parent instanceof Family) {
+ foreach ($parent->getSpouses() as $spouse) {
+ if ($record !== $spouse) {
+ echo '<a href="', $spouse->getHtmlUrl(), '">', $spouse->getFullName(), '</a> — ';
+ }
+ }
+ echo '<a href="', $parent->getHtmlUrl(), '">', I18N::translate('View family'), '</a><br>';
+ } elseif ($parent instanceof Individual) {
+ echo '<a href="', $parent->getHtmlUrl(), '">', $parent->getFullName(), '</a><br>';
+ }
+ }
+
+ // Print the value of this fact/event
+ switch ($fact->getTag()) {
+ case 'ADDR':
+ echo $fact->getValue();
+ break;
+ case 'AFN':
+ echo '<div class="field"><a href="https://familysearch.org/search/tree/results#count=20&query=afn:', rawurlencode($fact->getValue()), '" target="new">', Filter::escapeHtml($fact->getValue()), '</a></div>';
+ break;
+ case 'ASSO':
+ // we handle this later, in format_asso_rela_record()
+ break;
+ case 'EMAIL':
+ case 'EMAI':
+ case '_EMAIL':
+ echo '<div class="field"><a href="mailto:', Filter::escapeHtml($fact->getValue()), '">', Filter::escapeHtml($fact->getValue()), '</a></div>';
+ break;
+ case 'FILE':
+ if (Auth::isEditor($fact->getParent()->getTree())) {
+ echo '<div class="field">', Filter::escapeHtml($fact->getValue()), '</div>';
+ }
+ break;
+ case 'RESN':
+ echo '<div class="field">';
+ switch ($fact->getValue()) {
+ case 'none':
+ // Note: "1 RESN none" is not valid gedcom.
+ // However, webtrees privacy rules will interpret it as "show an otherwise private record to public".
+ echo '<i class="icon-resn-none"></i> ', I18N::translate('Show to visitors');
+ break;
+ case 'privacy':
+ echo '<i class="icon-class-none"></i> ', I18N::translate('Show to members');
+ break;
+ case 'confidential':
+ echo '<i class="icon-confidential-none"></i> ', I18N::translate('Show to managers');
+ break;
+ case 'locked':
+ echo '<i class="icon-locked-none"></i> ', I18N::translate('Only managers can edit');
+ break;
+ default:
+ echo Filter::escapeHtml($fact->getValue());
+ break;
+ }
+ echo '</div>';
+ break;
+ case 'PUBL': // Publication details might contain URLs.
+ echo '<div class="field">', Filter::expandUrls($fact->getValue()), '</div>';
+ break;
+ case 'REPO':
+ if (preg_match('/^@(' . WT_REGEX_XREF . ')@$/', $fact->getValue(), $match)) {
+ self::printRepositoryRecord($match[1]);
+ } else {
+ echo '<div class="error">', Filter::escapeHtml($fact->getValue()), '</div>';
+ }
+ break;
+ case 'URL':
+ case '_URL':
+ case 'WWW':
+ echo '<div class="field"><a href="', Filter::escapeHtml($fact->getValue()), '">', Filter::escapeHtml($fact->getValue()), '</a></div>';
+ break;
+ case 'TEXT': // 0 SOUR / 1 TEXT
+ echo '<div class="field">', nl2br(Filter::escapeHtml($fact->getValue()), false), '</div>';
+ break;
+ default:
+ // Display the value for all other facts/events
+ switch ($fact->getValue()) {
+ case '':
+ // Nothing to display
+ break;
+ case 'N':
+ // Not valid GEDCOM
+ echo '<div class="field">', I18N::translate('No'), '</div>';
+ break;
+ case 'Y':
+ // Do not display "Yes".
+ break;
+ default:
+ if (preg_match('/^@(' . WT_REGEX_XREF . ')@$/', $fact->getValue(), $match)) {
+ $target = GedcomRecord::getInstance($match[1], $fact->getParent()->getTree());
+ if ($target) {
+ echo '<div><a href="', $target->getHtmlUrl(), '">', $target->getFullName(), '</a></div>';
+ } else {
+ echo '<div class="error">', Filter::escapeHtml($fact->getValue()), '</div>';
+ }
+ } else {
+ echo '<div class="field"><span dir="auto">', Filter::escapeHtml($fact->getValue()), '</span></div>';
+ }
+ break;
+ }
+ break;
+ }
+
+ // Print the type of this fact/event
+ if ($type) {
+ $utype = strtoupper($type);
+ // Events of close relatives, e.g. _MARR_CHIL
+ if (substr($fact->getTag(), 0, 6) == '_MARR_' && ($utype == 'CIVIL' || $utype == 'PARTNERS' || $utype == 'RELIGIOUS')) {
+ // Translate MARR/TYPE using the code that supports MARR_CIVIL, etc. tags
+ $type = GedcomTag::getLabel('MARR_' . $utype);
+ } else {
+ // Allow (custom) translations for other types
+ $type = I18N::translate($type);
+ }
+ echo GedcomTag::getLabelValue('TYPE', Filter::escapeHtml($type));
+ }
+
+ // Print the date of this fact/event
+ echo FunctionsPrint::formatFactDate($fact, $record, true, true);
+
+ // Print the place of this fact/event
+ echo '<div class="place">', FunctionsPrint::formatFactPlace($fact, true, true, true), '</div>';
+ // A blank line between the primary attributes (value, date, place) and the secondary ones
+ echo '<br>';
+
+ $addr = $fact->getAttribute('ADDR');
+ if ($addr) {
+ echo GedcomTag::getLabelValue('ADDR', $addr);
+ }
+
+ // Print the associates of this fact/event
+ echo FunctionsPrint::formatAssociateRelationship($fact);
+
+ // Print any other "2 XXXX" attributes, in the order in which they appear.
+ preg_match_all('/\n2 (' . WT_REGEX_TAG . ') (.+)/', $fact->getGedcom(), $matches, PREG_SET_ORDER);
+ foreach ($matches as $match) {
+ switch ($match[1]) {
+ case 'DATE':
+ case 'TIME':
+ case 'AGE':
+ case 'PLAC':
+ case 'ADDR':
+ case 'ALIA':
+ case 'ASSO':
+ case '_ASSO':
+ case 'DESC':
+ case 'RELA':
+ case 'STAT':
+ case 'TEMP':
+ case 'TYPE':
+ case 'FAMS':
+ case 'CONT':
+ // These were already shown at the beginning
+ break;
+ case 'NOTE':
+ case 'OBJE':
+ case 'SOUR':
+ // These will be shown at the end
+ break;
+ case 'EVEN': // 0 SOUR / 1 DATA / 2 EVEN / 3 DATE / 3 PLAC
+ $events = array();
+ foreach (preg_split('/ *, */', $match[2]) as $event) {
+ $events[] = GedcomTag::getLabel($event);
+ }
+ if (count($events) == 1) {
+ echo GedcomTag::getLabelValue('EVEN', $event);
+ } else {
+ echo GedcomTag::getLabelValue('EVEN', implode(I18N::$list_separator, $events));
+ }
+ if (preg_match('/\n3 DATE (.+)/', $fact->getGedcom(), $date_match)) {
+ $date = new Date($date_match[1]);
+ echo GedcomTag::getLabelValue('DATE', $date->display());
+ }
+ if (preg_match('/\n3 PLAC (.+)/', $fact->getGedcom(), $plac_match)) {
+ echo GedcomTag::getLabelValue('PLAC', $plac_match[1]);
+ }
+ break;
+ case 'FAMC': // 0 INDI / 1 ADOP / 2 FAMC / 3 ADOP
+ $family = Family::getInstance(str_replace('@', '', $match[2]), $fact->getParent()->getTree());
+ if ($family) {
+ echo GedcomTag::getLabelValue('FAM', '<a href="' . $family->getHtmlUrl() . '">' . $family->getFullName() . '</a>');
+ if (preg_match('/\n3 ADOP (HUSB|WIFE|BOTH)/', $fact->getGedcom(), $match)) {
+ echo GedcomTag::getLabelValue('ADOP', GedcomCodeAdop::getValue($match[1], $label_person));
+ }
+ } else {
+ echo GedcomTag::getLabelValue('FAM', '<span class="error">' . $match[2] . '</span>');
+ }
+ break;
+ case '_WT_USER':
+ $user = User::findByIdentifier($match[2]); // may not exist
+ if ($user) {
+ echo GedcomTag::getLabelValue('_WT_USER', $user->getRealNameHtml());
+ } else {
+ echo GedcomTag::getLabelValue('_WT_USER', Filter::escapeHtml($match[2]));
+ }
+ break;
+ case 'RESN':
+ switch ($match[2]) {
+ case 'none':
+ // Note: "2 RESN none" is not valid gedcom.
+ // However, webtrees privacy rules will interpret it as "show an otherwise private fact to public".
+ echo GedcomTag::getLabelValue('RESN', '<i class="icon-resn-none"></i> ' . I18N::translate('Show to visitors'));
+ break;
+ case 'privacy':
+ echo GedcomTag::getLabelValue('RESN', '<i class="icon-resn-privacy"></i> ' . I18N::translate('Show to members'));
+ break;
+ case 'confidential':
+ echo GedcomTag::getLabelValue('RESN', '<i class="icon-resn-confidential"></i> ' . I18N::translate('Show to managers'));
+ break;
+ case 'locked':
+ echo GedcomTag::getLabelValue('RESN', '<i class="icon-resn-locked"></i> ' . I18N::translate('Only managers can edit'));
+ break;
+ default:
+ echo GedcomTag::getLabelValue('RESN', Filter::escapeHtml($match[2]));
+ break;
+ }
+ break;
+ case 'CALN':
+ echo GedcomTag::getLabelValue('CALN', Filter::expandUrls($match[2]));
+ break;
+ case 'FORM': // 0 OBJE / 1 FILE / 2 FORM / 3 TYPE
+ echo GedcomTag::getLabelValue('FORM', $match[2]);
+ if (preg_match('/\n3 TYPE (.+)/', $fact->getGedcom(), $type_match)) {
+ echo GedcomTag::getLabelValue('TYPE', GedcomTag::getFileFormTypeValue($type_match[1]));
+ }
+ break;
+ case 'URL':
+ case '_URL':
+ case 'WWW':
+ $link = '<a href="' . Filter::escapeHtml($match[2]) . '">' . Filter::escapeHtml($match[2]) . '</a>';
+ echo GedcomTag::getLabelValue($fact->getTag() . ':' . $match[1], $link);
+ break;
+ default:
+ if (!$fact->getParent()->getTree()->getPreference('HIDE_GEDCOM_ERRORS') || GedcomTag::isTag($match[1])) {
+ if (preg_match('/^@(' . WT_REGEX_XREF . ')@$/', $match[2], $xmatch)) {
+ // Links
+ $linked_record = GedcomRecord::getInstance($xmatch[1], $fact->getParent()->getTree());
+ if ($linked_record) {
+ $link = '<a href="' . $linked_record->getHtmlUrl() . '">' . $linked_record->getFullName() . '</a>';
+ echo GedcomTag::getLabelValue($fact->getTag() . ':' . $match[1], $link);
+ } else {
+ echo GedcomTag::getLabelValue($fact->getTag() . ':' . $match[1], Filter::escapeHtml($match[2]));
+ }
+ } else {
+ // Non links
+ echo GedcomTag::getLabelValue($fact->getTag() . ':' . $match[1], Filter::escapeHtml($match[2]));
+ }
+ }
+ break;
+ }
+ }
+ echo FunctionsPrintFacts::printFactSources($fact->getGedcom(), 2);
+ echo FunctionsPrint::printFactNotes($fact->getGedcom(), 2);
+ self::printMediaLinks($fact->getGedcom(), 2);
+ echo '</td></tr>';
+ }
+
+ /**
+ * print a repository record
+ *
+ * find and print repository information attached to a source
+ *
+ * @param string $xref the Gedcom Xref ID of the repository to print
+ */
+ public static function printRepositoryRecord($xref) {
+ global $WT_TREE;
+
+ $repository = Repository::getInstance($xref, $WT_TREE);
+ if ($repository && $repository->canShow()) {
+ echo '<a class="field" href="', $repository->getHtmlUrl(), '">', $repository->getFullName(), '</a><br>';
+ echo '<br>';
+ echo FunctionsPrint::printFactNotes($repository->getGedcom(), 1);
+ }
+ }
+
+ /**
+ * print a source linked to a fact (2 SOUR)
+ *
+ * this function is called by the FunctionsPrintFacts::print_fact function and other functions to
+ * print any source information attached to the fact
+ *
+ * @param string $factrec The fact record to look for sources in
+ * @param int $level The level to look for sources at
+ *
+ * @return string HTML text
+ */
+ public static function printFactSources($factrec, $level) {
+ global $WT_TREE;
+
+ $data = '';
+ $nlevel = $level + 1;
+
+ // -- Systems not using source records [ 1046971 ]
+ $ct = preg_match_all("/$level SOUR (.*)/", $factrec, $match, PREG_SET_ORDER);
+ for ($j = 0; $j < $ct; $j++) {
+ if (strpos($match[$j][1], '@') === false) {
+ $data .= '<div class="fact_SOUR"><span class="label">' . I18N::translate('Source') . ':</span> <span class="field" dir="auto">' . Filter::escapeHtml($match[$j][1]) . '</span></div>';
+ }
+ }
+ // -- find source for each fact
+ $ct = preg_match_all("/$level SOUR @(.*)@/", $factrec, $match, PREG_SET_ORDER);
+ $spos2 = 0;
+ for ($j = 0; $j < $ct; $j++) {
+ $sid = $match[$j][1];
+ $source = Source::getInstance($sid, $WT_TREE);
+ if ($source) {
+ if ($source->canShow()) {
+ $spos1 = strpos($factrec, "$level SOUR @" . $sid . "@", $spos2);
+ $spos2 = strpos($factrec, "\n$level", $spos1);
+ if (!$spos2) {
+ $spos2 = strlen($factrec);
+ }
+ $srec = substr($factrec, $spos1, $spos2 - $spos1);
+ $lt = preg_match_all("/$nlevel \w+/", $srec, $matches);
+ $data .= '<div class="fact_SOUR">';
+ $data .= '<span class="label">';
+ $elementID = Uuid::uuid4();
+ if ($WT_TREE->getPreference('EXPAND_SOURCES')) {
+ $plusminus = 'icon-minus';
+ } else {
+ $plusminus = 'icon-plus';
+ }
+ if ($lt > 0) {
+ $data .= '<a href="#" onclick="return expand_layer(\'' . $elementID . '\');"><i id="' . $elementID . '_img" class="' . $plusminus . '"></i></a> ';
+ }
+ $data .= I18N::translate('Source') . ':</span> <span class="field">';
+ $data .= '<a href="' . $source->getHtmlUrl() . '">' . $source->getFullName() . '</a>';
+ $data .= '</span></div>';
+
+ $data .= "<div id=\"$elementID\"";
+ if ($WT_TREE->getPreference('EXPAND_SOURCES')) {
+ $data .= ' style="display:block"';
+ }
+ $data .= ' class="source_citations">';
+ // PUBL
+ $publ = $source->getFirstFact('PUBL');
+ if ($publ) {
+ $data .= GedcomTag::getLabelValue('PUBL', $publ->getValue());
+ }
+ $data .= self::printSourceStructure(self::getSourceStructure($srec));
+ $data .= '<div class="indent">';
+ ob_start();
+ self::printMediaLinks($srec, $nlevel);
+ $data .= ob_get_clean();
+ $data .= FunctionsPrint::printFactNotes($srec, $nlevel, false);
+ $data .= '</div>';
+ $data .= '</div>';
+ } else {
+ // Here we could show that we do actually have sources for this data,
+ // but not the details. For example “Sources: ”.
+ // But not by default, based on user feedback.
+ // http://webtrees.net/index.php/en/forum/3-help-for-beta-and-svn-versions/27002-source-media-privacy-issue
+ }
+ } else {
+ $data .= GedcomTag::getLabelValue('SOUR', '<span class="error">' . $sid . '</span>');
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Print the links to media objects
+ *
+ * @param string $factrec
+ * @param int $level
+ */
+ public static function printMediaLinks($factrec, $level) {
+ global $WT_TREE;
+
+ $nlevel = $level + 1;
+ if (preg_match_all("/$level OBJE @(.*)@/", $factrec, $omatch, PREG_SET_ORDER) == 0) {
+ return;
+ }
+ $objectNum = 0;
+ while ($objectNum < count($omatch)) {
+ $media_id = $omatch[$objectNum][1];
+ $media = Media::getInstance($media_id, $WT_TREE);
+ if ($media) {
+ if ($media->canShow()) {
+ if ($objectNum > 0) {
+ echo '<br class="media-separator" style="clear:both;">';
+ }
+ echo '<div class="media-display"><div class="media-display-image">';
+ echo $media->displayImage();
+ echo '</div>';
+ echo '<div class="media-display-title">';
+ echo '<a href="', $media->getHtmlUrl(), '">', $media->getFullName(), '</a>';
+ // NOTE: echo the notes of the media
+ echo '<p>';
+ echo FunctionsPrint::printFactNotes($media->getGedcom(), 1);
+ $ttype = preg_match("/" . ($nlevel + 1) . " TYPE (.*)/", $media->getGedcom(), $match);
+ if ($ttype > 0) {
+ $mediaType = GedcomTag::getFileFormTypeValue($match[1]);
+ echo '<p class="label">', I18N::translate('Type'), ': </span> <span class="field">', $mediaType, '</p>';
+ }
+ echo '</p>';
+ //-- print spouse name for marriage events
+ $ct = preg_match("/WT_SPOUSE: (.*)/", $factrec, $match);
+ if ($ct > 0) {
+ $spouse = Individual::getInstance($match[1], $media->getTree());
+ if ($spouse) {
+ echo '<a href="', $spouse->getHtmlUrl(), '">';
+ echo $spouse->getFullName();
+ echo '</a>';
+ }
+ $ct = preg_match("/WT_FAMILY_ID: (.*)/", $factrec, $match);
+ if ($ct > 0) {
+ $famid = trim($match[1]);
+ $family = Family::getInstance($famid, $spouse->getTree());
+ if ($family) {
+ if ($spouse) {
+ echo " - ";
+ }
+ echo '<a href="', $family->getHtmlUrl(), '">', I18N::translate('View family'), '</a>';
+ }
+ }
+ }
+ echo FunctionsPrint::printFactNotes($media->getGedcom(), $nlevel);
+ echo FunctionsPrintFacts::printFactSources($media->getGedcom(), $nlevel);
+ echo '</div>'; //close div "media-display-title"
+ echo '</div>'; //close div "media-display"
+ }
+ } elseif (!$WT_TREE->getPreference('HIDE_GEDCOM_ERRORS')) {
+ echo '<p class="ui-state-error">', $media_id, '</p>';
+ }
+ $objectNum++;
+ }
+ }
+
+ /**
+ * Print a row for the sources tab on the individual page.
+ *
+ * @param Fact $fact
+ * @param int $level
+ */
+ public static function printMainSources(Fact $fact, $level) {
+ $factrec = $fact->getGedcom();
+ $fact_id = $fact->getFactId();
+ $parent = $fact->getParent();
+ $pid = $parent->getXref();
+
+ $nlevel = $level + 1;
+ if ($fact->isPendingAddition()) {
+ $styleadd = 'new';
+ $can_edit = $level == 1 && $fact->canEdit();
+ } elseif ($fact->isPendingDeletion()) {
+ $styleadd = 'old';
+ $can_edit = false;
+ } else {
+ $styleadd = '';
+ $can_edit = $level == 1 && $fact->canEdit();
+ }
+
+ // -- find source for each fact
+ $ct = preg_match_all("/($level SOUR (.+))/", $factrec, $match, PREG_SET_ORDER);
+ $spos2 = 0;
+ for ($j = 0; $j < $ct; $j++) {
+ $sid = trim($match[$j][2], '@');
+ $spos1 = strpos($factrec, $match[$j][1], $spos2);
+ $spos2 = strpos($factrec, "\n$level", $spos1);
+ if (!$spos2) {
+ $spos2 = strlen($factrec);
+ }
+ $srec = substr($factrec, $spos1, $spos2 - $spos1);
+ $source = Source::getInstance($sid, $fact->getParent()->getTree());
+ // Allow access to "1 SOUR @non_existent_source@", so it can be corrected/deleted
+ if (!$source || $source->canShow()) {
+ if ($level > 1) {
+ echo '<tr class="row_sour2">';
+ } else {
+ echo '<tr>';
+ }
+ echo '<td class="descriptionbox';
+ if ($level > 1) {
+ echo ' rela';
+ }
+ echo ' ', $styleadd, ' width20">';
+ $factlines = explode("\n", $factrec); // 1 BIRT Y\n2 SOUR ...
+ $factwords = explode(" ", $factlines[0]); // 1 BIRT Y
+ $factname = $factwords[1]; // BIRT
+ if ($factname == 'EVEN' || $factname == 'FACT') {
+ // Add ' EVEN' to provide sensible output for an event with an empty TYPE record
+ $ct = preg_match("/2 TYPE (.*)/", $factrec, $ematch);
+ if ($ct > 0) {
+ $factname = trim($ematch[1]);
+ echo $factname;
+ } else {
+ echo GedcomTag::getLabel($factname, $parent);
+ }
+ } elseif ($can_edit) {
+ echo "<a onclick=\"return edit_record('$pid', '$fact_id');\" href=\"#\" title=\"", I18N::translate('Edit'), '">';
+ if ($fact->getParent()->getTree()->getPreference('SHOW_FACT_ICONS')) {
+ if ($level == 1) {
+ echo '<i class="icon-source"></i> ';
+ }
+ }
+ echo GedcomTag::getLabel($factname, $parent), '</a>';
+ echo '<div class="editfacts">';
+ if (preg_match('/^@.+@$/', $match[$j][2])) {
+ // Inline sources can't be edited. Attempting to save one will convert it
+ // into a link, and delete it.
+ // e.g. "1 SOUR my source" becomes "1 SOUR @my source@" which does not exist.
+ echo "<div class=\"editlink\"><a class=\"editicon\" onclick=\"return edit_record('$pid', '$fact_id');\" href=\"#\" title=\"" . I18N::translate('Edit') . "\"><span class=\"link_text\">" . I18N::translate('Edit') . "</span></a></div>";
+ echo '<div class="copylink"><a class="copyicon" href="#" onclick="return copy_fact(\'', $pid, '\', \'', $fact_id, '\');" title="' . I18N::translate('Copy') . '"><span class="link_text">' . I18N::translate('Copy') . '</span></a></div>';
+ }
+ echo "<div class=\"deletelink\"><a class=\"deleteicon\" onclick=\"return delete_fact('" . I18N::translate('Are you sure you want to delete this fact?') . "', '$pid', '$fact_id');\" href=\"#\" title=\"" . I18N::translate('Delete') . "\"><span class=\"link_text\">" . I18N::translate('Delete') . "</span></a></div>";
+ echo '</div>';
+ } else {
+ echo GedcomTag::getLabel($factname, $parent);
+ }
+ echo '</td>';
+ echo '<td class="optionbox ', $styleadd, ' wrap">';
+ if ($source) {
+ echo '<a href="', $source->getHtmlUrl(), '">', $source->getFullName(), '</a>';
+ // PUBL
+ $publ = $source->getFirstFact('PUBL');
+ if ($publ) {
+ echo GedcomTag::getLabelValue('PUBL', $publ->getValue());
+ }
+ // 2 RESN tags. Note, there can be more than one, such as "privacy" and "locked"
+ if (preg_match_all("/\n2 RESN (.+)/", $factrec, $rmatches)) {
+ foreach ($rmatches[1] as $rmatch) {
+ echo '<br><span class="label">', GedcomTag::getLabel('RESN'), ':</span> <span class="field">';
+ switch ($rmatch) {
+ case 'none':
+ // Note: "2 RESN none" is not valid gedcom, and the GUI will not let you add it.
+ // However, webtrees privacy rules will interpret it as "show an otherwise private fact to public".
+ echo '<i class="icon-resn-none"></i> ', I18N::translate('Show to visitors');
+ break;
+ case 'privacy':
+ echo '<i class="icon-resn-privacy"></i> ', I18N::translate('Show to members');
+ break;
+ case 'confidential':
+ echo '<i class="icon-resn-confidential"></i> ', I18N::translate('Show to managers');
+ break;
+ case 'locked':
+ echo '<i class="icon-resn-locked"></i> ', I18N::translate('Only managers can edit');
+ break;
+ default:
+ echo $rmatch;
+ break;
+ }
+ echo '</span>';
+ }
+ }
+ $cs = preg_match("/$nlevel EVEN (.*)/", $srec, $cmatch);
+ if ($cs > 0) {
+ echo '<br><span class="label">', GedcomTag::getLabel('EVEN'), ' </span><span class="field">', $cmatch[1], '</span>';
+ $cs = preg_match("/" . ($nlevel + 1) . " ROLE (.*)/", $srec, $cmatch);
+ if ($cs > 0) {
+ echo '<br>&nbsp;&nbsp;&nbsp;&nbsp;<span class="label">', GedcomTag::getLabel('ROLE'), ' </span><span class="field">', $cmatch[1], '</span>';
+ }
+ }
+ echo self::printSourceStructure(self::getSourceStructure($srec));
+ echo '<div class="indent">';
+ self::printMediaLinks($srec, $nlevel);
+ if ($nlevel == 2) {
+ self::printMediaLinks($source->getGedcom(), 1);
+ }
+ echo FunctionsPrint::printFactNotes($srec, $nlevel);
+ if ($nlevel == 2) {
+ echo FunctionsPrint::printFactNotes($source->getGedcom(), 1);
+ }
+ echo '</div>';
+ } else {
+ echo $sid;
+ }
+ echo '</td></tr>';
+ }
+ }
+ }
+
+ /**
+ * Print SOUR structure
+ * This function prints the input array of SOUR sub-records built by the
+ * getSourceStructure() function.
+ *
+ * @param string[] $textSOUR
+ *
+ * @return string
+ */
+ public static function printSourceStructure($textSOUR) {
+ global $WT_TREE;
+ $html = '';
+
+ if ($textSOUR['PAGE']) {
+ $html .= GedcomTag::getLabelValue('PAGE', Filter::expandUrls($textSOUR['PAGE']));
+ }
+
+ if ($textSOUR['EVEN']) {
+ $html .= GedcomTag::getLabelValue('EVEN', Filter::escapeHtml($textSOUR['EVEN']));
+ if ($textSOUR['ROLE']) {
+ $html .= GedcomTag::getLabelValue('ROLE', Filter::escapeHtml($textSOUR['ROLE']));
+ }
+ }
+
+ if ($textSOUR['DATE'] || count($textSOUR['TEXT'])) {
+ if ($textSOUR['DATE']) {
+ $date = new Date($textSOUR['DATE']);
+ $html .= GedcomTag::getLabelValue('DATA:DATE', $date->display());
+ }
+ foreach ($textSOUR['TEXT'] as $text) {
+ $html .= GedcomTag::getLabelValue('TEXT', Filter::formatText($text, $WT_TREE));
+ }
+ }
+
+ if ($textSOUR['QUAY'] != '') {
+ $html .= GedcomTag::getLabelValue('QUAY', GedcomCodeQuay::getValue($textSOUR['QUAY']));
+ }
+
+ return '<div class="indent">' . $html . '</div>';
+ }
+
+ /**
+ * Extract SOUR structure from the incoming Source sub-record
+ * The output array is defined as follows:
+ * $textSOUR['PAGE'] = Source citation
+ * $textSOUR['EVEN'] = Event type
+ * $textSOUR['ROLE'] = Role in event
+ * $textSOUR['DATA'] = place holder (no text in this sub-record)
+ * $textSOUR['DATE'] = Entry recording date
+ * $textSOUR['TEXT'] = (array) Text from source
+ * $textSOUR['QUAY'] = Certainty assessment
+ *
+ * @param string $srec
+ *
+ * @return string[]
+ */
+ public static function getSourceStructure($srec) {
+ // Set up the output array
+ $textSOUR = array(
+ 'PAGE' => '',
+ 'EVEN' => '',
+ 'ROLE' => '',
+ 'DATA' => '',
+ 'DATE' => '',
+ 'TEXT' => array(),
+ 'QUAY' => '',
+ );
+
+ if ($srec) {
+ $subrecords = explode("\n", $srec);
+ for ($i = 0; $i < count($subrecords); $i++) {
+ $tag = substr($subrecords[$i], 2, 4);
+ $text = substr($subrecords[$i], 7);
+ $i++;
+ for (; $i < count($subrecords); $i++) {
+ $nextTag = substr($subrecords[$i], 2, 4);
+ if ($nextTag != 'CONT') {
+ $i--;
+ break;
+ }
+ if ($nextTag == 'CONT') {
+ $text .= "\n";
+ }
+ $text .= rtrim(substr($subrecords[$i], 7));
+ }
+ if ($tag == 'TEXT') {
+ $textSOUR[$tag][] = $text;
+ } else {
+ $textSOUR[$tag] = $text;
+ }
+ }
+ }
+
+ return $textSOUR;
+ }
+
+ /**
+ * Print a row for the notes tab on the individual page.
+ *
+ * @param Fact $fact
+ * @param int $level
+ */
+ public static function printMainNotes(Fact $fact, $level) {
+ $factrec = $fact->getGedcom();
+ $fact_id = $fact->getFactId();
+ $parent = $fact->getParent();
+ $pid = $parent->getXref();
+
+ if ($fact->isPendingAddition()) {
+ $styleadd = ' new';
+ $can_edit = $level == 1 && $fact->canEdit();
+ } elseif ($fact->isPendingDeletion()) {
+ $styleadd = ' old';
+ $can_edit = false;
+ } else {
+ $styleadd = '';
+ $can_edit = $level == 1 && $fact->canEdit();
+ }
+
+ $ct = preg_match_all("/$level NOTE (.*)/", $factrec, $match, PREG_SET_ORDER);
+ for ($j = 0; $j < $ct; $j++) {
+ // Note object, or inline note?
+ if (preg_match("/$level NOTE @(.*)@/", $match[$j][0], $nmatch)) {
+ $note = Note::getInstance($nmatch[1], $fact->getParent()->getTree());
+ if ($note && !$note->canShow()) {
+ continue;
+ }
+ } else {
+ $note = null;
+ }
+
+ if ($level >= 2) {
+ echo '<tr class="row_note2"><td class="descriptionbox rela ', $styleadd, ' width20">';
+ } else {
+ echo '<tr><td class="descriptionbox ', $styleadd, ' width20">';
+ }
+ if ($can_edit) {
+ echo '<a onclick="return edit_record(\'', $pid, '\', \'', $fact_id, '\');" href="#" title="', I18N::translate('Edit'), '">';
+ if ($level < 2) {
+ if ($fact->getParent()->getTree()->getPreference('SHOW_FACT_ICONS')) {
+ echo '<i class="icon-note"></i> ';
+ }
+ if ($note) {
+ echo GedcomTag::getLabel('SHARED_NOTE');
+ } else {
+ echo GedcomTag::getLabel('NOTE');
+ }
+ echo '</a>';
+ echo '<div class="editfacts">';
+ echo "<div class=\"editlink\"><a class=\"editicon\" onclick=\"return edit_record('$pid', '$fact_id');\" href=\"#\" title=\"" . I18N::translate('Edit') . "\"><span class=\"link_text\">" . I18N::translate('Edit') . "</span></a></div>";
+ echo '<div class="copylink"><a class="copyicon" href="#" onclick="return copy_fact(\'', $pid, '\', \'', $fact_id, '\');" title="' . I18N::translate('Copy') . '"><span class="link_text">' . I18N::translate('Copy') . '</span></a></div>';
+ echo "<div class=\"deletelink\"><a class=\"deleteicon\" onclick=\"return delete_fact('" . I18N::translate('Are you sure you want to delete this fact?') . "', '$pid', '$fact_id');\" href=\"#\" title=\"" . I18N::translate('Delete') . "\"><span class=\"link_text\">" . I18N::translate('Delete') . "</span></a></div>";
+ if ($note) {
+ echo '<a class="icon-note" href="', $note->getHtmlUrl(), '" title="' . I18N::translate('View') . '"><span class="link_text">' . I18N::translate('View') . '</span></a>';
+ }
+ echo '</div>';
+ }
+ } else {
+ if ($level < 2) {
+ if ($fact->getParent()->getTree()->getPreference('SHOW_FACT_ICONS')) {
+ echo '<i class="icon-note"></i> ';
+ }
+ if ($note) {
+ echo GedcomTag::getLabel('SHARED_NOTE');
+ } else {
+ echo GedcomTag::getLabel('NOTE');
+ }
+ }
+ $factlines = explode("\n", $factrec); // 1 BIRT Y\n2 NOTE ...
+ $factwords = explode(" ", $factlines[0]); // 1 BIRT Y
+ $factname = $factwords[1]; // BIRT
+ $parent = GedcomRecord::getInstance($pid, $fact->getParent()->getTree());
+ if ($factname == 'EVEN' || $factname == 'FACT') {
+ // Add ' EVEN' to provide sensible output for an event with an empty TYPE record
+ $ct = preg_match("/2 TYPE (.*)/", $factrec, $ematch);
+ if ($ct > 0) {
+ $factname = trim($ematch[1]);
+ echo $factname;
+ } else {
+ echo GedcomTag::getLabel($factname, $parent);
+ }
+ } elseif ($factname != 'NOTE') {
+ // Note is already printed
+ echo GedcomTag::getLabel($factname, $parent);
+ if ($note) {
+ echo '<div class="editfacts"><a class="icon-note" href="', $note->getHtmlUrl(), '" title="' . I18N::translate('View') . '"><span class="link_text">' . I18N::translate('View') . '</span></a></div>';
+
+ }
+ }
+ }
+ echo '</td>';
+ if ($note) {
+ // Note objects
+ if (Module::getModuleByName('GEDFact_assistant')) {
+ // If Census assistant installed, allow it to format the note
+ $text = CensusAssistantModule::formatCensusNote($note);
+ } else {
+ $text = Filter::formatText($note->getNote(), $fact->getParent()->getTree());
+ }
+ } else {
+ // Inline notes
+ $nrec = Functions::getSubRecord($level, "$level NOTE", $factrec, $j + 1);
+ $text = $match[$j][1] . Functions::getCont($level + 1, $nrec);
+ $text = Filter::formatText($text, $fact->getParent()->getTree());
+ }
+
+ echo '<td class="optionbox', $styleadd, ' wrap">';
+ echo $text;
+
+ if (!empty($noterec)) {
+ echo FunctionsPrintFacts::printFactSources($noterec, 1);
+ }
+
+ // 2 RESN tags. Note, there can be more than one, such as "privacy" and "locked"
+ if (preg_match_all("/\n2 RESN (.+)/", $factrec, $matches)) {
+ foreach ($matches[1] as $match) {
+ echo '<br><span class="label">', GedcomTag::getLabel('RESN'), ':</span> <span class="field">';
+ switch ($match) {
+ case 'none':
+ // Note: "2 RESN none" is not valid gedcom, and the GUI will not let you add it.
+ // However, webtrees privacy rules will interpret it as "show an otherwise private fact to public".
+ echo '<i class="icon-resn-none"></i> ', I18N::translate('Show to visitors');
+ break;
+ case 'privacy':
+ echo '<i class="icon-resn-privacy"></i> ', I18N::translate('Show to members');
+ break;
+ case 'confidential':
+ echo '<i class="icon-resn-confidential"></i> ', I18N::translate('Show to managers');
+ break;
+ case 'locked':
+ echo '<i class="icon-resn-locked"></i> ', I18N::translate('Only managers can edit');
+ break;
+ default:
+ echo $match;
+ break;
+ }
+ echo '</span>';
+ }
+ }
+ echo '</td></tr>';
+ }
+ }
+
+ /**
+ * Print a row for the media tab on the individual page.
+ *
+ * @param Fact $fact
+ * @param int $level
+ */
+ public static function printMainMedia(Fact $fact, $level) {
+ $factrec = $fact->getGedcom();
+ $parent = $fact->getParent();
+
+ if ($fact->isPendingAddition()) {
+ $styleadd = 'new';
+ $can_edit = $level == 1 && $fact->canEdit();
+ } elseif ($fact->isPendingDeletion()) {
+ $styleadd = 'old';
+ $can_edit = false;
+ } else {
+ $styleadd = '';
+ $can_edit = $level == 1 && $fact->canEdit();
+ }
+
+ // -- find source for each fact
+ preg_match_all('/(?:^|\n)' . $level . ' OBJE @(.*)@/', $factrec, $matches);
+ foreach ($matches[1] as $xref) {
+ $media = Media::getInstance($xref, $fact->getParent()->getTree());
+ // Allow access to "1 OBJE @non_existent_source@", so it can be corrected/deleted
+ if (!$media || $media->canShow()) {
+ if ($level > 1) {
+ echo '<tr class="row_obje2">';
+ } else {
+ echo '<tr>';
+ }
+ echo '<td class="descriptionbox';
+ if ($level > 1) {
+ echo ' rela';
+ }
+ echo ' ', $styleadd, ' width20">';
+ preg_match("/^\d (\w*)/", $factrec, $factname);
+ $factlines = explode("\n", $factrec); // 1 BIRT Y\n2 SOUR ...
+ $factwords = explode(" ", $factlines[0]); // 1 BIRT Y
+ $factname = $factwords[1]; // BIRT
+ if ($factname == 'EVEN' || $factname == 'FACT') {
+ // Add ' EVEN' to provide sensible output for an event with an empty TYPE record
+ $ct = preg_match("/2 TYPE (.*)/", $factrec, $ematch);
+ if ($ct > 0) {
+ $factname = $ematch[1];
+ echo $factname;
+ } else {
+ echo GedcomTag::getLabel($factname, $parent);
+ }
+ } elseif ($can_edit) {
+ echo '<a onclick="window.open(\'addmedia.php?action=editmedia&amp;pid=', $media->getXref(), '\', \'_blank\', edit_window_specs); return false;" href="#" title="', I18N::translate('Edit'), '">';
+ echo GedcomTag::getLabel($factname, $parent), '</a>';
+ echo '<div class="editfacts">';
+ echo '<div class="editlink"><a class="editicon" onclick="window.open(\'addmedia.php?action=editmedia&amp;pid=', $media->getXref(), '\', \'_blank\', edit_window_specs); return false;" href="#" title="', I18N::translate('Edit'), '"><span class="link_text">', I18N::translate('Edit'), '</span></a></div>';
+ echo '<div class="copylink"><a class="copyicon" href="#" onclick="jQuery.post(\'action.php\',{action:\'copy-fact\', type:\'\', factgedcom:\'' . rawurlencode($factrec) . '\'},function(){location.reload();})" title="' . I18N::translate('Copy') . '"><span class="link_text">' . I18N::translate('Copy') . '</span></a></div>';
+ echo '<div class="deletelink"><a class="deleteicon" onclick="return delete_fact(\'', I18N::translate('Are you sure you want to delete this fact?'), '\', \'', $parent->getXref(), '\', \'', $fact->getFactId(), '\');" href="#" title="', I18N::translate('Delete'), '"><span class="link_text">', I18N::translate('Delete'), '</span></a></div>';
+ echo '</div>';
+ } else {
+ echo GedcomTag::getLabel($factname, $parent);
+ }
+ echo '</td>';
+ echo '<td class="optionbox ', $styleadd, ' wrap">';
+ if ($media) {
+ echo '<span class="field">';
+ echo $media->displayImage();
+ echo '<a href="' . $media->getHtmlUrl() . '">';
+ echo '<em>';
+ foreach ($media->getAllNames() as $name) {
+ if ($name['type'] != 'TITL') {
+ echo '<br>';
+ }
+ echo $name['full'];
+ }
+ echo '</em>';
+ echo '</a>';
+ echo '</span>';
+
+ echo GedcomTag::getLabelValue('FORM', $media->mimeType());
+ $imgsize = $media->getImageAttributes('main');
+ if (!empty($imgsize['WxH'])) {
+ echo GedcomTag::getLabelValue('__IMAGE_SIZE__', $imgsize['WxH']);
+ }
+ if ($media->getFilesizeraw() > 0) {
+ echo GedcomTag::getLabelValue('__FILE_SIZE__', $media->getFilesize());
+ }
+ $mediatype = $media->getMediaType();
+ if ($mediatype) {
+ echo GedcomTag::getLabelValue('TYPE', GedcomTag::getFileFormTypeValue($mediatype));
+ }
+
+ switch ($media->isPrimary()) {
+ case 'Y':
+ echo GedcomTag::getLabelValue('_PRIM', I18N::translate('yes'));
+ break;
+ case 'N':
+ echo GedcomTag::getLabelValue('_PRIM', I18N::translate('no'));
+ break;
+ }
+ echo FunctionsPrint::printFactNotes($media->getGedcom(), 1);
+ echo FunctionsPrintFacts::printFactSources($media->getGedcom(), 1);
+ } else {
+ echo $xref;
+ }
+ echo '</td></tr>';
+ }
+ }
+ }
+}
diff --git a/app/Functions/FunctionsPrintLists.php b/app/Functions/FunctionsPrintLists.php
new file mode 100644
index 0000000000..912c2b88e9
--- /dev/null
+++ b/app/Functions/FunctionsPrintLists.php
@@ -0,0 +1,2297 @@
+<?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\Fact;
+use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\GedcomTag;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Media;
+use Fisharebest\Webtrees\Note;
+use Fisharebest\Webtrees\Place;
+use Fisharebest\Webtrees\Repository;
+use Fisharebest\Webtrees\Source;
+use Fisharebest\Webtrees\Stats;
+use Fisharebest\Webtrees\Tree;
+use Rhumsaa\Uuid\Uuid;
+
+/**
+ * Class FunctionsPrintLists - common functions
+ */
+class FunctionsPrintLists {
+ /**
+ * Print a table of individuals
+ *
+ * @param Individual[] $datalist
+ * @param string $option
+ *
+ * @return string
+ */
+ public static function individualTable($datalist, $option = '') {
+ global $controller, $WT_TREE;
+
+ $table_id = 'table-indi-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
+
+ $controller
+ ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
+ ->addInlineJavascript('
+ jQuery.fn.dataTableExt.oSort["unicode-asc" ]=function(a,b) {return a.replace(/<[^<]*>/, "").localeCompare(b.replace(/<[^<]*>/, ""))};
+ jQuery.fn.dataTableExt.oSort["unicode-desc" ]=function(a,b) {return b.replace(/<[^<]*>/, "").localeCompare(a.replace(/<[^<]*>/, ""))};
+ jQuery.fn.dataTableExt.oSort["num-html-asc" ]=function(a,b) {a=parseFloat(a.replace(/<[^<]*>/, "")); b=parseFloat(b.replace(/<[^<]*>/, "")); return (a<b) ? -1 : (a>b ? 1 : 0);};
+ jQuery.fn.dataTableExt.oSort["num-html-desc"]=function(a,b) {a=parseFloat(a.replace(/<[^<]*>/, "")); b=parseFloat(b.replace(/<[^<]*>/, "")); return (a>b) ? -1 : (a<b ? 1 : 0);};
+ jQuery("#' . $table_id . '").dataTable( {
+ dom: \'<"H"<"filtersH_' . $table_id . '">T<"dt-clear">pf<"dt-clear">irl>t<"F"pl<"dt-clear"><"filtersF_' . $table_id . '">>\',
+ ' . I18N::datatablesI18N() . ',
+ jQueryUI: true,
+ autoWidth: false,
+ processing: true,
+ retrieve: true,
+ columns: [
+ /* 0 givn */ { dataSort: 2 },
+ /* 1 surn */ { dataSort: 3 },
+ /* 2 GIVN,SURN */ { type: "unicode", visible: false },
+ /* 3 SURN,GIVN */ { type: "unicode", visible: false },
+ /* 4 sosa */ { dataSort: 5, class: "center", visible: ' . ($option == 'sosa' ? 'true' : 'false') . ' },
+ /* 5 SOSA */ { type: "num", visible: false },
+ /* 6 birt date */ { dataSort: 7 },
+ /* 7 BIRT:DATE */ { visible: false },
+ /* 8 anniv */ { dataSort: 7, class: "center" },
+ /* 9 birt plac */ { type: "unicode" },
+ /* 10 children */ { dataSort: 11, class: "center" },
+ /* 11 children */ { type: "num", visible: false },
+ /* 12 deat date */ { dataSort: 13 },
+ /* 13 DEAT:DATE */ { visible: false },
+ /* 14 anniv */ { dataSort: 13, class: "center" },
+ /* 15 age */ { dataSort: 16, class: "center" },
+ /* 16 AGE */ { type: "num", visible: false },
+ /* 17 deat plac */ { type: "unicode" },
+ /* 18 CHAN */ { dataSort: 19, visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
+ /* 19 CHAN_sort */ { visible: false },
+ /* 20 SEX */ { visible: false },
+ /* 21 BIRT */ { visible: false },
+ /* 22 DEAT */ { visible: false },
+ /* 23 TREE */ { visible: false }
+ ],
+ sorting: [[' . ($option == 'sosa' ? '4, "asc"' : '1, "asc"') . ']],
+ displayLength: 20,
+ pagingType: "full_numbers"
+ });
+
+ jQuery("#' . $table_id . '")
+ /* Hide/show parents */
+ .on("click", ".btn-toggle-parents", function() {
+ jQuery(this).toggleClass("ui-state-active");
+ jQuery(".parents", jQuery(this).closest("table").DataTable().rows().nodes()).slideToggle();
+ })
+ /* Hide/show statistics */
+ .on("click", ".btn-toggle-statistics", function() {
+ jQuery(this).toggleClass("ui-state-active");
+ jQuery("#indi_list_table-charts_' . $table_id . '").slideToggle();
+ })
+ /* Filter buttons in table header */
+ .on("click", "button[data-filter-column]", function() {
+ var btn = jQuery(this);
+ // De-activate the other buttons in this button group
+ btn.siblings().removeClass("ui-state-active");
+ // Apply (or clear) this filter
+ var col = jQuery("#' . $table_id . '").DataTable().column(btn.data("filter-column"));
+ if (btn.hasClass("ui-state-active")) {
+ btn.removeClass("ui-state-active");
+ col.search("").draw();
+ } else {
+ btn.addClass("ui-state-active");
+ col.search(btn.data("filter-value")).draw();
+ }
+ });
+
+ jQuery(".indi-list").css("visibility", "visible");
+ jQuery(".loading-image").css("display", "none");
+ ');
+
+ $stats = new Stats($WT_TREE);
+
+ // Bad data can cause "longest life" to be huge, blowing memory limits
+ $max_age = min($WT_TREE->getPreference('MAX_ALIVE_AGE'), $stats->LongestLifeAge()) + 1;
+
+ // Inititialise chart data
+ $deat_by_age = array();
+ for ($age = 0; $age <= $max_age; $age++) {
+ $deat_by_age[$age] = '';
+ }
+ $birt_by_decade = array();
+ $deat_by_decade = array();
+ for ($year = 1550; $year < 2030; $year += 10) {
+ $birt_by_decade[$year] = '';
+ $deat_by_decade[$year] = '';
+ }
+
+ $html = '
+ <div class="loading-image">&nbsp;</div>
+ <div class="indi-list">
+ <table id="' . $table_id . '">
+ <thead>
+ <tr>
+ <th colspan="24">
+ <div class="btn-toolbar">
+ <div class="btn-group">
+ <button
+ class="ui-state-default"
+ data-filter-column="20"
+ data-filter-value="M"
+ title="' . I18N::translate('Show only males.') . '"
+ type="button"
+ >
+ ' . Individual::sexImage('M', 'large') . '
+ </button>
+ <button
+ class="ui-state-default"
+ data-filter-column="20"
+ data-filter-value="F"
+ title="' . I18N::translate('Show only females.') . '"
+ type="button"
+ >
+ ' . Individual::sexImage('F', 'large') . '
+ </button>
+ <button
+ class="ui-state-default"
+ data-filter-column="20"
+ data-filter-value="U"
+ title="' . I18N::translate('Show only individuals for whom the gender is not known.') . '"
+ type="button"
+ >
+ ' . Individual::sexImage('U', 'large') . '
+ </button>
+ </div>
+ <div class="btn-group">
+ <button
+ class="ui-state-default"
+ data-filter-column="22"
+ data-filter-value="N"
+ title="' . I18N::translate('Show individuals who are alive or couples where both partners are alive.') . '"
+ type="button"
+ >
+ ' . I18N::translate('Alive') . '
+ </button>
+ <button
+ class="ui-state-default"
+ data-filter-column="22"
+ data-filter-value="Y"
+ title="' . I18N::translate('Show individuals who are dead or couples where both partners are deceased.') . '"
+ type="button"
+ >
+ ' . I18N::translate('Dead') . '
+ </button>
+ <button
+ class="ui-state-default"
+ data-filter-column="22"
+ data-filter-value="YES"
+ title="' . I18N::translate('Show individuals who died more than 100 years ago.') . '"
+ type="button"
+ >
+ ' . GedcomTag::getLabel('DEAT') . '&gt;100
+ </button>
+ <button
+ class="ui-state-default"
+ data-filter-column="22"
+ data-filter-value="Y100"
+ title="' . I18N::translate('Show individuals who died within the last 100 years.') . '"
+ type="button"
+ >
+ ' . GedcomTag::getLabel('DEAT') . '&lt;=100
+ </button>
+ </div>
+ <div class="btn-group">
+ <button
+ class="ui-state-default"
+ data-filter-column="21"
+ data-filter-value="YES"
+ title="' . I18N::translate('Show individuals born more than 100 years ago.') . '"
+ type="button"
+ >
+ ' . GedcomTag::getLabel('BIRT') . '&gt;100
+ </button>
+ <button
+ class="ui-state-default"
+ data-filter-column="21"
+ data-filter-value="Y100"
+ title="' . I18N::translate('Show individuals born within the last 100 years.') . '"
+ type="button"
+ >
+ ' . GedcomTag::getLabel('BIRT') . '&lt;=100
+ </button>
+ </div>
+ <div class="btn-group">
+ <button
+ class="ui-state-default"
+ data-filter-column="23"
+ data-filter-value="R"
+ title="' . I18N::translate('Show “roots” couples or individuals. These individuals may also be called “patriarchs”. They are individuals who have no parents recorded in the database.') . '"
+ type="button"
+ >
+ ' . I18N::translate('Roots') . '
+ </button>
+ <button
+ class="ui-state-default"
+ data-filter-column="23"
+ data-filter-value="L"
+ title="' . I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') . '"
+ type="button"
+ >
+ ' . I18N::translate('Leaves') . '
+ </button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr>
+ <th>' . GedcomTag::getLabel('GIVN') . '</th>
+ <th>' . GedcomTag::getLabel('SURN') . '</th>
+ <th>GIVN</th>
+ <th>SURN</th>
+ <th>' . /* I18N: Abbreviation for “Sosa-Stradonitz number”. This is an individual’s surname, so may need transliterating into non-latin alphabets. */
+ I18N::translate('Sosa') . '</th>
+ <th>SOSA</th>
+ <th>' . GedcomTag::getLabel('BIRT') . '</th>
+ <th>SORT_BIRT</th>
+ <th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
+ <th>' . GedcomTag::getLabel('PLAC') . '</th>
+ <th><i class="icon-children" title="' . I18N::translate('Children') . '"></i></th>
+ <th>NCHI</th>
+ <th>' . GedcomTag::getLabel('DEAT') . '</th>
+ <th>SORT_DEAT</th>
+ <th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
+ <th>' . GedcomTag::getLabel('AGE') . '</th>
+ <th>AGE</th>
+ <th>' . GedcomTag::getLabel('PLAC') . '</th>
+ <th>' . GedcomTag::getLabel('CHAN') . '</th>
+ <th>CHAN</th>
+ <th>SEX</th>
+ <th>BIRT</th>
+ <th>DEAT</th>
+ <th>TREE</th>
+ </tr>
+ </thead>
+ <tfoot>
+ <tr>
+ <th colspan="24">
+ <div class="btn-toolbar">
+ <div class="btn-group">
+ <button type="button" class="ui-state-default btn-toggle-parents">
+ ' . I18N::translate('Show parents') . '
+ </button>
+ <button type="button" class="ui-state-default btn-toggle-statistics">
+ ' . I18N::translate('Show statistics charts') . '
+ </button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ </tfoot>
+ <tbody>';
+
+ $d100y = new Date(date('Y') - 100); // 100 years ago
+ $unique_indis = array(); // Don't double-count indis with multiple names.
+ foreach ($datalist as $key => $person) {
+ if (!$person->canShowName()) {
+ continue;
+ }
+ if ($person->isPendingAddtion()) {
+ $class = ' class="new"';
+ } elseif ($person->isPendingDeletion()) {
+ $class = ' class="old"';
+ } else {
+ $class = '';
+ }
+ $html .= '<tr' . $class . '>';
+ //-- Indi name(s)
+ $html .= '<td colspan="2">';
+ foreach ($person->getAllNames() as $num => $name) {
+ if ($name['type'] == 'NAME') {
+ $title = '';
+ } else {
+ $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $person)) . '"';
+ }
+ if ($num == $person->getPrimaryName()) {
+ $class = ' class="name2"';
+ $sex_image = $person->getSexImage();
+ list($surn, $givn) = explode(',', $name['sort']);
+ } else {
+ $class = '';
+ $sex_image = '';
+ }
+ $html .= '<a ' . $title . ' href="' . $person->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
+ }
+ // Indi parents
+ $html .= $person->getPrimaryParentsNames('parents details1', 'none');
+ $html .= '</td>';
+ // Dummy column to match colspan in header
+ $html .= '<td style="display:none;"></td>';
+ //-- GIVN/SURN
+ // Use "AAAA" as a separator (instead of ",") as Javascript.localeCompare() ignores
+ // punctuation and "ANN,ROACH" would sort after "ANNE,ROACH", instead of before it.
+ // Similarly, @N.N. would sort as NN.
+ $html .= '<td>' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . 'AAAA' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . '</td>';
+ $html .= '<td>' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . 'AAAA' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . '</td>';
+ //-- SOSA
+ if ($option == 'sosa') {
+ $html .= '<td><a href="../../relationship.php?pid1=' . $datalist[1] . '&amp;pid2=' . $person->getXref() . '" title="' . I18N::translate('Relationships') . '">' . I18N::number($key) . '</a></td><td>' . $key . '</td>';
+ } else {
+ $html .= '<td></td><td>0</td>';
+ }
+ //-- Birth date
+ $html .= '<td>';
+ if ($birth_dates = $person->getAllBirthDates()) {
+ foreach ($birth_dates as $num => $birth_date) {
+ if ($num) {
+ $html .= '<br>';
+ }
+ $html .= $birth_date->display(true);
+ }
+ if ($birth_dates[0]->gregorianYear() >= 1550 && $birth_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$person->getXref()])) {
+ $birt_by_decade[(int) ($birth_dates[0]->gregorianYear() / 10) * 10] .= $person->getSex();
+ }
+ } else {
+ $birth_date = $person->getEstimatedBirthDate();
+ if ($person->getTree()->getPreference('SHOW_EST_LIST_DATES')) {
+ $html .= $birth_date->display(true);
+ } else {
+ $html .= '&nbsp;';
+ }
+ $birth_dates[0] = new Date('');
+ }
+ $html .= '</td>';
+ //-- Event date (sortable)hidden by datatables code
+ $html .= '<td>' . $birth_date->julianDay() . '</td>';
+ //-- Birth anniversary
+ $html .= '<td>' . Date::getAge($birth_dates[0], null, 2) . '</td>';
+ //-- Birth place
+ $html .= '<td>';
+ foreach ($person->getAllBirthPlaces() as $n => $birth_place) {
+ $tmp = new Place($birth_place, $person->getTree());
+ if ($n) {
+ $html .= '<br>';
+ }
+ $html .= '<a href="' . $tmp->getURL() . '" title="' . strip_tags($tmp->getFullName()) . '">';
+ $html .= FunctionsPrint::highlightSearchHits($tmp->getShortName()) . '</a>';
+ }
+ $html .= '</td>';
+ //-- Number of children
+ $nchi = $person->getNumberOfChildren();
+ $html .= '<td>' . I18N::number($nchi) . '</td><td>' . $nchi . '</td>';
+ //-- Death date
+ $html .= '<td>';
+ if ($death_dates = $person->getAllDeathDates()) {
+ foreach ($death_dates as $num => $death_date) {
+ if ($num) {
+ $html .= '<br>';
+ }
+ $html .= $death_date->display(true);
+ }
+ if ($death_dates[0]->gregorianYear() >= 1550 && $death_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$person->getXref()])) {
+ $deat_by_decade[(int) ($death_dates[0]->gregorianYear() / 10) * 10] .= $person->getSex();
+ }
+ } else {
+ $death_date = $person->getEstimatedDeathDate();
+ // Estimated death dates are a fixed number of years after the birth date.
+ // Don't show estimates in the future.
+ if ($person->getTree()->getPreference('SHOW_EST_LIST_DATES') && $death_date->minimumJulianDay() < WT_CLIENT_JD) {
+ $html .= $death_date->display(true);
+ } elseif ($person->isDead()) {
+ $html .= I18N::translate('yes');
+ } else {
+ $html .= '&nbsp;';
+ }
+ $death_dates[0] = new Date('');
+ }
+ $html .= '</td>';
+ //-- Event date (sortable)hidden by datatables code
+ $html .= '<td>' . $death_date->julianDay() . '</td>';
+ //-- Death anniversary
+ $html .= '<td>' . Date::getAge($death_dates[0], null, 2) . '</td>';
+ //-- Age at death
+ $age = Date::getAge($birth_dates[0], $death_dates[0], 0);
+ if (!isset($unique_indis[$person->getXref()]) && $age >= 0 && $age <= $max_age) {
+ $deat_by_age[$age] .= $person->getSex();
+ }
+ // Need both display and sortable age
+ $html .= '<td>' . Date::getAge($birth_dates[0], $death_dates[0], 2) . '</td><td>' . Date::getAge($birth_dates[0], $death_dates[0], 1) . '</td>';
+ //-- Death place
+ $html .= '<td>';
+ foreach ($person->getAllDeathPlaces() as $n => $death_place) {
+ $tmp = new Place($death_place, $person->getTree());
+ if ($n) {
+ $html .= '<br>';
+ }
+ $html .= '<a href="' . $tmp->getURL() . '" title="' . strip_tags($tmp->getFullName()) . '">';
+ $html .= FunctionsPrint::highlightSearchHits($tmp->getShortName()) . '</a>';
+ }
+ $html .= '</td>';
+ //-- Last change
+ $html .= '<td>' . $person->lastChangeTimestamp() . '</td>';
+ $html .= '<td>' . $person->lastChangeTimestamp(true) . '</td>';
+ //-- Sorting by gender
+ $html .= '<td>' . $person->getSex() . '</td>';
+ //-- Filtering by birth date
+ $html .= '<td>';
+ if (!$person->canShow() || Date::compare($birth_date, $d100y) > 0) {
+ $html .= 'Y100';
+ } else {
+ $html .= 'YES';
+ }
+ $html .= '</td>';
+ //-- Filtering by death date
+ $html .= '<td>';
+ // Died in last 100 years? Died? Not dead?
+ if (Date::compare($death_dates[0], $d100y) > 0) {
+ $html .= 'Y100';
+ } elseif ($death_dates[0]->minimumJulianDay() || $person->isDead()) {
+ $html .= 'YES';
+ } else {
+ $html .= 'N';
+ }
+ $html .= '</td>';
+ //-- Roots or Leaves ?
+ $html .= '<td>';
+ if (!$person->getChildFamilies()) {
+ $html .= 'R';
+ } // roots
+ elseif (!$person->isDead() && $person->getNumberOfChildren() < 1) {
+ $html .= 'L';
+ } // leaves
+ else {
+ $html .= '&nbsp;';
+ }
+ $html .= '</td>';
+ $html .= '</tr>';
+ $unique_indis[$person->getXref()] = true;
+ }
+ $html .= '
+ </tbody>
+ </table>
+ <div id="indi_list_table-charts_' . $table_id . '" style="display:none">
+ <table class="list-charts">
+ <tr>
+ <td>
+ ' . self::chartByDecade($birt_by_decade, I18N::translate('Decade of birth')) . '
+ </td>
+ <td>
+ ' . self::chartByDecade($deat_by_decade, I18N::translate('Decade of death')) . '
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ ' . self::chartByAge($deat_by_age, I18N::translate('Age related to death year')) . '
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>';
+
+ return $html;
+ }
+
+ /**
+ * Print a table of families
+ *
+ * @param Family[] $datalist
+ *
+ * @return string
+ */
+ public static function familyTable($datalist) {
+ global $WT_TREE, $controller;
+
+ $table_id = 'table-fam-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
+
+ $controller
+ ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
+ ->addInlineJavascript('
+ jQuery.fn.dataTableExt.oSort["unicode-asc" ]=function(a,b) {return a.replace(/<[^<]*>/, "").localeCompare(b.replace(/<[^<]*>/, ""))};
+ jQuery.fn.dataTableExt.oSort["unicode-desc"]=function(a,b) {return b.replace(/<[^<]*>/, "").localeCompare(a.replace(/<[^<]*>/, ""))};
+ jQuery("#' . $table_id . '").dataTable( {
+ dom: \'<"H"<"filtersH_' . $table_id . '"><"dt-clear">pf<"dt-clear">irl>t<"F"pl<"dt-clear"><"filtersF_' . $table_id . '">>\',
+ ' . I18N::datatablesI18N() . ',
+ jQueryUI: true,
+ autoWidth: false,
+ processing: true,
+ retrieve: true,
+ columns: [
+ /* 0 husb givn */ {dataSort: 2},
+ /* 1 husb surn */ {dataSort: 3},
+ /* 2 GIVN,SURN */ {type: "unicode", visible: false},
+ /* 3 SURN,GIVN */ {type: "unicode", visible: false},
+ /* 4 age */ {dataSort: 5, class: "center"},
+ /* 5 AGE */ {type: "num", visible: false},
+ /* 6 wife givn */ {dataSort: 8},
+ /* 7 wife surn */ {dataSort: 9},
+ /* 8 GIVN,SURN */ {type: "unicode", visible: false},
+ /* 9 SURN,GIVN */ {type: "unicode", visible: false},
+ /* 10 age */ {dataSort: 11, class: "center"},
+ /* 11 AGE */ {type: "num", visible: false},
+ /* 12 marr date */ {dataSort: 13},
+ /* 13 MARR:DATE */ {visible: false},
+ /* 14 anniv */ {dataSort: 13, class: "center"},
+ /* 15 marr plac */ {type: "unicode"},
+ /* 16 children */ {dataSort: 17, class: "center"},
+ /* 17 NCHI */ {type: "num", visible: false},
+ /* 18 CHAN */ {dataSort: 19, visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . '},
+ /* 19 CHAN_sort */ {visible: false},
+ /* 20 MARR */ {visible: false},
+ /* 21 DEAT */ {visible: false},
+ /* 22 TREE */ {visible: false}
+ ],
+ sorting: [[1, "asc"]],
+ displayLength: 20,
+ pagingType: "full_numbers"
+ });
+
+ jQuery("#' . $table_id . '")
+ /* Hide/show parents */
+ .on("click", ".btn-toggle-parents", function() {
+ jQuery(this).toggleClass("ui-state-active");
+ jQuery(".parents", jQuery(this).closest("table").DataTable().rows().nodes()).slideToggle();
+ })
+ /* Hide/show statistics */
+ .on("click", ".btn-toggle-statistics", function() {
+ jQuery(this).toggleClass("ui-state-active");
+ jQuery("#fam_list_table-charts_' . $table_id . '").slideToggle();
+ })
+ /* Filter buttons in table header */
+ .on("click", "button[data-filter-column]", function() {
+ var btn = $(this);
+ // De-activate the other buttons in this button group
+ btn.siblings().removeClass("ui-state-active");
+ // Apply (or clear) this filter
+ var col = jQuery("#' . $table_id . '").DataTable().column(btn.data("filter-column"));
+ if (btn.hasClass("ui-state-active")) {
+ btn.removeClass("ui-state-active");
+ col.search("").draw();
+ } else {
+ btn.addClass("ui-state-active");
+ col.search(btn.data("filter-value")).draw();
+ }
+ });
+
+ jQuery(".fam-list").css("visibility", "visible");
+ jQuery(".loading-image").css("display", "none");
+ ');
+
+ $stats = new Stats($WT_TREE);
+ $max_age = max($stats->oldestMarriageMaleAge(), $stats->oldestMarriageFemaleAge()) + 1;
+
+ //-- init chart data
+ $marr_by_age = array();
+ for ($age = 0; $age <= $max_age; $age++) {
+ $marr_by_age[$age] = '';
+ }
+ $birt_by_decade = array();
+ $marr_by_decade = array();
+ for ($year = 1550; $year < 2030; $year += 10) {
+ $birt_by_decade[$year] = '';
+ $marr_by_decade[$year] = '';
+ }
+
+ $html = '
+ <div class="loading-image">&nbsp;</div>
+ <div class="fam-list">
+ <table id="' . $table_id . '">
+ <thead>
+ <tr>
+ <th colspan="23">
+ <div class="btn-toolbar">
+ <div class="btn-group">
+ <button
+ type="button"
+ data-filter-column="21"
+ data-filter-value="N"
+ class="ui-state-default"
+ title="' . I18N::translate('Show individuals who are alive or couples where both partners are alive.') . '"
+ >
+ ' . I18N::translate('Both alive') . '
+ </button>
+ <button
+ type="button"
+ data-filter-column="21"
+ data-filter-value="W"
+ class="ui-state-default"
+ title="' . I18N::translate('Show couples where only the female partner is deceased.') . '"
+ >
+ ' . I18N::translate('Widower') . '
+ </button>
+ <button
+ type="button"
+ data-filter-column="21"
+ data-filter-value="H"
+ class="ui-state-default"
+ title="' . I18N::translate('Show couples where only the male partner is deceased.') . '"
+ >
+ ' . I18N::translate('Widow') . '
+ </button>
+ <button
+ type="button"
+ data-filter-column="21"
+ data-filter-value="Y"
+ class="ui-state-default"
+ title="' . I18N::translate('Show individuals who are dead or couples where both partners are deceased.') . '"
+ >
+ ' . I18N::translate('Both dead') . '
+ </button>
+ </div>
+ <div class="btn-group">
+ <button
+ type="button"
+ data-filter-column="22"
+ data-filter-value="R"
+ class="ui-state-default"
+ title="' . I18N::translate('Show “roots” couples or individuals. These individuals may also be called “patriarchs”. They are individuals who have no parents recorded in the database.') . '"
+ >
+ ' . I18N::translate('Roots') . '
+ </button>
+ <button
+ type="button"
+ data-filter-column="22"
+ data-filter-value="L"
+ class="ui-state-default"
+ title="' . I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') . '"
+ >
+ ' . I18N::translate('Leaves') . '
+ </button>
+ </div>
+ <div class="btn-group">
+ <button
+ type="button"
+ data-filter-column="20"
+ data-filter-value="U"
+ class="ui-state-default"
+ title="' . I18N::translate('Show couples with an unknown marriage date.') . '"
+ >
+ ' . GedcomTag::getLabel('MARR') . '
+ </button>
+ <button
+ type="button"
+ data-filter-column="20"
+ data-filter-value="YES"
+ class="ui-state-default"
+ title="' . I18N::translate('Show couples who married more than 100 years ago.') . '"
+ >
+ ' . GedcomTag::getLabel('MARR') . '&gt;100
+ </button>
+ <button
+ type="button"
+ data-filter-column="20"
+ data-filter-value="Y100"
+ class="ui-state-default"
+ title="' . I18N::translate('Show couples who married within the last 100 years.') . '"
+ >
+ ' . GedcomTag::getLabel('MARR') . '&lt;=100
+ </button>
+ <button
+ type="button"
+ data-filter-column="20"
+ data-filter-value="D"
+ class="ui-state-default"
+ title="' . I18N::translate('Show divorced couples.') . '"
+ >
+ ' . GedcomTag::getLabel('DIV') . '
+ </button>
+ <button
+ type="button"
+ data-filter-column="20"
+ data-filter-value="M"
+ class="ui-state-default"
+ title="' . I18N::translate('Show couples where either partner married more than once.') . '"
+ >
+ ' . I18N::translate('Multiple marriages') . '
+ </button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr>
+ <th>' . GedcomTag::getLabel('GIVN') . '</th>
+ <th>' . GedcomTag::getLabel('SURN') . '</th>
+ <th>HUSB:GIVN_SURN</th>
+ <th>HUSB:SURN_GIVN</th>
+ <th>' . GedcomTag::getLabel('AGE') . '</th>
+ <th>AGE</th>
+ <th>' . GedcomTag::getLabel('GIVN') . '</th>
+ <th>' . GedcomTag::getLabel('SURN') . '</th>
+ <th>WIFE:GIVN_SURN</th>
+ <th>WIFE:SURN_GIVN</th>
+ <th>' . GedcomTag::getLabel('AGE') . '</th>
+ <th>AGE</th>
+ <th>' . GedcomTag::getLabel('MARR') . '</th>
+ <th>MARR:DATE</th>
+ <th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
+ <th>' . GedcomTag::getLabel('PLAC') . '</th>
+ <th><i class="icon-children" title="' . I18N::translate('Children') . '"></i></th>
+ <th>NCHI</th>
+ <th>' . GedcomTag::getLabel('CHAN') . '</th>
+ <th>CHAN</th>
+ <th>MARR</th>
+ <th>DEAT</th>
+ <th>TREE</th>
+ </tr>
+ </thead>
+ <tfoot>
+ <tr>
+ <th colspan="23">
+ <div class="btn-toolbar">
+ <div class="btn-group">
+ <button type="button" class="ui-state-default btn-toggle-parents">
+ ' . I18N::translate('Show parents') . '
+ </button>
+ <button type="button" class="ui-state-default btn-toggle-statistics">
+ ' . I18N::translate('Show statistics charts') . '
+ </button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ </tfoot>
+ <tbody>';
+
+ $d100y = new Date(date('Y') - 100); // 100 years ago
+ foreach ($datalist as $family) {
+ //-- Retrieve husband and wife
+ $husb = $family->getHusband();
+ if (is_null($husb)) {
+ $husb = new Individual('H', '0 @H@ INDI', null, $family->getTree());
+ }
+ $wife = $family->getWife();
+ if (is_null($wife)) {
+ $wife = new Individual('W', '0 @W@ INDI', null, $family->getTree());
+ }
+ if (!$family->canShow()) {
+ continue;
+ }
+ if ($family->isPendingAddtion()) {
+ $class = ' class="new"';
+ } elseif ($family->isPendingDeletion()) {
+ $class = ' class="old"';
+ } else {
+ $class = '';
+ }
+ $html .= '<tr' . $class . '>';
+ //-- Husband name(s)
+ $html .= '<td colspan="2">';
+ foreach ($husb->getAllNames() as $num => $name) {
+ if ($name['type'] == 'NAME') {
+ $title = '';
+ } else {
+ $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $husb)) . '"';
+ }
+ if ($num == $husb->getPrimaryName()) {
+ $class = ' class="name2"';
+ $sex_image = $husb->getSexImage();
+ list($surn, $givn) = explode(',', $name['sort']);
+ } else {
+ $class = '';
+ $sex_image = '';
+ }
+ // Only show married names if they are the name we are filtering by.
+ if ($name['type'] != '_MARNM' || $num == $husb->getPrimaryName()) {
+ $html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
+ }
+ }
+ // Husband parents
+ $html .= $husb->getPrimaryParentsNames('parents details1', 'none');
+ $html .= '</td>';
+ // Dummy column to match colspan in header
+ $html .= '<td style="display:none;"></td>';
+ //-- Husb GIVN
+ // Use "AAAA" as a separator (instead of ",") as Javascript.localeCompare() ignores
+ // punctuation and "ANN,ROACH" would sort after "ANNE,ROACH", instead of before it.
+ // Similarly, @N.N. would sort as NN.
+ $html .= '<td>' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . 'AAAA' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . '</td>';
+ $html .= '<td>' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . 'AAAA' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . '</td>';
+ $mdate = $family->getMarriageDate();
+ //-- Husband age
+ $hdate = $husb->getBirthDate();
+ if ($hdate->isOK() && $mdate->isOK()) {
+ if ($hdate->gregorianYear() >= 1550 && $hdate->gregorianYear() < 2030) {
+ $birt_by_decade[(int) ($hdate->gregorianYear() / 10) * 10] .= $husb->getSex();
+ }
+ $hage = Date::getAge($hdate, $mdate, 0);
+ if ($hage >= 0 && $hage <= $max_age) {
+ $marr_by_age[$hage] .= $husb->getSex();
+ }
+ }
+ $html .= '<td>' . Date::getAge($hdate, $mdate, 2) . '</td><td>' . Date::getAge($hdate, $mdate, 1) . '</td>';
+ //-- Wife name(s)
+ $html .= '<td colspan="2">';
+ foreach ($wife->getAllNames() as $num => $name) {
+ if ($name['type'] == 'NAME') {
+ $title = '';
+ } else {
+ $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $wife)) . '"';
+ }
+ if ($num == $wife->getPrimaryName()) {
+ $class = ' class="name2"';
+ $sex_image = $wife->getSexImage();
+ list($surn, $givn) = explode(',', $name['sort']);
+ } else {
+ $class = '';
+ $sex_image = '';
+ }
+ // Only show married names if they are the name we are filtering by.
+ if ($name['type'] != '_MARNM' || $num == $wife->getPrimaryName()) {
+ $html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
+ }
+ }
+ // Wife parents
+ $html .= $wife->getPrimaryParentsNames('parents details1', 'none');
+ $html .= '</td>';
+ // Dummy column to match colspan in header
+ $html .= '<td style="display:none;"></td>';
+ //-- Wife GIVN
+ //-- Husb GIVN
+ // Use "AAAA" as a separator (instead of ",") as Javascript.localeCompare() ignores
+ // punctuation and "ANN,ROACH" would sort after "ANNE,ROACH", instead of before it.
+ // Similarly, @N.N. would sort as NN.
+ $html .= '<td>' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . 'AAAA' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . '</td>';
+ $html .= '<td>' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . 'AAAA' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . '</td>';
+ $mdate = $family->getMarriageDate();
+ //-- Wife age
+ $wdate = $wife->getBirthDate();
+ if ($wdate->isOK() && $mdate->isOK()) {
+ if ($wdate->gregorianYear() >= 1550 && $wdate->gregorianYear() < 2030) {
+ $birt_by_decade[(int) ($wdate->gregorianYear() / 10) * 10] .= $wife->getSex();
+ }
+ $wage = Date::getAge($wdate, $mdate, 0);
+ if ($wage >= 0 && $wage <= $max_age) {
+ $marr_by_age[$wage] .= $wife->getSex();
+ }
+ }
+ $html .= '<td>' . Date::getAge($wdate, $mdate, 2) . '</td><td>' . Date::getAge($wdate, $mdate, 1) . '</td>';
+ //-- Marriage date
+ $html .= '<td>';
+ if ($marriage_dates = $family->getAllMarriageDates()) {
+ foreach ($marriage_dates as $n => $marriage_date) {
+ if ($n) {
+ $html .= '<br>';
+ }
+ $html .= '<div>' . $marriage_date->display(true) . '</div>';
+ }
+ if ($marriage_dates[0]->gregorianYear() >= 1550 && $marriage_dates[0]->gregorianYear() < 2030) {
+ $marr_by_decade[(int) ($marriage_dates[0]->gregorianYear() / 10) * 10] .= $husb->getSex() . $wife->getSex();
+ }
+ } elseif ($family->getFacts('_NMR')) {
+ $html .= I18N::translate('no');
+ } elseif ($family->getFacts('MARR')) {
+ $html .= I18N::translate('yes');
+ } else {
+ $html .= '&nbsp;';
+ }
+ $html .= '</td>';
+ //-- Event date (sortable)hidden by datatables code
+ $html .= '<td>';
+ if ($marriage_dates) {
+ $html .= $marriage_date->julianDay();
+ } else {
+ $html .= 0;
+ }
+ $html .= '</td>';
+ //-- Marriage anniversary
+ $html .= '<td>' . Date::getAge($mdate, null, 2) . '</td>';
+ //-- Marriage place
+ $html .= '<td>';
+ foreach ($family->getAllMarriagePlaces() as $n => $marriage_place) {
+ $tmp = new Place($marriage_place, $family->getTree());
+ if ($n) {
+ $html .= '<br>';
+ }
+ $html .= '<a href="' . $tmp->getURL() . '" title="' . strip_tags($tmp->getFullName()) . '">';
+ $html .= FunctionsPrint::highlightSearchHits($tmp->getShortName()) . '</a>';
+ }
+ $html .= '</td>';
+ //-- Number of children
+ $nchi = $family->getNumberOfChildren();
+ $html .= '<td>' . I18N::number($nchi) . '</td><td>' . $nchi . '</td>';
+ //-- Last change
+ $html .= '<td>' . $family->LastChangeTimestamp() . '</td>';
+ $html .= '<td>' . $family->LastChangeTimestamp(true) . '</td>';
+ //-- Sorting by marriage date
+ $html .= '<td>';
+ if (!$family->canShow() || !$mdate->isOK()) {
+ $html .= 'U';
+ } else {
+ if (Date::compare($mdate, $d100y) > 0) {
+ $html .= 'Y100';
+ } else {
+ $html .= 'YES';
+ }
+ }
+ if ($family->getFacts(WT_EVENTS_DIV)) {
+ $html .= 'D';
+ }
+ if (count($husb->getSpouseFamilies()) > 1 || count($wife->getSpouseFamilies()) > 1) {
+ $html .= 'M';
+ }
+ $html .= '</td>';
+ //-- Sorting alive/dead
+ $html .= '<td>';
+ if ($husb->isDead() && $wife->isDead()) {
+ $html .= 'Y';
+ }
+ if ($husb->isDead() && !$wife->isDead()) {
+ if ($wife->getSex() == 'F') {
+ $html .= 'H';
+ }
+ if ($wife->getSex() == 'M') {
+ $html .= 'W';
+ } // male partners
+ }
+ if (!$husb->isDead() && $wife->isDead()) {
+ if ($husb->getSex() == 'M') {
+ $html .= 'W';
+ }
+ if ($husb->getSex() == 'F') {
+ $html .= 'H';
+ } // female partners
+ }
+ if (!$husb->isDead() && !$wife->isDead()) {
+ $html .= 'N';
+ }
+ $html .= '</td>';
+ //-- Roots or Leaves
+ $html .= '<td>';
+ if (!$husb->getChildFamilies() && !$wife->getChildFamilies()) {
+ $html .= 'R';
+ } elseif (!$husb->isDead() && !$wife->isDead() && $family->getNumberOfChildren() < 1) {
+ $html .= 'L';
+ } else {
+ $html .= '&nbsp;';
+ }
+ $html .= '</td>
+ </tr>';
+ }
+ $html .= '
+ </tbody>
+ </table>
+ <div id="fam_list_table-charts_' . $table_id . '" style="display:none">
+ <table class="list-charts">
+ <tr>
+ <td>
+ ' . self::chartByDecade($birt_by_decade, I18N::translate('Decade of birth')) . '
+ </td>
+ <td>
+ ' . self::chartByDecade($marr_by_decade, I18N::translate('Decade of marriage')) . '
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ ' . self::chartByAge($marr_by_age, I18N::translate('Age in year of marriage')) . '
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>';
+
+ return $html;
+ }
+
+ /**
+ * Print a table of sources
+ *
+ * @param Source[] $datalist
+ *
+ * @return string
+ */
+ public static function sourceTable($datalist) {
+ global $WT_TREE, $controller;
+
+ // Count the number of linked records. These numbers include private records.
+ // It is not good to bypass privacy, but many servers do not have the resources
+ // to process privacy for every record in the tree
+ $count_individuals = Database::prepare(
+ "SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##individuals` JOIN `##link` ON l_from = i_id AND l_file = i_file AND l_type = 'SOUR' GROUP BY l_to, l_file"
+ )->fetchAssoc();
+ $count_families = Database::prepare(
+ "SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##families` JOIN `##link` ON l_from = f_id AND l_file = f_file AND l_type = 'SOUR' GROUP BY l_to, l_file"
+ )->fetchAssoc();
+ $count_media = Database::prepare(
+ "SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##media` JOIN `##link` ON l_from = m_id AND l_file = m_file AND l_type = 'SOUR' GROUP BY l_to, l_file"
+ )->fetchAssoc();
+ $count_notes = Database::prepare(
+ "SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##other` JOIN `##link` ON l_from = o_id AND l_file = o_file AND o_type = 'NOTE' AND l_type = 'SOUR' GROUP BY l_to, l_file"
+ )->fetchAssoc();
+
+ $html = '';
+ $table_id = 'table-sour-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
+ $controller
+ ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
+ ->addInlineJavascript('
+ jQuery.fn.dataTableExt.oSort["unicode-asc" ]=function(a,b) {return a.replace(/<[^<]*>/, "").localeCompare(b.replace(/<[^<]*>/, ""))};
+ jQuery.fn.dataTableExt.oSort["unicode-desc"]=function(a,b) {return b.replace(/<[^<]*>/, "").localeCompare(a.replace(/<[^<]*>/, ""))};
+ jQuery("#' . $table_id . '").dataTable( {
+ dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\',
+ ' . I18N::datatablesI18N() . ',
+ jQueryUI: true,
+ autoWidth: false,
+ processing: true,
+ columns: [
+ /* 0 title */ { dataSort: 1 },
+ /* 1 TITL */ { visible: false, type: "unicode" },
+ /* 2 author */ { type: "unicode" },
+ /* 3 #indi */ { dataSort: 4, class: "center" },
+ /* 4 #INDI */ { type: "num", visible: false },
+ /* 5 #fam */ { dataSort: 6, class: "center" },
+ /* 6 #FAM */ { type: "num", visible: false },
+ /* 7 #obje */ { dataSort: 8, class: "center" },
+ /* 8 #OBJE */ { type: "num", visible: false },
+ /* 9 #note */ { dataSort: 10, class: "center" },
+ /* 10 #NOTE */ { type: "num", visible: false },
+ /* 11 CHAN */ { dataSort: 12, visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
+ /* 12 CHAN_sort */ { visible: false },
+ /* 13 DELETE */ { visible: ' . (Auth::isManager($WT_TREE) ? 'true' : 'false') . ', sortable: false }
+ ],
+ displayLength: 20,
+ pagingType: "full_numbers"
+ });
+ jQuery(".source-list").css("visibility", "visible");
+ jQuery(".loading-image").css("display", "none");
+ ');
+
+ //--table wrapper
+ $html .= '<div class="loading-image">&nbsp;</div>';
+ $html .= '<div class="source-list">';
+ //-- table header
+ $html .= '<table id="' . $table_id . '"><thead><tr>';
+ $html .= '<th>' . GedcomTag::getLabel('TITL') . '</th>';
+ $html .= '<th>TITL</th>';
+ $html .= '<th>' . GedcomTag::getLabel('AUTH') . '</th>';
+ $html .= '<th>' . I18N::translate('Individuals') . '</th>';
+ $html .= '<th>#INDI</th>';
+ $html .= '<th>' . I18N::translate('Families') . '</th>';
+ $html .= '<th>#FAM</th>';
+ $html .= '<th>' . I18N::translate('Media objects') . '</th>';
+ $html .= '<th>#OBJE</th>';
+ $html .= '<th>' . I18N::translate('Shared notes') . '</th>';
+ $html .= '<th>#NOTE</th>';
+ $html .= '<th' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? '' : '') . '>' . GedcomTag::getLabel('CHAN') . '</th>';
+ $html .= '<th' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? '' : '') . '>CHAN</th>';
+ $html .= '<th></th>'; //delete
+ $html .= '</tr></thead>';
+ //-- table body
+ $html .= '<tbody>';
+
+ foreach ($datalist as $source) {
+ if (!$source->canShow()) {
+ continue;
+ }
+ if ($source->isPendingAddtion()) {
+ $class = ' class="new"';
+ } elseif ($source->isPendingDeletion()) {
+ $class = ' class="old"';
+ } else {
+ $class = '';
+ }
+ $html .= '<tr' . $class . '>';
+ //-- Source name(s)
+ $html .= '<td>';
+ foreach ($source->getAllNames() as $n => $name) {
+ if ($n) {
+ $html .= '<br>';
+ }
+ if ($n == $source->getPrimaryName()) {
+ $html .= '<a class="name2" href="' . $source->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>';
+ } else {
+ $html .= '<a href="' . $source->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>';
+ }
+ }
+ $html .= '</td>';
+ // Sortable name
+ $html .= '<td>' . strip_tags($source->getFullName()) . '</td>';
+ //-- Author
+ $auth = $source->getFirstFact('AUTH');
+ if ($auth) {
+ $author = $auth->getValue();
+ } else {
+ $author = '';
+ }
+ $html .= '<td>' . FunctionsPrint::highlightSearchHits($author) . '</td>';
+ $key = $source->getXref() . '@' . $source->getTree()->getTreeId();
+ //-- Linked INDIs
+ $num = array_key_exists($key, $count_individuals) ? $count_individuals[$key] : 0;
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Linked FAMs
+ $num = array_key_exists($key, $count_families) ? $count_families[$key] : 0;
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Linked OBJEcts
+ $num = array_key_exists($key, $count_media) ? $count_media[$key] : 0;
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Linked NOTEs
+ $num = array_key_exists($key, $count_notes) ? $count_notes[$key] : 0;
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Last change
+ if ($WT_TREE->getPreference('SHOW_LAST_CHANGE')) {
+ $html .= '<td>' . $source->LastChangeTimestamp() . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ //-- Last change hidden sort column
+ if ($WT_TREE->getPreference('SHOW_LAST_CHANGE')) {
+ $html .= '<td>' . $source->LastChangeTimestamp(true) . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ //-- Delete
+ if (Auth::isManager($WT_TREE)) {
+ $html .= '<td><div title="' . I18N::translate('Delete') . '" class="deleteicon" onclick="return delete_source(\'' . I18N::translate('Are you sure you want to delete “%s”?', Filter::escapeJs(Filter::unescapeHtml($source->getFullName()))) . "', '" . $source->getXref() . '\');"><span class="link_text">' . I18N::translate('Delete') . '</span></div></td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ $html .= '</tr>';
+ }
+ $html .= '</tbody></table></div>';
+
+ return $html;
+ }
+
+ /**
+ * Print a table of shared notes
+ *
+ * @param Note[] $datalist
+ *
+ * @return string
+ */
+ public static function noteTable($datalist) {
+ global $WT_TREE, $controller;
+
+ $html = '';
+ $table_id = 'table-note-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
+ $controller
+ ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
+ ->addInlineJavascript('
+ jQuery.fn.dataTableExt.oSort["unicode-asc" ]=function(a,b) {return a.replace(/<[^<]*>/, "").localeCompare(b.replace(/<[^<]*>/, ""))};
+ jQuery.fn.dataTableExt.oSort["unicode-desc"]=function(a,b) {return b.replace(/<[^<]*>/, "").localeCompare(a.replace(/<[^<]*>/, ""))};
+ jQuery("#' . $table_id . '").dataTable({
+ dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\',
+ ' . I18N::datatablesI18N() . ',
+ jQueryUI: true,
+ autoWidth: false,
+ processing: true,
+ columns: [
+ /* 0 title */ { type: "unicode" },
+ /* 1 #indi */ { dataSort: 2, class: "center" },
+ /* 2 #INDI */ { type: "num", visible: false },
+ /* 3 #fam */ { dataSort: 4, class: "center" },
+ /* 4 #FAM */ { type: "num", visible: false },
+ /* 5 #obje */ { dataSort: 6, class: "center" },
+ /* 6 #OBJE */ { type: "num", visible: false },
+ /* 7 #sour */ { dataSort: 8, class: "center" },
+ /* 8 #SOUR */ { type: "num", visible: false },
+ /* 9 CHAN */ { dataSort: 10, visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
+ /* 10 CHAN_sort */ { visible: false },
+ /* 11 DELETE */ { visible: ' . (Auth::isManager($WT_TREE) ? 'true' : 'false') . ', sortable: false }
+ ],
+ displayLength: 20,
+ pagingType: "full_numbers"
+ });
+ jQuery(".note-list").css("visibility", "visible");
+ jQuery(".loading-image").css("display", "none");
+ ');
+
+ //--table wrapper
+ $html .= '<div class="loading-image">&nbsp;</div>';
+ $html .= '<div class="note-list">';
+ //-- table header
+ $html .= '<table id="' . $table_id . '"><thead><tr>';
+ $html .= '<th>' . GedcomTag::getLabel('TITL') . '</th>';
+ $html .= '<th>' . I18N::translate('Individuals') . '</th>';
+ $html .= '<th>#INDI</th>';
+ $html .= '<th>' . I18N::translate('Families') . '</th>';
+ $html .= '<th>#FAM</th>';
+ $html .= '<th>' . I18N::translate('Media objects') . '</th>';
+ $html .= '<th>#OBJE</th>';
+ $html .= '<th>' . I18N::translate('Sources') . '</th>';
+ $html .= '<th>#SOUR</th>';
+ $html .= '<th' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? '' : '') . '>' . GedcomTag::getLabel('CHAN') . '</th>';
+ $html .= '<th' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? '' : '') . '>CHAN</th>';
+ $html .= '<th></th>'; //delete
+ $html .= '</tr></thead>';
+ //-- table body
+ $html .= '<tbody>';
+ foreach ($datalist as $note) {
+ if (!$note->canShow()) {
+ continue;
+ }
+ if ($note->isPendingAddtion()) {
+ $class = ' class="new"';
+ } elseif ($note->isPendingDeletion()) {
+ $class = ' class="old"';
+ } else {
+ $class = '';
+ }
+ $html .= '<tr' . $class . '>';
+ //-- Shared Note name
+ $html .= '<td><a class="name2" href="' . $note->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($note->getFullName()) . '</a></td>';
+ //-- Linked INDIs
+ $num = count($note->linkedIndividuals('NOTE'));
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Linked FAMs
+ $num = count($note->linkedfamilies('NOTE'));
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Linked OBJEcts
+ $num = count($note->linkedMedia('NOTE'));
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Linked SOURs
+ $num = count($note->linkedSources('NOTE'));
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Last change
+ if ($WT_TREE->getPreference('SHOW_LAST_CHANGE')) {
+ $html .= '<td>' . $note->LastChangeTimestamp() . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ //-- Last change hidden sort column
+ if ($WT_TREE->getPreference('SHOW_LAST_CHANGE')) {
+ $html .= '<td>' . $note->LastChangeTimestamp(true) . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ //-- Delete
+ if (Auth::isManager($WT_TREE)) {
+ $html .= '<td><div title="' . I18N::translate('Delete') . '" class="deleteicon" onclick="return delete_note(\'' . I18N::translate('Are you sure you want to delete “%s”?', Filter::escapeJs(Filter::unescapeHtml($note->getFullName()))) . "', '" . $note->getXref() . '\');"><span class="link_text">' . I18N::translate('Delete') . '</span></div></td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ $html .= '</tr>';
+ }
+ $html .= '</tbody></table></div>';
+
+ return $html;
+ }
+
+ /**
+ * Print a table of repositories
+ *
+ * @param Repository[] $repositories
+ *
+ * @return string
+ */
+ public static function repositoryTable($repositories) {
+ global $WT_TREE, $controller;
+
+ // Count the number of linked records. These numbers include private records.
+ // It is not good to bypass privacy, but many servers do not have the resources
+ // to process privacy for every record in the tree
+ $count_sources = Database::prepare(
+ "SELECT CONCAT(l_to, '@', l_file), COUNT(*) FROM `##sources` JOIN `##link` ON l_from = s_id AND l_file = s_file AND l_type = 'REPO' GROUP BY l_to, l_file"
+ )->fetchAssoc();
+
+ $html = '';
+ $table_id = 'table-repo-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
+ $controller
+ ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
+ ->addInlineJavascript('
+ jQuery.fn.dataTableExt.oSort["unicode-asc" ]=function(a,b) {return a.replace(/<[^<]*>/, "").localeCompare(b.replace(/<[^<]*>/, ""))};
+ jQuery.fn.dataTableExt.oSort["unicode-desc"]=function(a,b) {return b.replace(/<[^<]*>/, "").localeCompare(a.replace(/<[^<]*>/, ""))};
+ jQuery("#' . $table_id . '").dataTable({
+ dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\',
+ ' . I18N::datatablesI18N() . ',
+ jQueryUI: true,
+ autoWidth: false,
+ processing: true,
+ columns: [
+ /* 0 name */ { type: "unicode" },
+ /* 1 #sour */ { dataSort: 2, class: "center" },
+ /* 2 #SOUR */ { type: "num", visible: false },
+ /* 3 CHAN */ { dataSort: 4, visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
+ /* 4 CHAN_sort */ { visible: false },
+ /* 5 DELETE */ { visible: ' . (Auth::isManager($WT_TREE) ? 'true' : 'false') . ', sortable: false }
+ ],
+ displayLength: 20,
+ pagingType: "full_numbers"
+ });
+ jQuery(".repo-list").css("visibility", "visible");
+ jQuery(".loading-image").css("display", "none");
+ ');
+
+ //--table wrapper
+ $html .= '<div class="loading-image">&nbsp;</div>';
+ $html .= '<div class="repo-list">';
+ //-- table header
+ $html .= '<table id="' . $table_id . '"><thead><tr>';
+ $html .= '<th>' . I18N::translate('Repository name') . '</th>';
+ $html .= '<th>' . I18N::translate('Sources') . '</th>';
+ $html .= '<th>#SOUR</th>';
+ $html .= '<th' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? '' : '') . '>' . GedcomTag::getLabel('CHAN') . '</th>';
+ $html .= '<th' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? '' : '') . '>CHAN</th>';
+ $html .= '<th></th>'; //delete
+ $html .= '</tr></thead>';
+ //-- table body
+ $html .= '<tbody>';
+
+ foreach ($repositories as $repository) {
+ if (!$repository->canShow()) {
+ continue;
+ }
+ if ($repository->isPendingAddtion()) {
+ $class = ' class="new"';
+ } elseif ($repository->isPendingDeletion()) {
+ $class = ' class="old"';
+ } else {
+ $class = '';
+ }
+ $html .= '<tr' . $class . '>';
+ //-- Repository name(s)
+ $html .= '<td>';
+ foreach ($repository->getAllNames() as $n => $name) {
+ if ($n) {
+ $html .= '<br>';
+ }
+ if ($n == $repository->getPrimaryName()) {
+ $html .= '<a class="name2" href="' . $repository->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>';
+ } else {
+ $html .= '<a href="' . $repository->getHtmlUrl() . '">' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>';
+ }
+ }
+ $html .= '</td>';
+ $key = $repository->getXref() . '@' . $repository->getTree()->getTreeId();
+ //-- Linked SOURces
+ $num = array_key_exists($key, $count_sources) ? $count_sources[$key] : 0;
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Last change
+ if ($WT_TREE->getPreference('SHOW_LAST_CHANGE')) {
+ $html .= '<td>' . $repository->LastChangeTimestamp() . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ //-- Last change hidden sort column
+ if ($WT_TREE->getPreference('SHOW_LAST_CHANGE')) {
+ $html .= '<td>' . $repository->LastChangeTimestamp(true) . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ //-- Delete
+ if (Auth::isManager($WT_TREE)) {
+ $html .= '<td><div title="' . I18N::translate('Delete') . '" class="deleteicon" onclick="return delete_repository(\'' . I18N::translate('Are you sure you want to delete “%s”?', Filter::escapeJs(Filter::unescapeHtml($repository->getFullName()))) . "', '" . $repository->getXref() . '\');"><span class="link_text">' . I18N::translate('Delete') . '</span></div></td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ $html .= '</tr>';
+ }
+ $html .= '</tbody></table></div>';
+
+ return $html;
+ }
+
+ /**
+ * Print a table of media objects
+ *
+ * @param Media[] $media_objects
+ *
+ * @return string
+ */
+ public static function mediaTable($media_objects) {
+ global $WT_TREE, $controller;
+
+ $html = '';
+ $table_id = 'table-obje-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
+ $controller
+ ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
+ ->addInlineJavascript('
+ jQuery.fn.dataTableExt.oSort["unicode-asc" ]=function(a,b) {return a.replace(/<[^<]*>/, "").localeCompare(b.replace(/<[^<]*>/, ""))};
+ jQuery.fn.dataTableExt.oSort["unicode-desc"]=function(a,b) {return b.replace(/<[^<]*>/, "").localeCompare(a.replace(/<[^<]*>/, ""))};
+ jQuery("#' . $table_id . '").dataTable({
+ dom: \'<"H"pf<"dt-clear">irl>t<"F"pl>\',
+ ' . I18N::datatablesI18N() . ',
+ jQueryUI: true,
+ autoWidth:false,
+ processing: true,
+ columns: [
+ /* 0 media */ { sortable: false },
+ /* 1 title */ { type: "unicode" },
+ /* 2 #indi */ { dataSort: 3, class: "center" },
+ /* 3 #INDI */ { type: "num", visible: false },
+ /* 4 #fam */ { dataSort: 5, class: "center" },
+ /* 5 #FAM */ { type: "num", visible: false },
+ /* 6 #sour */ { dataSort: 7, class: "center" },
+ /* 7 #SOUR */ { type: "num", visible: false },
+ /* 8 CHAN */ { dataSort: 9, visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
+ /* 9 CHAN_sort */ { visible: false },
+ ],
+ displayLength: 20,
+ pagingType: "full_numbers"
+ });
+ jQuery(".media-list").css("visibility", "visible");
+ jQuery(".loading-image").css("display", "none");
+ ');
+
+ //--table wrapper
+ $html .= '<div class="loading-image">&nbsp;</div>';
+ $html .= '<div class="media-list">';
+ //-- table header
+ $html .= '<table id="' . $table_id . '"><thead><tr>';
+ $html .= '<th>' . I18N::translate('Media') . '</th>';
+ $html .= '<th>' . GedcomTag::getLabel('TITL') . '</th>';
+ $html .= '<th>' . I18N::translate('Individuals') . '</th>';
+ $html .= '<th>#INDI</th>';
+ $html .= '<th>' . I18N::translate('Families') . '</th>';
+ $html .= '<th>#FAM</th>';
+ $html .= '<th>' . I18N::translate('Sources') . '</th>';
+ $html .= '<th>#SOUR</th>';
+ $html .= '<th' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? '' : '') . '>' . GedcomTag::getLabel('CHAN') . '</th>';
+ $html .= '<th' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? '' : '') . '>CHAN</th>';
+ $html .= '</tr></thead>';
+ //-- table body
+ $html .= '<tbody>';
+
+ foreach ($media_objects as $media_object) {
+ if ($media_object->canShow()) {
+ $name = $media_object->getFullName();
+ if ($media_object->isPendingAddtion()) {
+ $class = ' class="new"';
+ } elseif ($media_object->isPendingDeletion()) {
+ $class = ' class="old"';
+ } else {
+ $class = '';
+ }
+ $html .= '<tr' . $class . '>';
+ //-- Object thumbnail
+ $html .= '<td>' . $media_object->displayImage() . '</td>';
+ //-- Object name(s)
+ $html .= '<td>';
+ $html .= '<a href="' . $media_object->getHtmlUrl() . '" class="list_item name2">';
+ $html .= FunctionsPrint::highlightSearchHits($name) . '</a>';
+ if (Auth::isEditor($media_object->getTree())) {
+ $html .= '<br><a href="' . $media_object->getHtmlUrl() . '">' . basename($media_object->getFilename()) . '</a>';
+ }
+ $html .= '</td>';
+
+ //-- Linked INDIs
+ $num = count($media_object->linkedIndividuals('OBJE'));
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Linked FAMs
+ $num = count($media_object->linkedfamilies('OBJE'));
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Linked SOURces
+ $num = count($media_object->linkedSources('OBJE'));
+ $html .= '<td>' . I18N::number($num) . '</td><td>' . $num . '</td>';
+ //-- Last change
+ if ($WT_TREE->getPreference('SHOW_LAST_CHANGE')) {
+ $html .= '<td>' . $media_object->LastChangeTimestamp() . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ //-- Last change hidden sort column
+ if ($WT_TREE->getPreference('SHOW_LAST_CHANGE')) {
+ $html .= '<td>' . $media_object->LastChangeTimestamp(true) . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ $html .= '</tr>';
+ }
+ }
+ $html .= '</tbody></table></div>';
+
+ return $html;
+ }
+
+ /**
+ * Print a table of surnames, for the top surnames block, the indi/fam lists, etc.
+ *
+ * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
+ * @param string $script "indilist.php" (counts of individuals) or "famlist.php" (counts of spouses)
+ * @param Tree $tree generate links for this tree
+ *
+ * @return string
+ */
+ public static function surnameTable($surnames, $script, Tree $tree) {
+ global $controller;
+
+ $html = '';
+ $controller
+ ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
+ ->addInlineJavascript('
+ jQuery.fn.dataTableExt.oSort["num-asc" ]=function(a,b) {a=parseFloat(a); b=parseFloat(b); return (a<b) ? -1 : (a>b ? 1 : 0);};
+ jQuery.fn.dataTableExt.oSort["num-desc"]=function(a,b) {a=parseFloat(a); b=parseFloat(b); return (a>b) ? -1 : (a<b ? 1 : 0);};
+ jQuery(".surname-list").dataTable( {
+ dom: \'t\',
+ jQueryUI: true,
+ autoWidth: false,
+ paging: false,
+ sorting: [],
+ columns: [
+ /* 0 name */ { dataSort: 1 },
+ /* 1 NAME */ { visible: false },
+ /* 2 count */ { dataSort: 3, class: "center" },
+ /* 3 COUNT */ { visible: false }
+ ],
+ });
+ ');
+
+ if ($script == 'famlist.php') {
+ $col_heading = I18N::translate('Spouses');
+ } else {
+ $col_heading = I18N::translate('Individuals');
+ }
+
+ $html .= '<table class="surname-list">' .
+ '<thead><tr>' .
+ '<th>' . GedcomTag::getLabel('SURN') . '</th>' .
+ '<th></th>' .
+ '<th>' . $col_heading . '</th>' .
+ '<th></th>' .
+ '</tr></thead>';
+
+ $html .= '<tbody>';
+ foreach ($surnames as $surn => $surns) {
+ // Each surname links back to the indi/fam surname list
+ if ($surn) {
+ $url = $script . '?surname=' . rawurlencode($surn) . '&amp;ged=' . $tree->getNameUrl();
+ } else {
+ $url = $script . '?alpha=,&amp;ged=' . $tree->getNameUrl();
+ }
+ // Row counter
+ $html .= '<tr>';
+ // Surname
+ $html .= '<td>';
+ // Multiple surname variants, e.g. von Groot, van Groot, van der Groot, etc.
+ foreach ($surns as $spfxsurn => $indis) {
+ if ($spfxsurn) {
+ $html .= '<a href="' . $url . '" dir="auto">' . Filter::escapeHtml($spfxsurn) . '</a><br>';
+ } else {
+ // No surname, but a value from "2 SURN"? A common workaround for toponyms, etc.
+ $html .= '<a href="' . $url . '" dir="auto">' . Filter::escapeHtml($surn) . '</a><br>';
+ }
+ }
+ $html .= '</td>';
+ // Sort column for name
+ $html .= '<td>' . $surn . '</td>';
+ // Surname count
+ $html .= '<td>';
+ $subtotal = 0;
+ foreach ($surns as $indis) {
+ $subtotal += count($indis);
+ $html .= I18N::number(count($indis)) . '<br>';
+ }
+ // More than one surname variant? Show a subtotal
+ if (count($surns) > 1) {
+ $html .= I18N::number($subtotal);
+ }
+ $html .= '</td>';
+ // add hidden numeric sort column
+ $html .= '<td>' . $subtotal . '</td></tr>';
+ }
+ $html .= '</tbody></table>';
+
+ return $html;
+ }
+
+ /**
+ * Print a tagcloud of surnames.
+ *
+ * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
+ * @param string $script indilist or famlist
+ * @param bool $totals show totals after each name
+ * @param Tree $tree generate links to this tree
+ *
+ * @return string
+ */
+ public static function surnameTagCloud($surnames, $script, $totals, Tree $tree) {
+ global $WT_TREE;
+
+ $minimum = PHP_INT_MAX;
+ $maximum = 1;
+ foreach ($surnames as $surn => $surns) {
+ foreach ($surns as $spfxsurn => $indis) {
+ $maximum = max($maximum, count($indis));
+ $minimum = min($minimum, count($indis));
+ }
+ }
+
+ $html = '';
+ foreach ($surnames as $surn => $surns) {
+ foreach ($surns as $spfxsurn => $indis) {
+ $size = 75.0 + 125.0 * (count($indis) - $minimum) / ($maximum - $minimum);
+ $html .= '<a style="font-size:' . $size . '%" href="' . $script . '?';
+ if ($surn) {
+ $html .= 'surname=' . urlencode($surn) . '&amp;ged=' . $tree->getNameUrl();
+ } else {
+ $html .= '?alpha=,&amp;ged=' . $WT_TREE->getNameUrl();
+ }
+ if ($totals) {
+ $html .= I18N::translate('%1$s (%2$s)', '<span dir="auto">' . $spfxsurn . '</span>', I18N::number(count($indis)));
+ } else {
+ $html .= $spfxsurn;
+ }
+ $html .= '</a> ';
+ }
+ }
+
+ return '<div class="tag_cloud">' . $html . '</div>';
+ }
+
+ /**
+ * Print a list of surnames.
+ *
+ * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID)
+ * @param int $style 1=bullet list, 2=semicolon-separated list, 3=tabulated list with up to 4 columns
+ * @param bool $totals show totals after each name
+ * @param string $script indilist or famlist
+ * @param Tree $tree Link back to the individual list in this tree
+ *
+ * @return string
+ */
+ public static function surnameList($surnames, $style, $totals, $script, Tree $tree) {
+ $html = array();
+ foreach ($surnames as $surn => $surns) {
+ // Each surname links back to the indilist
+ if ($surn) {
+ $url = $script . '?surname=' . urlencode($surn) . '&amp;ged=' . $tree->getNameUrl();
+ } else {
+ $url = $script . '?alpha=,&amp;ged=' . $tree->getNameUrl();
+ }
+ // If all the surnames are just case variants, then merge them into one
+ // Comment out this block if you want SMITH listed separately from Smith
+ $first_spfxsurn = null;
+ foreach ($surns as $spfxsurn => $indis) {
+ if ($first_spfxsurn) {
+ if (I18N::strtoupper($spfxsurn) == I18N::strtoupper($first_spfxsurn)) {
+ $surns[$first_spfxsurn] = array_merge($surns[$first_spfxsurn], $surns[$spfxsurn]);
+ unset($surns[$spfxsurn]);
+ }
+ } else {
+ $first_spfxsurn = $spfxsurn;
+ }
+ }
+ $subhtml = '<a href="' . $url . '" dir="auto">' . Filter::escapeHtml(implode(I18N::$list_separator, array_keys($surns))) . '</a>';
+
+ if ($totals) {
+ $subtotal = 0;
+ foreach ($surns as $indis) {
+ $subtotal += count($indis);
+ }
+ $subhtml .= '&nbsp;(' . I18N::number($subtotal) . ')';
+ }
+ $html[] = $subhtml;
+
+ }
+ switch ($style) {
+ case 1:
+ return '<ul><li>' . implode('</li><li>', $html) . '</li></ul>';
+ case 2:
+ return implode(I18N::$list_separator, $html);
+ case 3:
+ $i = 0;
+ $count = count($html);
+ if ($count > 36) {
+ $col = 4;
+ } elseif ($count > 18) {
+ $col = 3;
+ } elseif ($count > 6) {
+ $col = 2;
+ } else {
+ $col = 1;
+ }
+ $newcol = ceil($count / $col);
+ $html2 = '<table class="list_table"><tr>';
+ $html2 .= '<td class="list_value" style="padding: 14px;">';
+
+ foreach ($html as $surns) {
+ $html2 .= $surns . '<br>';
+ $i++;
+ if ($i == $newcol && $i < $count) {
+ $html2 .= '</td><td class="list_value" style="padding: 14px;">';
+ $newcol = $i + ceil($count / $col);
+ }
+ }
+ $html2 .= '</td></tr></table>';
+
+ return $html2;
+ }
+ }
+
+ /**
+ * Print a table of events
+ *
+ * @param string[] $change_ids
+ * @param string $sort
+ *
+ * @return string
+ */
+ public static function changesList($change_ids, $sort) {
+ global $WT_TREE;
+
+ $n = 0;
+ $arr = array();
+ foreach ($change_ids as $change_id) {
+ $record = GedcomRecord::getInstance($change_id, $WT_TREE);
+ if (!$record || !$record->canShow()) {
+ continue;
+ }
+ // setup sorting parameters
+ $arr[$n]['record'] = $record;
+ $arr[$n]['jd'] = ($sort == 'name') ? 1 : $n;
+ $arr[$n]['anniv'] = $record->lastChangeTimestamp(true);
+ $arr[$n++]['fact'] = $record->getSortName(); // in case two changes have same timestamp
+ }
+
+ switch ($sort) {
+ case 'name':
+ uasort($arr, '\Fisharebest\Webtrees\Functions\Functions::eventSortName');
+ break;
+ case 'date_asc':
+ uasort($arr, '\Fisharebest\Webtrees\Functions\Functions::eventSort');
+ $arr = array_reverse($arr);
+ break;
+ case 'date_desc':
+ uasort($arr, '\Fisharebest\Webtrees\Functions\Functions::eventSort');
+ }
+ $html = '';
+ foreach ($arr as $value) {
+ $html .= '<a href="' . $value['record']->getHtmlUrl() . '" class="list_item name2">' . $value['record']->getFullName() . '</a>';
+ $html .= '<div class="indent" style="margin-bottom: 5px;">';
+ if ($value['record'] instanceof Individual) {
+ if ($value['record']->getAddName()) {
+ $html .= '<a href="' . $value['record']->getHtmlUrl() . '" class="list_item">' . $value['record']->getAddName() . '</a>';
+ }
+ }
+ $html .= /* I18N: [a record was] Changed on <date/time> by <user> */
+ I18N::translate('Changed on %1$s by %2$s', $value['record']->lastChangeTimestamp(), Filter::escapeHtml($value['record']->lastChangeUser()));
+ $html .= '</div>';
+ }
+
+ return $html;
+ }
+
+ /**
+ * Print a table of events
+ *
+ * @param string[] $change_ids
+ * @param string $sort
+ *
+ * @return string
+ */
+ public static function changesTable($change_ids, $sort) {
+ global $controller, $WT_TREE;
+
+ $n = 0;
+ $table_id = 'table-chan-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
+ switch ($sort) {
+ case 'name': //name
+ $aaSorting = "[5,'asc'], [4,'desc']";
+ break;
+ case 'date_asc': //date ascending
+ $aaSorting = "[4,'asc'], [5,'asc']";
+ break;
+ case 'date_desc': //date descending
+ $aaSorting = "[4,'desc'], [5,'asc']";
+ break;
+ }
+ $html = '';
+ $controller
+ ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
+ ->addInlineJavascript('
+ jQuery.fn.dataTableExt.oSort["unicode-asc" ]=function(a,b) {return a.replace(/<[^<]*>/, "").localeCompare(b.replace(/<[^<]*>/, ""))};
+ jQuery.fn.dataTableExt.oSort["unicode-desc"]=function(a,b) {return b.replace(/<[^<]*>/, "").localeCompare(a.replace(/<[^<]*>/, ""))};
+ jQuery("#' . $table_id . '").dataTable({
+ dom: \'t\',
+ paging: false,
+ autoWidth:false,
+ lengthChange: false,
+ filter: false,
+ ' . I18N::datatablesI18N() . ',
+ jQueryUI: true,
+ sorting: [' . $aaSorting . '],
+ columns: [
+ /* 0-Type */ { sortable: false, class: "center" },
+ /* 1-Record */ { dataSort: 5 },
+ /* 2-Change */ { dataSort: 4 },
+ /* 3-By */ null,
+ /* 4-DATE */ { visible: false },
+ /* 5-SORTNAME */{ type: "unicode", visible: false }
+ ]
+ });
+ ');
+
+ //-- table header
+ $html .= '<table id="' . $table_id . '" class="width100">';
+ $html .= '<thead><tr>';
+ $html .= '<th></th>';
+ $html .= '<th>' . I18N::translate('Record') . '</th>';
+ $html .= '<th>' . GedcomTag::getLabel('CHAN') . '</th>';
+ $html .= '<th>' . GedcomTag::getLabel('_WT_USER') . '</th>';
+ $html .= '<th>DATE</th>'; //hidden by datatables code
+ $html .= '<th>SORTNAME</th>'; //hidden by datatables code
+ $html .= '</tr></thead><tbody>';
+
+ //-- table body
+ foreach ($change_ids as $change_id) {
+ $record = GedcomRecord::getInstance($change_id, $WT_TREE);
+ if (!$record || !$record->canShow()) {
+ continue;
+ }
+ $html .= '<tr><td>';
+ switch ($record::RECORD_TYPE) {
+ case 'INDI':
+ $icon = $record->getSexImage('small');
+ break;
+ case 'FAM':
+ $icon = '<i class="icon-button_family"></i>';
+ break;
+ case 'OBJE':
+ $icon = '<i class="icon-button_media"></i>';
+ break;
+ case 'NOTE':
+ $icon = '<i class="icon-button_note"></i>';
+ break;
+ case 'SOUR':
+ $icon = '<i class="icon-button_source"></i>';
+ break;
+ case 'REPO':
+ $icon = '<i class="icon-button_repository"></i>';
+ break;
+ default:
+ $icon = '&nbsp;';
+ break;
+ }
+ $html .= '<a href="' . $record->getHtmlUrl() . '">' . $icon . '</a>';
+ $html .= '</td>';
+ ++$n;
+ //-- Record name(s)
+ $name = $record->getFullName();
+ $html .= '<td class="wrap">';
+ $html .= '<a href="' . $record->getHtmlUrl() . '">' . $name . '</a>';
+ if ($record instanceof Individual) {
+ $addname = $record->getAddName();
+ if ($addname) {
+ $html .= '<div class="indent"><a href="' . $record->getHtmlUrl() . '">' . $addname . '</a></div>';
+ }
+ }
+ $html .= "</td>";
+ //-- Last change date/time
+ $html .= '<td class="wrap">' . $record->lastChangeTimestamp() . '</td>';
+ //-- Last change user
+ $html .= '<td class="wrap">' . Filter::escapeHtml($record->lastChangeUser()) . '</td>';
+ //-- change date (sortable) hidden by datatables code
+ $html .= '<td>' . $record->lastChangeTimestamp(true) . '</td>';
+ //-- names (sortable) hidden by datatables code
+ $html .= '<td>' . $record->getSortName() . '</td></tr>';
+ }
+
+ $html .= '</tbody></table>';
+
+ return $html;
+ }
+
+ /**
+ * Print a table of events
+ *
+ * @param int $startjd
+ * @param int $endjd
+ * @param string $events
+ * @param bool $only_living
+ * @param string $sort_by
+ *
+ * @return string
+ */
+ public static function eventsTable($startjd, $endjd, $events = 'BIRT MARR DEAT', $only_living = false, $sort_by = 'anniv') {
+ global $controller, $WT_TREE;
+
+ $html = '';
+ $table_id = 'table-even-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
+ $controller
+ ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
+ ->addInlineJavascript('
+ jQuery("#' . $table_id . '").dataTable({
+ dom: \'t\',
+ ' . I18N::datatablesI18N() . ',
+ autoWidth: false,
+ paging: false,
+ lengthChange: false,
+ filter: false,
+ info: true,
+ jQueryUI: true,
+ sorting: [[ ' . ($sort_by == 'alpha' ? 1 : 3) . ', "asc"]],
+ columns: [
+ /* 0-Record */ { dataSort: 1 },
+ /* 1-NAME */ { visible: false },
+ /* 2-Date */ { dataSort: 3 },
+ /* 3-DATE */ { visible: false },
+ /* 4-Anniv. */ { dataSort: 5, class: "center" },
+ /* 5-ANNIV */ { type: "num", visible: false },
+ /* 6-Event */ { class: "center" }
+ ]
+ });
+ ');
+
+ // Did we have any output? Did we skip anything?
+ $output = 0;
+ $filter = 0;
+ $filtered_events = array();
+
+ foreach (FunctionsDb::getEventsList($startjd, $endjd, $events, $WT_TREE) as $fact) {
+ $record = $fact->getParent();
+ //-- only living people ?
+ if ($only_living) {
+ if ($record instanceof Individual && $record->isDead()) {
+ $filter++;
+ continue;
+ }
+ if ($record instanceof Family) {
+ $husb = $record->getHusband();
+ if (is_null($husb) || $husb->isDead()) {
+ $filter++;
+ continue;
+ }
+ $wife = $record->getWife();
+ if (is_null($wife) || $wife->isDead()) {
+ $filter++;
+ continue;
+ }
+ }
+ }
+
+ //-- Counter
+ $output++;
+
+ if ($output == 1) {
+ //-- table body
+ $html .= '<table id="' . $table_id . '" class="width100">';
+ $html .= '<thead><tr>';
+ $html .= '<th>' . I18N::translate('Record') . '</th>';
+ $html .= '<th>NAME</th>'; //hidden by datatables code
+ $html .= '<th>' . GedcomTag::getLabel('DATE') . '</th>';
+ $html .= '<th>DATE</th>'; //hidden by datatables code
+ $html .= '<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>';
+ $html .= '<th>ANNIV</th>';
+ $html .= '<th>' . GedcomTag::getLabel('EVEN') . '</th>';
+ $html .= '</tr></thead><tbody>';
+ }
+
+ $filtered_events[] = $fact;
+ }
+
+ foreach ($filtered_events as $n => $fact) {
+ $record = $fact->getParent();
+ $html .= '<tr>';
+ $html .= '<td>';
+ $html .= '<a href="' . $record->getHtmlUrl() . '">' . $record->getFullName() . '</a>';
+ if ($record instanceof Individual) {
+ $html .= $record->getSexImage();
+ }
+ $html .= '</td>';
+ $html .= '<td>' . $record->getSortName() . '</td>';
+ $html .= '<td>' . $fact->getDate()->display(!Auth::isSearchEngine()) . '</td>';
+ $html .= '<td>' . $n . '</td>';
+ $html .= '<td>' . I18N::number($fact->anniv) . '</td>';
+ $html .= '<td>' . $fact->anniv . '</td>';
+ $html .= '<td>' . $fact->getLabel() . '</td>';
+ $html .= '</tr>';
+ }
+
+ if ($output != 0) {
+ $html .= '</tbody></table>';
+ }
+
+ // Print a final summary message about restricted/filtered facts
+ $summary = '';
+ if ($endjd == WT_CLIENT_JD) {
+ // We're dealing with the Today’s Events block
+ if ($output == 0) {
+ if ($filter == 0) {
+ $summary = I18N::translate('No events exist for today.');
+ } else {
+ $summary = I18N::translate('No events for living individuals exist for today.');
+ }
+ }
+ } else {
+ // We're dealing with the Upcoming Events block
+ if ($output == 0) {
+ if ($filter == 0) {
+ if ($endjd == $startjd) {
+ $summary = I18N::translate('No events exist for tomorrow.');
+ } else {
+ // I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
+ $summary = I18N::plural('No events exist for the next %s day.', 'No events exist for the next %s days.', $endjd - $startjd + 1, I18N::number($endjd - $startjd + 1));
+ }
+ } else {
+ if ($endjd == $startjd) {
+ $summary = I18N::translate('No events for living individuals exist for tomorrow.');
+ } else {
+ // I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
+ $summary = I18N::plural('No events for living people exist for the next %s day.', 'No events for living people exist for the next %s days.', $endjd - $startjd + 1, I18N::number($endjd - $startjd + 1));
+ }
+ }
+ }
+ }
+ if ($summary != "") {
+ $html .= '<strong>' . $summary . '</strong>';
+ }
+
+ return $html;
+ }
+
+ /**
+ * Print a list of events
+ *
+ * This performs the same function as print_events_table(), but formats the output differently.
+ *
+ * @param int $startjd
+ * @param int $endjd
+ * @param string $events
+ * @param bool $only_living
+ * @param string $sort_by
+ *
+ * @return string
+ */
+ public static function eventsList($startjd, $endjd, $events = 'BIRT MARR DEAT', $only_living = false, $sort_by = 'anniv') {
+ global $WT_TREE;
+
+ // Did we have any output? Did we skip anything?
+ $output = 0;
+ $filter = 0;
+ $filtered_events = array();
+ $html = '';
+ foreach (FunctionsDb::getEventsList($startjd, $endjd, $events, $WT_TREE) as $fact) {
+ $record = $fact->getParent();
+ //-- only living people ?
+ if ($only_living) {
+ if ($record instanceof Individual && $record->isDead()) {
+ $filter++;
+ continue;
+ }
+ if ($record instanceof Family) {
+ $husb = $record->getHusband();
+ if (is_null($husb) || $husb->isDead()) {
+ $filter++;
+ continue;
+ }
+ $wife = $record->getWife();
+ if (is_null($wife) || $wife->isDead()) {
+ $filter++;
+ continue;
+ }
+ }
+ }
+
+ $output++;
+
+ $filtered_events[] = $fact;
+ }
+
+ // Now we've filtered the list, we can sort by event, if required
+ switch ($sort_by) {
+ case 'anniv':
+ // Data is already sorted by anniversary date
+ break;
+ case 'alpha':
+ uasort($filtered_events, function (Fact $x, Fact $y) {
+ return GedcomRecord::compare($x->getParent(), $y->getParent());
+ });
+ break;
+ }
+
+ foreach ($filtered_events as $fact) {
+ $record = $fact->getParent();
+ $html .= '<a href="' . $record->getHtmlUrl() . '" class="list_item name2">' . $record->getFullName() . '</a>';
+ if ($record instanceof Individual) {
+ $html .= $record->getSexImage();
+ }
+ $html .= '<br><div class="indent">';
+ $html .= $fact->getLabel() . ' — ' . $fact->getDate()->display(true);
+ if ($fact->anniv) {
+ $html .= ' (' . I18N::translate('%s year anniversary', $fact->anniv) . ')';
+ }
+ if (!$fact->getPlace()->isEmpty()) {
+ $html .= ' — <a href="' . $fact->getPlace()->getURL() . '">' . $fact->getPlace()->getFullName() . '</a>';
+ }
+ $html .= '</div>';
+ }
+
+ // Print a final summary message about restricted/filtered facts
+ $summary = '';
+ if ($endjd == WT_CLIENT_JD) {
+ // We're dealing with the Today’s Events block
+ if ($output == 0) {
+ if ($filter == 0) {
+ $summary = I18N::translate('No events exist for today.');
+ } else {
+ $summary = I18N::translate('No events for living individuals exist for today.');
+ }
+ }
+ } else {
+ // We're dealing with the Upcoming Events block
+ if ($output == 0) {
+ if ($filter == 0) {
+ if ($endjd == $startjd) {
+ $summary = I18N::translate('No events exist for tomorrow.');
+ } else {
+ // I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
+ $summary = I18N::plural('No events exist for the next %s day.', 'No events exist for the next %s days.', $endjd - $startjd + 1, I18N::number($endjd - $startjd + 1));
+ }
+ } else {
+ if ($endjd == $startjd) {
+ $summary = I18N::translate('No events for living individuals exist for tomorrow.');
+ } else {
+ // I18N: translation for %s==1 is unused; it is translated separately as “tomorrow”
+ $summary = I18N::plural('No events for living people exist for the next %s day.', 'No events for living people exist for the next %s days.', $endjd - $startjd + 1, I18N::number($endjd - $startjd + 1));
+ }
+ }
+ }
+ }
+ if ($summary) {
+ $html .= "<b>" . $summary . "</b>";
+ }
+
+ return $html;
+ }
+
+ /**
+ * Print a chart by age using Google chart API
+ *
+ * @param integer[] $data
+ * @param string $title
+ *
+ * @return string
+ */
+ public static function chartByAge($data, $title) {
+ $count = 0;
+ $agemax = 0;
+ $vmax = 0;
+ $avg = 0;
+ foreach ($data as $age => $v) {
+ $n = strlen($v);
+ $vmax = max($vmax, $n);
+ $agemax = max($agemax, $age);
+ $count += $n;
+ $avg += $age * $n;
+ }
+ if ($count < 1) {
+ return '';
+ }
+ $avg = round($avg / $count);
+ $chart_url = "https://chart.googleapis.com/chart?cht=bvs"; // chart type
+ $chart_url .= "&amp;chs=725x150"; // size
+ $chart_url .= "&amp;chbh=3,2,2"; // bvg : 4,1,2
+ $chart_url .= "&amp;chf=bg,s,FFFFFF99"; //background color
+ $chart_url .= "&amp;chco=0000FF,FFA0CB,FF0000"; // bar color
+ $chart_url .= "&amp;chdl=" . rawurlencode(I18N::translate('Males')) . "|" . rawurlencode(I18N::translate('Females')) . "|" . rawurlencode(I18N::translate('Average age') . ": " . $avg); // legend & average age
+ $chart_url .= "&amp;chtt=" . rawurlencode($title); // title
+ $chart_url .= "&amp;chxt=x,y,r"; // axis labels specification
+ $chart_url .= "&amp;chm=V,FF0000,0," . ($avg - 0.3) . ",1"; // average age line marker
+ $chart_url .= "&amp;chxl=0:|"; // label
+ for ($age = 0; $age <= $agemax; $age += 5) {
+ $chart_url .= $age . "|||||"; // x axis
+ }
+ $chart_url .= "|1:||" . rawurlencode(I18N::percentage($vmax / $count)); // y axis
+ $chart_url .= "|2:||";
+ $step = $vmax;
+ for ($d = $vmax; $d > 0; $d--) {
+ if ($vmax < ($d * 10 + 1) && ($vmax % $d) == 0) {
+ $step = $d;
+ }
+ }
+ if ($step == $vmax) {
+ for ($d = $vmax - 1; $d > 0; $d--) {
+ if (($vmax - 1) < ($d * 10 + 1) && (($vmax - 1) % $d) == 0) {
+ $step = $d;
+ }
+ }
+ }
+ for ($n = $step; $n < $vmax; $n += $step) {
+ $chart_url .= $n . "|";
+ }
+ $chart_url .= rawurlencode($vmax . " / " . $count); // r axis
+ $chart_url .= "&amp;chg=100," . round(100 * $step / $vmax, 1) . ",1,5"; // grid
+ $chart_url .= "&amp;chd=s:"; // data : simple encoding from A=0 to 9=61
+ $CHART_ENCODING61 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ for ($age = 0; $age <= $agemax; $age++) {
+ $chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$age], "M") * 61 / $vmax)];
+ }
+ $chart_url .= ",";
+ for ($age = 0; $age <= $agemax; $age++) {
+ $chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$age], "F") * 61 / $vmax)];
+ }
+ $html = '<img src="' . $chart_url . '" alt="' . $title . '" title="' . $title . '" class="gchart">';
+
+ return $html;
+ }
+
+ /**
+ * Print a chart by decade using Google chart API
+ *
+ * @param integer[] $data
+ * @param string $title
+ *
+ * @return string
+ */
+ public static function chartByDecade($data, $title) {
+ $count = 0;
+ $vmax = 0;
+ foreach ($data as $v) {
+ $n = strlen($v);
+ $vmax = max($vmax, $n);
+ $count += $n;
+ }
+ if ($count < 1) {
+ return '';
+ }
+ $chart_url = "https://chart.googleapis.com/chart?cht=bvs"; // chart type
+ $chart_url .= "&amp;chs=360x150"; // size
+ $chart_url .= "&amp;chbh=3,3"; // bvg : 4,1,2
+ $chart_url .= "&amp;chf=bg,s,FFFFFF99"; //background color
+ $chart_url .= "&amp;chco=0000FF,FFA0CB"; // bar color
+ $chart_url .= "&amp;chtt=" . rawurlencode($title); // title
+ $chart_url .= "&amp;chxt=x,y,r"; // axis labels specification
+ $chart_url .= "&amp;chxl=0:|&lt;|||"; // <1570
+ for ($y = 1600; $y < 2030; $y += 50) {
+ $chart_url .= $y . "|||||"; // x axis
+ }
+ $chart_url .= "|1:||" . rawurlencode(I18N::percentage($vmax / $count)); // y axis
+ $chart_url .= "|2:||";
+ $step = $vmax;
+ for ($d = $vmax; $d > 0; $d--) {
+ if ($vmax < ($d * 10 + 1) && ($vmax % $d) == 0) {
+ $step = $d;
+ }
+ }
+ if ($step == $vmax) {
+ for ($d = $vmax - 1; $d > 0; $d--) {
+ if (($vmax - 1) < ($d * 10 + 1) && (($vmax - 1) % $d) == 0) {
+ $step = $d;
+ }
+ }
+ }
+ for ($n = $step; $n < $vmax; $n += $step) {
+ $chart_url .= $n . "|";
+ }
+ $chart_url .= rawurlencode($vmax . " / " . $count); // r axis
+ $chart_url .= "&amp;chg=100," . round(100 * $step / $vmax, 1) . ",1,5"; // grid
+ $chart_url .= "&amp;chd=s:"; // data : simple encoding from A=0 to 9=61
+ $CHART_ENCODING61 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ for ($y = 1570; $y < 2030; $y += 10) {
+ $chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$y], "M") * 61 / $vmax)];
+ }
+ $chart_url .= ",";
+ for ($y = 1570; $y < 2030; $y += 10) {
+ $chart_url .= $CHART_ENCODING61[(int) (substr_count($data[$y], "F") * 61 / $vmax)];
+ }
+ $html = '<img src="' . $chart_url . '" alt="' . $title . '" title="' . $title . '" class="gchart">';
+
+ return $html;
+ }
+}
diff --git a/app/Functions/FunctionsRtl.php b/app/Functions/FunctionsRtl.php
new file mode 100644
index 0000000000..b9cced5edf
--- /dev/null
+++ b/app/Functions/FunctionsRtl.php
@@ -0,0 +1,1154 @@
+<?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/>.
+ */
+
+// RTL Functions for use in the PDF/HTML reports
+
+use Fisharebest\Webtrees\I18N;
+
+/**
+ * Class FunctionsRtl - common functions
+ */
+class FunctionsRtl {
+ const OPEN_PARENTHESES = '([{';
+
+ const CLOSE_PARENTHESES = ')]}';
+
+ const NUMBERS = '0123456789';
+
+ const NUMBER_PREFIX = '+-'; // Treat these like numbers when at beginning or end of numeric strings
+
+ const NUMBER_PUNCTUATION = '- ,.:/'; // Treat these like numbers when inside numeric strings
+
+ const PUNCTUATION = ',.:;?!';
+
+ private static $previousState;
+
+ private static $currentState;
+
+ private static $waitingText;
+
+ private static $startLTR;
+
+ private static $endLTR;
+
+ private static $startRTL;
+
+ private static $endRTL;
+
+ private static $lenStart;
+
+ private static $lenEnd;
+
+ private static $spanNumber = 0;
+
+ private static $posSpanStart;
+
+ /**
+ * This function strips &lrm; and &rlm; from the input string. It should be used for all
+ * text that has been passed through the PrintReady() function before that text is stored
+ * in the database. The database should NEVER contain these characters.
+ *
+ * @param string $inputText The string from which the &lrm; and &rlm; characters should be stripped
+ *
+ * @return string The input string, with &lrm; and &rlm; stripped
+ */
+ public static function stripLrmRlm($inputText) {
+ return str_replace(array(WT_UTF8_LRM, WT_UTF8_RLM, WT_UTF8_LRO, WT_UTF8_RLO, WT_UTF8_LRE, WT_UTF8_RLE, WT_UTF8_PDF, "&lrm;", "&rlm;", "&LRM;", "&RLM;"), "", $inputText);
+ }
+
+ /**
+ * This function encapsulates all texts in the input with <span dir='xxx'> and </span>
+ * according to the directionality specified.
+ *
+ * @param string $inputText Raw input
+ * @param string $direction Directionality (LTR, BOTH, RTL) default BOTH
+ * @param string $class Additional text to insert into output <span dir="xxx"> (such as 'class="yyy"')
+ *
+ * @return string The string with all texts encapsulated as required
+ */
+ public static function spanLtrRtl($inputText, $direction = 'BOTH', $class = '') {
+ if ($inputText == '') {
+ // Nothing to do
+ return '';
+ }
+ self::$spanNumber++;
+
+ $workingText = str_replace("\n", '<br>', $inputText);
+ $workingText = str_replace(array('<span class="starredname"><br>', '<span<br>class="starredname">'), '<br><span class="starredname">', $workingText); // Reposition some incorrectly placed line breaks
+ $workingText = self::stripLrmRlm($workingText); // Get rid of any existing UTF8 control codes
+
+ // $nothing = '&zwnj;'; // Zero Width Non-Joiner (not sure whether this is still needed to work around a TCPDF bug)
+ $nothing = '';
+
+ self::$startLTR = '<LTR>'; // This will become '<span dir="ltr">' at the end
+ self::$endLTR = '</LTR>'; // This will become '</span>' at the end
+ self::$startRTL = '<RTL>'; // This will become '<span dir="rtl">' at the end
+ self::$endRTL = '</RTL>'; // This will become '</span>' at the end
+ self::$lenStart = strlen(self::$startLTR); // RTL version MUST have same length
+ self::$lenEnd = strlen(self::$endLTR); // RTL version MUST have same length
+
+ self::$previousState = '';
+ self::$currentState = strtoupper(I18N::direction());
+ $numberState = false; // Set when we're inside a numeric string
+ $result = '';
+ self::$waitingText = '';
+ $openParDirection = array();
+
+ self::beginCurrentSpan($result);
+
+ while ($workingText != '') {
+ $charArray = self::getChar($workingText, 0); // Get the next ASCII or UTF-8 character
+ $currentLetter = $charArray['letter'];
+ $currentLen = $charArray['length'];
+
+ $openParIndex = strpos(self::OPEN_PARENTHESES, $currentLetter); // Which opening parenthesis is this?
+ $closeParIndex = strpos(self::CLOSE_PARENTHESES, $currentLetter); // Which closing parenthesis is this?
+
+ switch ($currentLetter) {
+ case '<':
+ // Assume this '<' starts an HTML element
+ $endPos = strpos($workingText, '>'); // look for the terminating '>'
+ if ($endPos === false) {
+ $endPos = 0;
+ }
+ $currentLen += $endPos;
+ $element = substr($workingText, 0, $currentLen);
+ $temp = strtolower(substr($element, 0, 3));
+ if (strlen($element) < 7 && $temp == '<br') {
+ if ($numberState) {
+ $numberState = false;
+ if (self::$currentState == 'RTL') {
+ self::$waitingText .= WT_UTF8_PDF;
+ }
+ }
+ self::breakCurrentSpan($result);
+ } elseif (self::$waitingText == '') {
+ $result .= $element;
+ } else {
+ self::$waitingText .= $element;
+ }
+ $workingText = substr($workingText, $currentLen);
+ break;
+ case '&':
+ // Assume this '&' starts an HTML entity
+ $endPos = strpos($workingText, ';'); // look for the terminating ';'
+ if ($endPos === false) {
+ $endPos = 0;
+ }
+ $currentLen += $endPos;
+ $entity = substr($workingText, 0, $currentLen);
+ if (strtolower($entity) == '&nbsp;') {
+ $entity .= '&nbsp;'; // Ensure consistent case for this entity
+ }
+ if (self::$waitingText == '') {
+ $result .= $entity;
+ } else {
+ self::$waitingText .= $entity;
+ }
+ $workingText = substr($workingText, $currentLen);
+ break;
+ case '{':
+ if (substr($workingText, 1, 1) == '{') {
+ // Assume this '{{' starts a TCPDF directive
+ $endPos = strpos($workingText, '}}'); // look for the terminating '}}'
+ if ($endPos === false) {
+ $endPos = 0;
+ }
+ $currentLen = $endPos + 2;
+ $directive = substr($workingText, 0, $currentLen);
+ $workingText = substr($workingText, $currentLen);
+ $result = $result . self::$waitingText . $directive;
+ self::$waitingText = '';
+ break;
+ }
+ default:
+ // Look for strings of numbers with optional leading or trailing + or -
+ // and with optional embedded numeric punctuation
+ if ($numberState) {
+ // If we're inside a numeric string, look for reasons to end it
+ $offset = 0; // Be sure to look at the current character first
+ $charArray = self::getChar($workingText . "\n", $offset);
+ if (strpos(self::NUMBERS, $charArray['letter']) === false) {
+ // This is not a digit. Is it numeric punctuation?
+ if (substr($workingText . "\n", $offset, 6) == '&nbsp;') {
+ $offset += 6; // This could be numeric punctuation
+ } elseif (strpos(self::NUMBER_PUNCTUATION, $charArray['letter']) !== false) {
+ $offset += $charArray['length']; // This could be numeric punctuation
+ }
+ // If the next character is a digit, the current character is numeric punctuation
+ $charArray = self::getChar($workingText . "\n", $offset);
+ if (strpos(self::NUMBERS, $charArray['letter']) === false) {
+ // This is not a digit. End the run of digits and punctuation.
+ $numberState = false;
+ if (self::$currentState == 'RTL') {
+ if (strpos(self::NUMBER_PREFIX, $currentLetter) === false) {
+ $currentLetter = WT_UTF8_PDF . $currentLetter;
+ } else {
+ $currentLetter = $currentLetter . WT_UTF8_PDF; // Include a trailing + or - in the run
+ }
+ }
+ }
+ }
+ } else {
+ // If we're outside a numeric string, look for reasons to start it
+ if (strpos(self::NUMBER_PREFIX, $currentLetter) !== false) {
+ // This might be a number lead-in
+ $offset = $currentLen;
+ $nextChar = substr($workingText . "\n", $offset, 1);
+ if (strpos(self::NUMBERS, $nextChar) !== false) {
+ $numberState = true; // We found a digit: the lead-in is therefore numeric
+ if (self::$currentState == 'RTL') {
+ $currentLetter = WT_UTF8_LRE . $currentLetter;
+ }
+ }
+ } elseif (strpos(self::NUMBERS, $currentLetter) !== false) {
+ $numberState = true; // The current letter is a digit
+ if (self::$currentState == 'RTL') {
+ $currentLetter = WT_UTF8_LRE . $currentLetter;
+ }
+ }
+ }
+
+ // Determine the directionality of the current UTF-8 character
+ $newState = self::$currentState;
+ while (true) {
+ if (I18N::scriptDirection(I18N::textScript($currentLetter)) === 'rtl') {
+ if (self::$currentState == '') {
+ $newState = 'RTL';
+ break;
+ }
+
+ if (self::$currentState == 'RTL') {
+ break;
+ }
+ // Switch to RTL only if this isn't a solitary RTL letter
+ $tempText = substr($workingText, $currentLen);
+ while ($tempText != '') {
+ $nextCharArray = self::getChar($tempText, 0);
+ $nextLetter = $nextCharArray['letter'];
+ $nextLen = $nextCharArray['length'];
+ $tempText = substr($tempText, $nextLen);
+
+ if (I18N::scriptDirection(I18N::textScript($nextLetter)) === 'rtl') {
+ $newState = 'RTL';
+ break 2;
+ }
+
+ if (strpos(self::PUNCTUATION, $nextLetter) !== false || strpos(self::OPEN_PARENTHESES, $nextLetter) !== false) {
+ $newState = 'RTL';
+ break 2;
+ }
+
+ if ($nextLetter === ' ') {
+ break;
+ }
+ $nextLetter .= substr($tempText . "\n", 0, 5);
+ if ($nextLetter === '&nbsp;') {
+ break;
+ }
+ }
+ // This is a solitary RTL letter : wrap it in UTF8 control codes to force LTR directionality
+ $currentLetter = WT_UTF8_LRO . $currentLetter . WT_UTF8_PDF;
+ $newState = 'LTR';
+ break;
+ }
+ if (($currentLen != 1) || ($currentLetter >= 'A' && $currentLetter <= 'Z') || ($currentLetter >= 'a' && $currentLetter <= 'z')) {
+ // Since it’s neither Hebrew nor Arabic, this UTF-8 character or ASCII letter must be LTR
+ $newState = 'LTR';
+ break;
+ }
+ if ($closeParIndex !== false) {
+ // This closing parenthesis has to inherit the matching opening parenthesis' directionality
+ if (!empty($openParDirection[$closeParIndex]) && $openParDirection[$closeParIndex] != '?') {
+ $newState = $openParDirection[$closeParIndex];
+ }
+ $openParDirection[$closeParIndex] = '';
+ break;
+ }
+ if ($openParIndex !== false) {
+ // Opening parentheses always inherit the following directionality
+ self::$waitingText .= $currentLetter;
+ $workingText = substr($workingText, $currentLen);
+ while (true) {
+ if ($workingText === '') {
+ break;
+ }
+ if (substr($workingText, 0, 1) === ' ') {
+ // Spaces following this left parenthesis inherit the following directionality too
+ self::$waitingText .= ' ';
+ $workingText = substr($workingText, 1);
+ continue;
+ }
+ if (substr($workingText, 0, 6) === '&nbsp;') {
+ // Spaces following this left parenthesis inherit the following directionality too
+ self::$waitingText .= '&nbsp;';
+ $workingText = substr($workingText, 6);
+ continue;
+ }
+ break;
+ }
+ $openParDirection[$openParIndex] = '?';
+ break 2; // double break because we're waiting for more information
+ }
+
+ // We have a digit or a "normal" special character.
+ //
+ // When this character is not at the start of the input string, it inherits the preceding directionality;
+ // at the start of the input string, it assumes the following directionality.
+ //
+ // Exceptions to this rule will be handled later during final clean-up.
+ //
+ self::$waitingText .= $currentLetter;
+ $workingText = substr($workingText, $currentLen);
+ if (self::$currentState != '') {
+ $result .= self::$waitingText;
+ self::$waitingText = '';
+ }
+ break 2; // double break because we're waiting for more information
+ }
+ if ($newState != self::$currentState) {
+ // A direction change has occurred
+ self::finishCurrentSpan($result, false);
+ self::$previousState = self::$currentState;
+ self::$currentState = $newState;
+ self::beginCurrentSpan($result);
+ }
+ self::$waitingText .= $currentLetter;
+ $workingText = substr($workingText, $currentLen);
+ $result .= self::$waitingText;
+ self::$waitingText = '';
+
+ foreach ($openParDirection as $index => $value) {
+ // Since we now know the proper direction, remember it for all waiting opening parentheses
+ if ($value === '?') {
+ $openParDirection[$index] = self::$currentState;
+ }
+ }
+
+ break;
+ }
+ }
+
+ // We're done. Finish last <span> if necessary
+ if ($numberState) {
+ if (self::$waitingText === '') {
+ if (self::$currentState === 'RTL') {
+ $result .= WT_UTF8_PDF;
+ }
+ } else {
+ if (self::$currentState === 'RTL') {
+ self::$waitingText .= WT_UTF8_PDF;
+ }
+ }
+ }
+ self::finishCurrentSpan($result, true);
+
+ // Get rid of any waiting text
+ if (self::$waitingText != '') {
+ if (I18N::direction() === 'rtl' && self::$currentState === 'LTR') {
+ $result .= self::$startRTL;
+ $result .= self::$waitingText;
+ $result .= self::$endRTL;
+ } else {
+ $result .= self::$startLTR;
+ $result .= self::$waitingText;
+ $result .= self::$endLTR;
+ }
+ self::$waitingText = '';
+ }
+
+ // Lastly, do some more cleanups
+
+ // Move leading RTL numeric strings to following LTR text
+ // (this happens when the page direction is RTL and the original text begins with a number and is followed by LTR text)
+ while (substr($result, 0, self::$lenStart + 3) === self::$startRTL . WT_UTF8_LRE) {
+ $spanEnd = strpos($result, self::$endRTL . self::$startLTR);
+ if ($spanEnd === false) {
+ break;
+ }
+ $textSpan = self::stripLrmRlm(substr($result, self::$lenStart + 3, $spanEnd - self::$lenStart - 3));
+ if (I18N::scriptDirection(I18N::textScript($textSpan)) === 'rtl') {
+ break;
+ }
+ $result = self::$startLTR . substr($result, self::$lenStart, $spanEnd - self::$lenStart) . substr($result, $spanEnd + self::$lenStart + self::$lenEnd);
+ break;
+ }
+
+ // On RTL pages, put trailing "." in RTL numeric strings into its own RTL span
+ if (I18N::direction() === 'rtl') {
+ $result = str_replace(WT_UTF8_PDF . '.' . self::$endRTL, WT_UTF8_PDF . self::$endRTL . self::$startRTL . '.' . self::$endRTL, $result);
+ }
+
+ // Trim trailing blanks preceding <br> in LTR text
+ while (self::$previousState != 'RTL') {
+ if (strpos($result, ' <LTRbr>') !== false) {
+ $result = str_replace(' <LTRbr>', '<LTRbr>', $result);
+ continue;
+ }
+ if (strpos($result, '&nbsp;<LTRbr>') !== false) {
+ $result = str_replace('&nbsp;<LTRbr>', '<LTRbr>', $result);
+ continue;
+ }
+ if (strpos($result, ' <br>') !== false) {
+ $result = str_replace(' <br>', '<br>', $result);
+ continue;
+ }
+ if (strpos($result, '&nbsp;<br>') !== false) {
+ $result = str_replace('&nbsp;<br>', '<br>', $result);
+ continue;
+ }
+ break; // Neither space nor &nbsp; : we're done
+ }
+
+ // Trim trailing blanks preceding <br> in RTL text
+ while (true) {
+ if (strpos($result, ' <RTLbr>') !== false) {
+ $result = str_replace(' <RTLbr>', '<RTLbr>', $result);
+ continue;
+ }
+ if (strpos($result, '&nbsp;<RTLbr>') !== false) {
+ $result = str_replace('&nbsp;<RTLbr>', '<RTLbr>', $result);
+ continue;
+ }
+ break; // Neither space nor &nbsp; : we're done
+ }
+
+ // Convert '<LTRbr>' and '<RTLbr /'
+ $result = str_replace(array('<LTRbr>', '<RTLbr>'), array(self::$endLTR . '<br>' . self::$startLTR, self::$endRTL . '<br>' . self::$startRTL), $result);
+
+ // Include leading indeterminate directional text in whatever follows
+ if (substr($result . "\n", 0, self::$lenStart) != self::$startLTR && substr($result . "\n", 0, self::$lenStart) != self::$startRTL && substr($result . "\n", 0, 6) != '<br>') {
+ $leadingText = '';
+ while (true) {
+ if ($result == '') {
+ $result = $leadingText;
+ break;
+ }
+ if (substr($result . "\n", 0, self::$lenStart) != self::$startLTR && substr($result . "\n", 0, self::$lenStart) != self::$startRTL) {
+ $leadingText .= substr($result, 0, 1);
+ $result = substr($result, 1);
+ continue;
+ }
+ $result = substr($result, 0, self::$lenStart) . $leadingText . substr($result, self::$lenStart);
+ break;
+ }
+ }
+
+ // Include solitary "-" and "+" in surrounding RTL text
+ $result = str_replace(array(self::$endRTL . self::$startLTR . '-' . self::$endLTR . self::$startRTL, self::$endRTL . self::$startLTR . '-' . self::$endLTR . self::$startRTL), array('-', '+'), $result);
+
+ // Remove empty spans
+ $result = str_replace(array(self::$startLTR . self::$endLTR, self::$startRTL . self::$endRTL), '', $result);
+
+ // Finally, correct '<LTR>', '</LTR>', '<RTL>', and '</RTL>'
+ switch ($direction) {
+ case 'BOTH':
+ case 'both':
+ // LTR text: <span dir="ltr"> text </span>
+ // RTL text: <span dir="rtl"> text </span>
+ $sLTR = '<span dir="ltr" ' . $class . '>' . $nothing;
+ $eLTR = $nothing . '</span>';
+ $sRTL = '<span dir="rtl" ' . $class . '>' . $nothing;
+ $eRTL = $nothing . '</span>';
+ break;
+ case 'LTR':
+ case 'ltr':
+ // LTR text: <span dir="ltr"> text </span>
+ // RTL text: text
+ $sLTR = '<span dir="ltr" ' . $class . '>' . $nothing;
+ $eLTR = $nothing . '</span>';
+ $sRTL = '';
+ $eRTL = '';
+ break;
+ case 'RTL':
+ case 'rtl':
+ default:
+ // LTR text: text
+ // RTL text: <span dir="rtl"> text </span>
+ $sLTR = '';
+ $eLTR = '';
+ $sRTL = '<span dir="rtl" ' . $class . '>' . $nothing;
+ $eRTL = $nothing . '</span>';
+ break;
+ }
+ $result = str_replace(array(self::$startLTR, self::$endLTR, self::$startRTL, self::$endRTL), array($sLTR, $eLTR, $sRTL, $eRTL), $result);
+
+ return $result;
+ }
+
+ /**
+ * Wrap words that have an asterisk suffix in <u> and </u> tags.
+ * This should underline starred names to show the preferred name.
+ *
+ * @param string $textSpan
+ * @param string $direction
+ *
+ * @return string
+ */
+ public static function starredName($textSpan, $direction) {
+ // To avoid a TCPDF bug that mixes up the word order, insert those <u> and </u> tags
+ // only when page and span directions are identical.
+ if ($direction === strtoupper(I18N::direction())) {
+ while (true) {
+ $starPos = strpos($textSpan, '*');
+ if ($starPos === false) {
+ break;
+ }
+ $trailingText = substr($textSpan, $starPos + 1);
+ $textSpan = substr($textSpan, 0, $starPos);
+ $wordStart = strrpos($textSpan, ' '); // Find the start of the word
+ if ($wordStart !== false) {
+ $leadingText = substr($textSpan, 0, $wordStart + 1);
+ $wordText = substr($textSpan, $wordStart + 1);
+ } else {
+ $leadingText = '';
+ $wordText = $textSpan;
+ }
+ $textSpan = $leadingText . '<u>' . $wordText . '</u>' . $trailingText;
+ }
+ $textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '<u>\1</u>', $textSpan);
+ // The &nbsp; is a work-around for a TCPDF bug eating blanks.
+ $textSpan = str_replace(array(' <u>', '</u> '), array('&nbsp;<u>', '</u>&nbsp;'), $textSpan);
+ } else {
+ // Text and page directions differ: remove the <span> and </span>
+ $textSpan = preg_replace('~(.*)\*~', '\1', $textSpan);
+ $textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '\1', $textSpan);
+ }
+
+ return $textSpan;
+ }
+
+ /**
+ * Get the next character from an input string
+ *
+ * @param string $text
+ * @param string $offset
+ *
+ * @return array
+ */
+ public static function getChar($text, $offset) {
+
+ if ($text == '') {
+ return array('letter' => '', 'length' => 0);
+ }
+
+ $char = substr($text, $offset, 1);
+ $length = 1;
+ if ((ord($char) & 0xE0) == 0xC0) {
+ $length = 2;
+ }
+ if ((ord($char) & 0xF0) == 0xE0) {
+ $length = 3;
+ }
+ if ((ord($char) & 0xF8) == 0xF0) {
+ $length = 4;
+ }
+ $letter = substr($text, $offset, $length);
+
+ return array('letter' => $letter, 'length' => $length);
+ }
+
+ /**
+ * Insert <br> into current span
+ *
+ * @param string $result
+ */
+ public static function breakCurrentSpan(&$result) {
+ // Interrupt the current span, insert that <br>, and then continue the current span
+ $result .= self::$waitingText;
+ self::$waitingText = '';
+
+ $breakString = '<' . self::$currentState . 'br>';
+ $result .= $breakString;
+
+ return;
+ }
+
+ /**
+ * Begin current span
+ *
+ * @param string $result
+ */
+ public static function beginCurrentSpan(&$result) {
+ if (self::$currentState == 'LTR') {
+ $result .= self::$startLTR;
+ }
+ if (self::$currentState == 'RTL') {
+ $result .= self::$startRTL;
+ }
+
+ self::$posSpanStart = strlen($result);
+ }
+
+ /**
+ * Finish current span
+ *
+ * @param string $result
+ * @param bool $theEnd
+ */
+ public static function finishCurrentSpan(&$result, $theEnd = false) {
+ $textSpan = substr($result, self::$posSpanStart);
+ $result = substr($result, 0, self::$posSpanStart);
+
+ // Get rid of empty spans, so that our check for presence of RTL will work
+ $result = str_replace(array(self::$startLTR . self::$endLTR, self::$startRTL . self::$endRTL), '', $result);
+
+ // Look for numeric strings that are times (hh:mm:ss). These have to be separated from surrounding numbers.
+ $tempResult = '';
+ while ($textSpan != '') {
+ $posColon = strpos($textSpan, ':');
+ if ($posColon === false) {
+ break;
+ } // No more possible time strings
+ $posLRE = strpos($textSpan, WT_UTF8_LRE);
+ if ($posLRE === false) {
+ break;
+ } // No more numeric strings
+ $posPDF = strpos($textSpan, WT_UTF8_PDF, $posLRE);
+ if ($posPDF === false) {
+ break;
+ } // No more numeric strings
+
+ $tempResult .= substr($textSpan, 0, $posLRE + 3); // Copy everything preceding the numeric string
+ $numericString = substr($textSpan, $posLRE + 3, $posPDF - $posLRE); // Separate the entire numeric string
+ $textSpan = substr($textSpan, $posPDF + 3);
+ $posColon = strpos($numericString, ':');
+ if ($posColon === false) {
+ // Nothing that looks like a time here
+ $tempResult .= $numericString;
+ continue;
+ }
+ $posBlank = strpos($numericString . ' ', ' ');
+ $posNbsp = strpos($numericString . '&nbsp;', '&nbsp;');
+ if ($posBlank < $posNbsp) {
+ $posSeparator = $posBlank;
+ $lengthSeparator = 1;
+ } else {
+ $posSeparator = $posNbsp;
+ $lengthSeparator = 6;
+ }
+ if ($posColon > $posSeparator) {
+ // We have a time string preceded by a blank: Exclude that blank from the numeric string
+ $tempResult .= substr($numericString, 0, $posSeparator);
+ $tempResult .= WT_UTF8_PDF;
+ $tempResult .= substr($numericString, $posSeparator, $lengthSeparator);
+ $tempResult .= WT_UTF8_LRE;
+ $numericString = substr($numericString, $posSeparator + $lengthSeparator);
+ }
+
+ $posBlank = strpos($numericString, ' ');
+ $posNbsp = strpos($numericString, '&nbsp;');
+ if ($posBlank === false && $posNbsp === false) {
+ // The time string isn't followed by a blank
+ $textSpan = $numericString . $textSpan;
+ continue;
+ }
+
+ // We have a time string followed by a blank: Exclude that blank from the numeric string
+ if ($posBlank === false) {
+ $posSeparator = $posNbsp;
+ $lengthSeparator = 6;
+ } elseif ($posNbsp === false) {
+ $posSeparator = $posBlank;
+ $lengthSeparator = 1;
+ } elseif ($posBlank < $posNbsp) {
+ $posSeparator = $posBlank;
+ $lengthSeparator = 1;
+ } else {
+ $posSeparator = $posNbsp;
+ $lengthSeparator = 6;
+ }
+ $tempResult .= substr($numericString, 0, $posSeparator);
+ $tempResult .= WT_UTF8_PDF;
+ $tempResult .= substr($numericString, $posSeparator, $lengthSeparator);
+ $posSeparator += $lengthSeparator;
+ $numericString = substr($numericString, $posSeparator);
+ $textSpan = WT_UTF8_LRE . $numericString . $textSpan;
+ }
+ $textSpan = $tempResult . $textSpan;
+ $trailingBlanks = '';
+ $trailingBreaks = '';
+
+ /* ****************************** LTR text handling ******************************** */
+
+ if (self::$currentState === 'LTR') {
+ // Move trailing numeric strings to the following RTL text. Include any blanks preceding or following the numeric text too.
+ if (I18N::direction() === 'rtl' && self::$previousState === 'RTL' && !$theEnd) {
+ $trailingString = '';
+ $savedSpan = $textSpan;
+ while ($textSpan !== '') {
+ // Look for trailing spaces and tentatively move them
+ if (substr($textSpan, -1) === ' ') {
+ $trailingString = ' ' . $trailingString;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr($textSpan, -6) === '&nbsp;') {
+ $trailingString = '&nbsp;' . $trailingString;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr($textSpan, -3) !== WT_UTF8_PDF) {
+ // There is no trailing numeric string
+ $textSpan = $savedSpan;
+ break;
+ }
+
+ // We have a numeric string
+ $posStartNumber = strrpos($textSpan, WT_UTF8_LRE);
+ if ($posStartNumber === false) {
+ $posStartNumber = 0;
+ }
+ $trailingString = substr($textSpan, $posStartNumber, strlen($textSpan) - $posStartNumber) . $trailingString;
+ $textSpan = substr($textSpan, 0, $posStartNumber);
+
+ // Look for more spaces and move them too
+ while ($textSpan != '') {
+ if (substr($textSpan, -1) == ' ') {
+ $trailingString = ' ' . $trailingString;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr($textSpan, -6) == '&nbsp;') {
+ $trailingString = '&nbsp;' . $trailingString;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ break;
+ }
+
+ self::$waitingText = $trailingString . self::$waitingText;
+ break;
+ }
+ }
+
+ $savedSpan = $textSpan;
+ // Move any trailing <br>, optionally preceded or followed by blanks, outside this LTR span
+ while ($textSpan != '') {
+ if (substr($textSpan, -1) == ' ') {
+ $trailingBlanks = ' ' . $trailingBlanks;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr('......' . $textSpan, -6) == '&nbsp;') {
+ $trailingBlanks = '&nbsp;' . $trailingBlanks;
+ $textSpan = substr($textSpan, 0, -6);
+ continue;
+ }
+ break;
+ }
+ while (substr($textSpan, -9) == '<LTRbr>') {
+ $trailingBreaks = '<br>' . $trailingBreaks; // Plain <br> because it’s outside a span
+ $textSpan = substr($textSpan, 0, -9);
+ }
+ if ($trailingBreaks != '') {
+ while ($textSpan != '') {
+ if (substr($textSpan, -1) == ' ') {
+ $trailingBreaks = ' ' . $trailingBreaks;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr('......' . $textSpan, -6) == '&nbsp;') {
+ $trailingBreaks = '&nbsp;' . $trailingBreaks;
+ $textSpan = substr($textSpan, 0, -6);
+ continue;
+ }
+ break;
+ }
+ self::$waitingText = $trailingBlanks . self::$waitingText; // Put those trailing blanks inside the following span
+ } else {
+ $textSpan = $savedSpan;
+ }
+
+ $trailingBlanks = '';
+ $trailingPunctuation = '';
+ $trailingID = '';
+ $trailingSeparator = '';
+ $leadingSeparator = '';
+ while (I18N::direction() === 'rtl') {
+ if (strpos($result, self::$startRTL) !== false) {
+ // Remove trailing blanks for inclusion in a separate LTR span
+ while ($textSpan != '') {
+ if (substr($textSpan, -1) === ' ') {
+ $trailingBlanks = ' ' . $trailingBlanks;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr($textSpan, -6) === '&nbsp;') {
+ $trailingBlanks = '&nbsp;' . $trailingBlanks;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ break;
+ }
+
+ // Remove trailing punctuation for inclusion in a separate LTR span
+ if ($textSpan == '') {
+ $trailingChar = "\n";
+ } else {
+ $trailingChar = substr($textSpan, -1);
+ }
+ if (strpos(self::PUNCTUATION, $trailingChar) !== false) {
+ $trailingPunctuation = $trailingChar;
+ $textSpan = substr($textSpan, 0, -1);
+ }
+ }
+
+ // Remove trailing ID numbers that look like "(xnnn)" for inclusion in a separate LTR span
+ while (true) {
+ if (substr($textSpan, -1) != ')') {
+ break;
+ } // There is no trailing ')'
+ $posLeftParen = strrpos($textSpan, '(');
+ if ($posLeftParen === false) {
+ break;
+ } // There is no leading '('
+ $temp = self::stripLrmRlm(substr($textSpan, $posLeftParen)); // Get rid of UTF8 control codes
+
+ // If the parenthesized text doesn't look like an ID number,
+ // we don't want to touch it.
+ // This check won’t work if somebody uses ID numbers with an unusual format.
+ $offset = 1;
+ $charArray = self::getchar($temp, $offset); // Get 1st character of parenthesized text
+ if (strpos(self::NUMBERS, $charArray['letter']) !== false) {
+ break;
+ }
+ $offset += $charArray['length']; // Point at 2nd character of parenthesized text
+ if (strpos(self::NUMBERS, substr($temp, $offset, 1)) === false) {
+ break;
+ }
+ // 1st character of parenthesized text is alpha, 2nd character is a digit; last has to be a digit too
+ if (strpos(self::NUMBERS, substr($temp, -2, 1)) === false) {
+ break;
+ }
+
+ $trailingID = substr($textSpan, $posLeftParen);
+ $textSpan = substr($textSpan, 0, $posLeftParen);
+ break;
+ }
+
+ // Look for " - " or blank preceding the ID number and remove it for inclusion in a separate LTR span
+ if ($trailingID != '') {
+ while ($textSpan != '') {
+ if (substr($textSpan, -1) == ' ') {
+ $trailingSeparator = ' ' . $trailingSeparator;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr($textSpan, -6) == '&nbsp;') {
+ $trailingSeparator = '&nbsp;' . $trailingSeparator;
+ $textSpan = substr($textSpan, 0, -6);
+ continue;
+ }
+ if (substr($textSpan, -1) == '-') {
+ $trailingSeparator = '-' . $trailingSeparator;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ break;
+ }
+ }
+
+ // Look for " - " preceding the text and remove it for inclusion in a separate LTR span
+ $foundSeparator = false;
+ $savedSpan = $textSpan;
+ while ($textSpan != '') {
+ if (substr($textSpan, 0, 1) == ' ') {
+ $leadingSeparator = ' ' . $leadingSeparator;
+ $textSpan = substr($textSpan, 1);
+ continue;
+ }
+ if (substr($textSpan, 0, 6) == '&nbsp;') {
+ $leadingSeparator = '&nbsp;' . $leadingSeparator;
+ $textSpan = substr($textSpan, 6);
+ continue;
+ }
+ if (substr($textSpan, 0, 1) == '-') {
+ $leadingSeparator = '-' . $leadingSeparator;
+ $textSpan = substr($textSpan, 1);
+ $foundSeparator = true;
+ continue;
+ }
+ break;
+ }
+ if (!$foundSeparator) {
+ $textSpan = $savedSpan;
+ $leadingSeparator = '';
+ }
+ break;
+ }
+
+ // We're done: finish the span
+ $textSpan = self::starredName($textSpan, 'LTR'); // Wrap starred name in <u> and </u> tags
+ while (true) {
+ // Remove blanks that precede <LTRbr>
+ if (strpos($textSpan, ' <LTRbr>') !== false) {
+ $textSpan = str_replace(' <LTRbr>', '<LTRbr>', $textSpan);
+ continue;
+ }
+ if (strpos($textSpan, '&nbsp;<LTRbr>') !== false) {
+ $textSpan = str_replace('&nbsp;<LTRbr>', '<LTRbr>', $textSpan);
+ continue;
+ }
+ break;
+ }
+ if ($leadingSeparator != '') {
+ $result = $result . self::$startLTR . $leadingSeparator . self::$endLTR;
+ }
+ $result = $result . $textSpan . self::$endLTR;
+ if ($trailingSeparator != '') {
+ $result = $result . self::$startLTR . $trailingSeparator . self::$endLTR;
+ }
+ if ($trailingID != '') {
+ $result = $result . self::$startLTR . $trailingID . self::$endLTR;
+ }
+ if ($trailingPunctuation != '') {
+ $result = $result . self::$startLTR . $trailingPunctuation . self::$endLTR;
+ }
+ if ($trailingBlanks != '') {
+ $result = $result . self::$startLTR . $trailingBlanks . self::$endLTR;
+ }
+ }
+
+ /* ****************************** RTL text handling ******************************** */
+
+ if (self::$currentState == 'RTL') {
+ $savedSpan = $textSpan;
+
+ // Move any trailing <br>, optionally followed by blanks, outside this RTL span
+ while ($textSpan != '') {
+ if (substr($textSpan, -1) == ' ') {
+ $trailingBlanks = ' ' . $trailingBlanks;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr('......' . $textSpan, -6) == '&nbsp;') {
+ $trailingBlanks = '&nbsp;' . $trailingBlanks;
+ $textSpan = substr($textSpan, 0, -6);
+ continue;
+ }
+ break;
+ }
+ while (substr($textSpan, -9) == '<RTLbr>') {
+ $trailingBreaks = '<br>' . $trailingBreaks; // Plain <br> because it’s outside a span
+ $textSpan = substr($textSpan, 0, -9);
+ }
+ if ($trailingBreaks != '') {
+ self::$waitingText = $trailingBlanks . self::$waitingText; // Put those trailing blanks inside the following span
+ } else {
+ $textSpan = $savedSpan;
+ }
+
+ // Move trailing numeric strings to the following LTR text. Include any blanks preceding or following the numeric text too.
+ if (!$theEnd && I18N::direction() !== 'rtl') {
+ $trailingString = '';
+ $savedSpan = $textSpan;
+ while ($textSpan != '') {
+ // Look for trailing spaces and tentatively move them
+ if (substr($textSpan, -1) === ' ') {
+ $trailingString = ' ' . $trailingString;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr($textSpan, -6) === '&nbsp;') {
+ $trailingString = '&nbsp;' . $trailingString;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr($textSpan, -3) !== WT_UTF8_PDF) {
+ // There is no trailing numeric string
+ $textSpan = $savedSpan;
+ break;
+ }
+
+ // We have a numeric string
+ $posStartNumber = strrpos($textSpan, WT_UTF8_LRE);
+ if ($posStartNumber === false) {
+ $posStartNumber = 0;
+ }
+ $trailingString = substr($textSpan, $posStartNumber, strlen($textSpan) - $posStartNumber) . $trailingString;
+ $textSpan = substr($textSpan, 0, $posStartNumber);
+
+ // Look for more spaces and move them too
+ while ($textSpan != '') {
+ if (substr($textSpan, -1) == ' ') {
+ $trailingString = ' ' . $trailingString;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr($textSpan, -6) == '&nbsp;') {
+ $trailingString = '&nbsp;' . $trailingString;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ break;
+ }
+
+ self::$waitingText = $trailingString . self::$waitingText;
+ break;
+ }
+ }
+
+ // Trailing " - " needs to be prefixed to the following span
+ if (!$theEnd && substr('...' . $textSpan, -3) == ' - ') {
+ $textSpan = substr($textSpan, 0, -3);
+ self::$waitingText = ' - ' . self::$waitingText;
+ }
+
+ while (I18N::direction() === 'rtl') {
+ // Look for " - " preceding <RTLbr> and relocate it to the front of the string
+ $posDashString = strpos($textSpan, ' - <RTLbr>');
+ if ($posDashString === false) {
+ break;
+ }
+ $posStringStart = strrpos(substr($textSpan, 0, $posDashString), '<RTLbr>');
+ if ($posStringStart === false) {
+ $posStringStart = 0;
+ } else {
+ $posStringStart += 9;
+ } // Point to the first char following the last <RTLbr>
+
+ $textSpan = substr($textSpan, 0, $posStringStart) . ' - ' . substr($textSpan, $posStringStart, $posDashString - $posStringStart) . substr($textSpan, $posDashString + 3);
+ }
+
+ // Strip leading spaces from the RTL text
+ $countLeadingSpaces = 0;
+ while ($textSpan != '') {
+ if (substr($textSpan, 0, 1) == ' ') {
+ $countLeadingSpaces++;
+ $textSpan = substr($textSpan, 1);
+ continue;
+ }
+ if (substr($textSpan, 0, 6) == '&nbsp;') {
+ $countLeadingSpaces++;
+ $textSpan = substr($textSpan, 6);
+ continue;
+ }
+ break;
+ }
+
+ // Strip trailing spaces from the RTL text
+ $countTrailingSpaces = 0;
+ while ($textSpan != '') {
+ if (substr($textSpan, -1) == ' ') {
+ $countTrailingSpaces++;
+ $textSpan = substr($textSpan, 0, -1);
+ continue;
+ }
+ if (substr($textSpan, -6) == '&nbsp;') {
+ $countTrailingSpaces++;
+ $textSpan = substr($textSpan, 0, -6);
+ continue;
+ }
+ break;
+ }
+
+ // Look for trailing " -", reverse it, and relocate it to the front of the string
+ if (substr($textSpan, -2) === ' -') {
+ $posDashString = strlen($textSpan) - 2;
+ $posStringStart = strrpos(substr($textSpan, 0, $posDashString), '<RTLbr>');
+ if ($posStringStart === false) {
+ $posStringStart = 0;
+ } else {
+ $posStringStart += 9;
+ } // Point to the first char following the last <RTLbr>
+
+ $textSpan = substr($textSpan, 0, $posStringStart) . '- ' . substr($textSpan, $posStringStart, $posDashString - $posStringStart) . substr($textSpan, $posDashString + 2);
+ }
+
+ if ($countLeadingSpaces != 0) {
+ $newLength = strlen($textSpan) + $countLeadingSpaces;
+ $textSpan = str_pad($textSpan, $newLength, ' ', (I18N::direction() === 'rtl' ? STR_PAD_LEFT : STR_PAD_RIGHT));
+ }
+ if ($countTrailingSpaces != 0) {
+ if (I18N::direction() === 'ltr') {
+ if ($trailingBreaks === '') {
+ // Move trailing RTL spaces to front of following LTR span
+ $newLength = strlen(self::$waitingText) + $countTrailingSpaces;
+ self::$waitingText = str_pad(self::$waitingText, $newLength, ' ', STR_PAD_LEFT);
+ }
+ } else {
+ $newLength = strlen($textSpan) + $countTrailingSpaces;
+ $textSpan = str_pad($textSpan, $newLength, ' ', STR_PAD_RIGHT);
+ }
+ }
+
+ // We're done: finish the span
+ $textSpan = self::starredName($textSpan, 'RTL'); // Wrap starred name in <u> and </u> tags
+ $result = $result . $textSpan . self::$endRTL;
+ }
+
+ if (self::$currentState != 'LTR' && self::$currentState != 'RTL') {
+ $result = $result . $textSpan;
+ }
+
+ $result .= $trailingBreaks; // Get rid of any waiting <br>
+
+ return;
+ }
+
+ /**
+ * @param string $string
+ * @param int $width
+ * @param string $sep
+ * @param bool $cut
+ *
+ * @return string
+ */
+ public static function utf8WordWrap($string, $width = 75, $sep = "\n", $cut = false) {
+ $out = '';
+ while ($string) {
+ if (mb_strlen($string) <= $width) {
+ // Do not wrap any text that is less than the output area.
+ $out .= $string;
+ $string = '';
+ } else {
+ $sub1 = mb_substr($string, 0, $width + 1);
+ if (mb_substr($string, mb_strlen($sub1) - 1, 1) == ' ') {
+ // include words that end by a space immediately after the area.
+ $sub = $sub1;
+ } else {
+ $sub = mb_substr($string, 0, $width);
+ }
+ $spacepos = strrpos($sub, ' ');
+ if ($spacepos === false) {
+ // No space on line?
+ if ($cut) {
+ $out .= $sub . $sep;
+ $string = mb_substr($string, mb_strlen($sub));
+ } else {
+ $spacepos = strpos($string, ' ');
+ if ($spacepos === false) {
+ $out .= $string;
+ $string = '';
+ } else {
+ $out .= substr($string, 0, $spacepos) . $sep;
+ $string = substr($string, $spacepos + 1);
+ }
+ }
+ } else {
+ // Split at space;
+ $out .= substr($string, 0, $spacepos) . $sep;
+ $string = substr($string, $spacepos + 1);
+ }
+ }
+ }
+
+ return $out;
+ }
+}
diff --git a/app/Functions/functions.php b/app/Functions/functions.php
new file mode 100644
index 0000000000..8a5aa29dd1
--- /dev/null
+++ b/app/Functions/functions.php
@@ -0,0 +1,2225 @@
+<?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\Fact;
+use Fisharebest\Webtrees\File;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Site;
+
+/**
+ * Class Functions - common functions
+ */
+class Functions {
+ /**
+ * Check with the webtrees.net server for the latest version of webtrees.
+ * Fetching the remote file can be slow, so check infrequently, and cache the result.
+ * Pass the current versions of webtrees, PHP and MySQL, as the response
+ * may be different for each. The server logs are used to generate
+ * installation statistics which can be found at http://svn.webtrees.net/statistics.html
+ *
+ * @return null|string
+ */
+ public static function fetchLatestVersion() {
+ $last_update_timestamp = Site::getPreference('LATEST_WT_VERSION_TIMESTAMP');
+ if ($last_update_timestamp < WT_TIMESTAMP - 24 * 60 * 60) {
+ $row = Database::prepare("SHOW VARIABLES LIKE 'version'")->fetchOneRow();
+ $params = '?w=' . WT_VERSION . '&p=' . PHP_VERSION . '&m=' . $row->value . '&o=' . (DIRECTORY_SEPARATOR === '/' ? 'u' : 'w');
+ $latest_version_txt = File::fetchUrl('http://dev.webtrees.net/build/latest-version.txt' . $params);
+ if ($latest_version_txt) {
+ Site::setPreference('LATEST_WT_VERSION', $latest_version_txt);
+ Site::setPreference('LATEST_WT_VERSION_TIMESTAMP', WT_TIMESTAMP);
+
+ return $latest_version_txt;
+ } else {
+ // Cannot connect to server - use cached version (if we have one)
+ return Site::getPreference('LATEST_WT_VERSION');
+ }
+ } else {
+ return Site::getPreference('LATEST_WT_VERSION');
+ }
+ }
+
+ /**
+ * Convert a file upload PHP error code into user-friendly text.
+ *
+ * @param int $error_code
+ *
+ * @return string
+ */
+ public static function fileUploadErrorText($error_code) {
+ switch ($error_code) {
+ case UPLOAD_ERR_OK:
+ return I18N::translate('File successfully uploaded');
+ case UPLOAD_ERR_INI_SIZE:
+ case UPLOAD_ERR_FORM_SIZE:
+ // I18N: PHP internal error message - php.net/manual/en/features.file-upload.errors.php
+ return I18N::translate('The uploaded file exceeds the allowed size.');
+ case UPLOAD_ERR_PARTIAL:
+ // I18N: PHP internal error message - php.net/manual/en/features.file-upload.errors.php
+ return I18N::translate('The file was only partially uploaded. Please try again.');
+ case UPLOAD_ERR_NO_FILE:
+ // I18N: PHP internal error message - php.net/manual/en/features.file-upload.errors.php
+ return I18N::translate('No file was received. Please try again.');
+ case UPLOAD_ERR_NO_TMP_DIR:
+ // I18N: PHP internal error message - php.net/manual/en/features.file-upload.errors.php
+ return I18N::translate('The PHP temporary folder is missing.');
+ case UPLOAD_ERR_CANT_WRITE:
+ // I18N: PHP internal error message - php.net/manual/en/features.file-upload.errors.php
+ return I18N::translate('PHP failed to write to disk.');
+ case UPLOAD_ERR_EXTENSION:
+ // I18N: PHP internal error message - php.net/manual/en/features.file-upload.errors.php
+ return I18N::translate('PHP blocked the file because of its extension.');
+ default:
+ return 'Error: ' . $error_code;
+ }
+ }
+
+ /**
+ * get a gedcom subrecord
+ *
+ * searches a gedcom record and returns a subrecord of it. A subrecord is defined starting at a
+ * line with level N and all subsequent lines greater than N until the next N level is reached.
+ * For example, the following is a BIRT subrecord:
+ * <code>1 BIRT
+ * 2 DATE 1 JAN 1900
+ * 2 PLAC Phoenix, Maricopa, Arizona</code>
+ * The following example is the DATE subrecord of the above BIRT subrecord:
+ * <code>2 DATE 1 JAN 1900</code>
+ *
+ * @param int $level the N level of the subrecord to get
+ * @param string $tag a gedcom tag or string to search for in the record (ie 1 BIRT or 2 DATE)
+ * @param string $gedrec the parent gedcom record to search in
+ * @param int $num this allows you to specify which matching <var>$tag</var> to get. Oftentimes a
+ * gedcom record will have more that 1 of the same type of subrecord. An individual may have
+ * multiple events for example. Passing $num=1 would get the first 1. Passing $num=2 would get the
+ * second one, etc.
+ *
+ * @return string the subrecord that was found or an empty string "" if not found.
+ */
+ public static function getSubRecord($level, $tag, $gedrec, $num = 1) {
+ if (empty($gedrec)) {
+ return '';
+ }
+ // -- adding \n before and after gedrec
+ $gedrec = "\n" . $gedrec . "\n";
+ $tag = trim($tag);
+ $searchTarget = "~[\n]" . $tag . "[\s]~";
+ $ct = preg_match_all($searchTarget, $gedrec, $match, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
+ if ($ct == 0) {
+ return '';
+ }
+ if ($ct < $num) {
+ return '';
+ }
+ $pos1 = $match[$num - 1][0][1];
+ $pos2 = strpos($gedrec, "\n$level", $pos1 + 1);
+ if (!$pos2) {
+ $pos2 = strpos($gedrec, "\n1", $pos1 + 1);
+ }
+ if (!$pos2) {
+ $pos2 = strpos($gedrec, "\nWT_", $pos1 + 1); // WT_SPOUSE, WT_FAMILY_ID ...
+ }
+ if (!$pos2) {
+ return ltrim(substr($gedrec, $pos1));
+ }
+ $subrec = substr($gedrec, $pos1, $pos2 - $pos1);
+
+ return ltrim($subrec);
+ }
+
+ /**
+ * get CONT lines
+ *
+ * get the N+1 CONT or CONC lines of a gedcom subrecord
+ *
+ * @param int $nlevel the level of the CONT lines to get
+ * @param string $nrec the gedcom subrecord to search in
+ *
+ * @return string a string with all CONT lines merged
+ */
+ public static function getCont($nlevel, $nrec) {
+ $text = '';
+
+ $subrecords = explode("\n", $nrec);
+ foreach ($subrecords as $thisSubrecord) {
+ if (substr($thisSubrecord, 0, 2) !== $nlevel . ' ') {
+ continue;
+ }
+ $subrecordType = substr($thisSubrecord, 2, 4);
+ if ($subrecordType === 'CONT') {
+ $text .= "\n" . substr($thisSubrecord, 7);
+ }
+ }
+
+ return $text;
+ }
+
+ /**
+ * Sort a list events for the today/upcoming blocks
+ *
+ * @param array $a
+ * @param array $b
+ *
+ * @return int
+ */
+ public static function eventSort($a, $b) {
+ if ($a['jd'] == $b['jd']) {
+ if ($a['anniv'] == $b['anniv']) {
+ return I18N::strcasecmp($a['fact'], $b['fact']);
+ } else {
+ return $a['anniv'] - $b['anniv'];
+ }
+ } else {
+ return $a['jd'] - $b['jd'];
+ }
+ }
+
+ /**
+ * Sort a list events for the today/upcoming blocks
+ *
+ * @param array $a
+ * @param array $b
+ *
+ * @return int
+ */
+ public static function eventSortName($a, $b) {
+ if ($a['jd'] == $b['jd']) {
+ return GedcomRecord::compare($a['record'], $b['record']);
+ } else {
+ return $a['jd'] - $b['jd'];
+ }
+ }
+
+ /**
+ * A multi-key sort
+ * 1. First divide the facts into two arrays one set with dates and one set without dates
+ * 2. Sort each of the two new arrays, the date using the compare date function, the non-dated
+ * using the compare type function
+ * 3. Then merge the arrays back into the original array using the compare type function
+ *
+ * @param Fact[] $arr
+ */
+ public static function sortFacts(&$arr) {
+ $dated = array();
+ $nondated = array();
+ //-- split the array into dated and non-dated arrays
+ $order = 0;
+ foreach ($arr as $event) {
+ $event->sortOrder = $order;
+ $order++;
+ if ($event->getDate()->isOk()) {
+ $dated[] = $event;
+ } else {
+ $nondated[] = $event;
+ }
+ }
+
+ //-- sort each type of array
+ usort($dated, '\Fisharebest\Webtrees\Fact::compareDate');
+ usort($nondated, '\Fisharebest\Webtrees\Fact::compareType');
+
+ //-- merge the arrays back together comparing by Facts
+ $dc = count($dated);
+ $nc = count($nondated);
+ $i = 0;
+ $j = 0;
+ $k = 0;
+ // while there is anything in the dated array continue merging
+ while ($i < $dc) {
+ // compare each fact by type to merge them in order
+ if ($j < $nc && Fact::compareType($dated[$i], $nondated[$j]) > 0) {
+ $arr[$k] = $nondated[$j];
+ $j++;
+ } else {
+ $arr[$k] = $dated[$i];
+ $i++;
+ }
+ $k++;
+ }
+
+ // get anything that might be left in the nondated array
+ while ($j < $nc) {
+ $arr[$k] = $nondated[$j];
+ $j++;
+ $k++;
+ }
+
+ }
+
+ /**
+ * For close family relationships, such as the families tab and the family navigator
+ * Display a tick if both individuals are the same.
+ *
+ * @param Individual $person1
+ * @param Individual $person2
+ *
+ * @return string
+ */
+ public static function getCloseRelationshipName(Individual $person1, Individual $person2) {
+ if ($person1 === $person2) {
+ $label = '<i class="icon-selected" title="' . I18N::translate('self') . '"></i>';
+ } else {
+ $label = self::getRelationshipName(self::getRelationship($person1, $person2));
+ }
+
+ return $label;
+ }
+
+ /**
+ * For facts on the individual/family pages.
+ *
+ * @param Individual $person1
+ * @param Individual $person2
+ *
+ * @return string
+ */
+ public static function getAssociateRelationshipName(Individual $person1, Individual $person2) {
+ if ($person1 === $person2) {
+ $label = I18N::translate('self');
+ } else {
+ $label = get_relationship_name(get_relationship($person1, $person2));
+ }
+
+ return $label;
+ }
+
+ /**
+ * Get relationship between two individuals in the gedcom
+ *
+ * @param Individual $person1 The person to compute the relationship from
+ * @param Individual $person2 The person to compute the relatiohip to
+ * @param int $maxlength The maximum length of path
+ *
+ * @return array|bool An array of nodes on the relationship path, or false if no path found
+ */
+ public static function getRelationship(Individual $person1, Individual $person2, $maxlength = 4) {
+ if ($person1 === $person2) {
+ return false;
+ }
+
+ $spouse_codes = array('M' => 'hus', 'F' => 'wif', 'U' => 'spo');
+ $parent_codes = array('M' => 'fat', 'F' => 'mot', 'U' => 'par');
+ $child_codes = array('M' => 'son', 'F' => 'dau', 'U' => 'chi');
+ $sibling_codes = array('M' => 'bro', 'F' => 'sis', 'U' => 'sib');
+
+ //-- current path nodes
+ $p1nodes = array();
+ //-- ids visited
+ $visited = array();
+
+ //-- set up first node for person1
+ $node1 = array(
+ 'path' => array($person1),
+ 'length' => 0,
+ 'indi' => $person1,
+ 'relations' => array('self'),
+ );
+ $p1nodes[] = $node1;
+
+ $visited[$person1->getXref()] = true;
+
+ $found = false;
+ while (!$found) {
+ //-- search the node list for the shortest path length
+ $shortest = -1;
+ foreach ($p1nodes as $index => $node) {
+ if ($shortest == -1) {
+ $shortest = $index;
+ } else {
+ $node1 = $p1nodes[$shortest];
+ if ($node1['length'] > $node['length']) {
+ $shortest = $index;
+ }
+ }
+ }
+ if ($shortest === -1) {
+ return false;
+ }
+ $node = $p1nodes[$shortest];
+ if ($maxlength == 0 || count($node['path']) <= $maxlength) {
+ $indi = $node['indi'];
+ //-- check all parents and siblings of this node
+ foreach ($indi->getChildFamilies(Auth::PRIV_HIDE) as $family) {
+ $visited[$family->getXref()] = true;
+ foreach ($family->getSpouses(Auth::PRIV_HIDE) as $spouse) {
+ if (!isset($visited[$spouse->getXref()])) {
+ $node1 = $node;
+ $node1['length']++;
+ $node1['path'][] = $spouse;
+ $node1['indi'] = $spouse;
+ $node1['relations'][] = $parent_codes[$spouse->getSex()];
+ $p1nodes[] = $node1;
+ if ($spouse === $person2) {
+ $found = true;
+ $resnode = $node1;
+ } else {
+ $visited[$spouse->getXref()] = true;
+ }
+ }
+ }
+ foreach ($family->getChildren(Auth::PRIV_HIDE) as $child) {
+ if (!isset($visited[$child->getXref()])) {
+ $node1 = $node;
+ $node1['length']++;
+ $node1['path'][] = $child;
+ $node1['indi'] = $child;
+ $node1['relations'][] = $sibling_codes[$child->getSex()];
+ $p1nodes[] = $node1;
+ if ($child === $person2) {
+ $found = true;
+ $resnode = $node1;
+ } else {
+ $visited[$child->getXref()] = true;
+ }
+ }
+ }
+ }
+ //-- check all spouses and children of this node
+ foreach ($indi->getSpouseFamilies(Auth::PRIV_HIDE) as $family) {
+ $visited[$family->getXref()] = true;
+ foreach ($family->getSpouses(Auth::PRIV_HIDE) as $spouse) {
+ if (!in_array($spouse->getXref(), $node1) || !isset($visited[$spouse->getXref()])) {
+ $node1 = $node;
+ $node1['length']++;
+ $node1['path'][] = $spouse;
+ $node1['indi'] = $spouse;
+ $node1['relations'][] = $spouse_codes[$spouse->getSex()];
+ $p1nodes[] = $node1;
+ if ($spouse === $person2) {
+ $found = true;
+ $resnode = $node1;
+ } else {
+ $visited[$spouse->getXref()] = true;
+ }
+ }
+ }
+ foreach ($family->getChildren(Auth::PRIV_HIDE) as $child) {
+ if (!isset($visited[$child->getXref()])) {
+ $node1 = $node;
+ $node1['length']++;
+ $node1['path'][] = $child;
+ $node1['indi'] = $child;
+ $node1['relations'][] = $child_codes[$child->getSex()];
+ $p1nodes[] = $node1;
+ if ($child === $person2) {
+ $found = true;
+ $resnode = $node1;
+ } else {
+ $visited[$child->getXref()] = true;
+ }
+ }
+ }
+ }
+ }
+ unset($p1nodes[$shortest]);
+ }
+
+ return $resnode;
+ }
+
+ /**
+ * Convert the result of get_relationship() into a relationship name.
+ *
+ * @param mixed[][] $nodes
+ *
+ * @return string
+ */
+ public static function getRelationshipName($nodes) {
+ if (!is_array($nodes)) {
+ return '';
+ }
+ $person1 = $nodes['path'][0];
+ $person2 = $nodes['path'][count($nodes['path']) - 1];
+ $path = array_slice($nodes['relations'], 1);
+ // Look for paths with *specific* names first.
+ // Note that every combination must be listed separately, as the same English
+ // name can be used for many different relationships. e.g.
+ // brother’s wife & husband’s sister = sister-in-law.
+ //
+ // $path is an array of the 12 possible gedcom family relationships:
+ // mother/father/parent
+ // brother/sister/sibling
+ // husband/wife/spouse
+ // son/daughter/child
+ //
+ // This is always the shortest path, so “father, daughter” is “half-sister”, not “sister”.
+ //
+ // This is very repetitive in English, but necessary in order to handle the
+ // complexities of other languages.
+
+ return self::getRelationshipNameFromPath(
+ implode('', array_slice($nodes['relations'], 1)),
+ $nodes['path'][0],
+ $nodes['path'][count($nodes['path']) - 1]
+ );
+ }
+
+ /**
+ * @param int $n
+ * @param string $sex
+ *
+ * @return string
+ */
+ public static function cousinName($n, $sex) {
+ switch ($sex) {
+ case 'M':
+ switch ($n) {
+ case 1:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'first cousin');
+ case 2:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'second cousin');
+ case 3:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'third cousin');
+ case 4:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'fourth cousin');
+ case 5:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'fifth cousin');
+ case 6:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'sixth cousin');
+ case 7:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'seventh cousin');
+ case 8:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'eighth cousin');
+ case 9:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'ninth cousin');
+ case 10:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'tenth cousin');
+ case 11:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'eleventh cousin');
+ case 12:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'twelfth cousin');
+ case 13:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'thirteenth cousin');
+ case 14:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'fourteenth cousin');
+ case 15:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'fifteenth cousin');
+ default:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', '%s × cousin', I18N::number($n));
+ }
+ case 'F':
+ switch ($n) {
+ case 1:
+ return I18N::translateContext('FEMALE', 'first cousin');
+ case 2:
+ return I18N::translateContext('FEMALE', 'second cousin');
+ case 3:
+ return I18N::translateContext('FEMALE', 'third cousin');
+ case 4:
+ return I18N::translateContext('FEMALE', 'fourth cousin');
+ case 5:
+ return I18N::translateContext('FEMALE', 'fifth cousin');
+ case 6:
+ return I18N::translateContext('FEMALE', 'sixth cousin');
+ case 7:
+ return I18N::translateContext('FEMALE', 'seventh cousin');
+ case 8:
+ return I18N::translateContext('FEMALE', 'eighth cousin');
+ case 9:
+ return I18N::translateContext('FEMALE', 'ninth cousin');
+ case 10:
+ return I18N::translateContext('FEMALE', 'tenth cousin');
+ case 11:
+ return I18N::translateContext('FEMALE', 'eleventh cousin');
+ case 12:
+ return I18N::translateContext('FEMALE', 'twelfth cousin');
+ case 13:
+ return I18N::translateContext('FEMALE', 'thirteenth cousin');
+ case 14:
+ return I18N::translateContext('FEMALE', 'fourteenth cousin');
+ case 15:
+ return I18N::translateContext('FEMALE', 'fifteenth cousin');
+ default:
+ return I18N::translateContext('FEMALE', '%s × cousin', I18N::number($n));
+ }
+ default:
+ switch ($n) {
+ case 1:
+ return I18N::translateContext('MALE/FEMALE', 'first cousin');
+ case 2:
+ return I18N::translateContext('MALE/FEMALE', 'second cousin');
+ case 3:
+ return I18N::translateContext('MALE/FEMALE', 'third cousin');
+ case 4:
+ return I18N::translateContext('MALE/FEMALE', 'fourth cousin');
+ case 5:
+ return I18N::translateContext('MALE/FEMALE', 'fifth cousin');
+ case 6:
+ return I18N::translateContext('MALE/FEMALE', 'sixth cousin');
+ case 7:
+ return I18N::translateContext('MALE/FEMALE', 'seventh cousin');
+ case 8:
+ return I18N::translateContext('MALE/FEMALE', 'eighth cousin');
+ case 9:
+ return I18N::translateContext('MALE/FEMALE', 'ninth cousin');
+ case 10:
+ return I18N::translateContext('MALE/FEMALE', 'tenth cousin');
+ case 11:
+ return I18N::translateContext('MALE/FEMALE', 'eleventh cousin');
+ case 12:
+ return I18N::translateContext('MALE/FEMALE', 'twelfth cousin');
+ case 13:
+ return I18N::translateContext('MALE/FEMALE', 'thirteenth cousin');
+ case 14:
+ return I18N::translateContext('MALE/FEMALE', 'fourteenth cousin');
+ case 15:
+ return I18N::translateContext('MALE/FEMALE', 'fifteenth cousin');
+ default:
+ return I18N::translateContext('MALE/FEMALE', '%s × cousin', I18N::number($n));
+ }
+ }
+ }
+
+ /**
+ * A variation on cousin_name(), for constructs such as “sixth great-nephew”
+ * Currently used only by Spanish relationship names.
+ *
+ * @param int $n
+ * @param string $sex
+ * @param string $relation
+ *
+ * @return string
+ */
+ public static function cousinName2($n, $sex, $relation) {
+ switch ($sex) {
+ case 'M':
+ switch ($n) {
+ case 1: // I18N: A Spanish relationship name, such as third great-nephew
+ return I18N::translateContext('MALE', 'first %s', $relation);
+ case 2:
+ return I18N::translateContext('MALE', 'second %s', $relation);
+ case 3:
+ return I18N::translateContext('MALE', 'third %s', $relation);
+ case 4:
+ return I18N::translateContext('MALE', 'fourth %s', $relation);
+ case 5:
+ return I18N::translateContext('MALE', 'fifth %s', $relation);
+ default: // I18N: A Spanish relationship name, such as third great-nephew
+ return I18N::translateContext('MALE', '%1$s × %2$s', I18N::number($n), $relation);
+ }
+ case 'F':
+ switch ($n) {
+ case 1: // I18N: A Spanish relationship name, such as third great-nephew
+ return I18N::translateContext('FEMALE', 'first %s', $relation);
+ case 2:
+ return I18N::translateContext('FEMALE', 'second %s', $relation);
+ case 3:
+ return I18N::translateContext('FEMALE', 'third %s', $relation);
+ case 4:
+ return I18N::translateContext('FEMALE', 'fourth %s', $relation);
+ case 5:
+ return I18N::translateContext('FEMALE', 'fifth %s', $relation);
+ default: // I18N: A Spanish relationship name, such as third great-nephew
+ return I18N::translateContext('FEMALE', '%1$s × %2$s', I18N::number($n), $relation);
+ }
+ default:
+ switch ($n) {
+ case 1: // I18N: A Spanish relationship name, such as third great-nephew
+ return I18N::translateContext('MALE/FEMALE', 'first %s', $relation);
+ case 2:
+ return I18N::translateContext('MALE/FEMALE', 'second %s', $relation);
+ case 3:
+ return I18N::translateContext('MALE/FEMALE', 'third %s', $relation);
+ case 4:
+ return I18N::translateContext('MALE/FEMALE', 'fourth %s', $relation);
+ case 5:
+ return I18N::translateContext('MALE/FEMALE', 'fifth %s', $relation);
+ default: // I18N: A Spanish relationship name, such as third great-nephew
+ return I18N::translateContext('MALE/FEMALE', '%1$s × %2$s', I18N::number($n), $relation);
+ }
+ }
+ }
+
+ /**
+ * @param string $path
+ * @param Individual $person1
+ * @param Individual $person2
+ *
+ * @return string
+ */
+ public static function getRelationshipNameFromPath($path, Individual $person1 = null, Individual $person2 = null) {
+ if (!preg_match('/^(mot|fat|par|hus|wif|spo|son|dau|chi|bro|sis|sib)*$/', $path)) {
+ // TODO: Update all the “3 RELA ” values in class_person
+ return '<span class="error">' . $path . '</span>';
+ }
+ // The path does not include the starting person. In some languages, the
+ // translation for a man’s (relative) is different from a woman’s (relative),
+ // due to inflection.
+ $sex1 = $person1 ? $person1->getSex() : 'U';
+
+ // The sex of the last person in the relationship determines the name in
+ // many cases. e.g. great-aunt / great-uncle
+ if (preg_match('/(fat|hus|son|bro)$/', $path)) {
+ $sex2 = 'M';
+ } elseif (preg_match('/(mot|wif|dau|sis)$/', $path)) {
+ $sex2 = 'F';
+ } else {
+ $sex2 = 'U';
+ }
+
+ switch ($path) {
+ case '':
+ return I18N::translate('self');
+ // Level One relationships
+ case 'mot':
+ return I18N::translate('mother');
+ case 'fat':
+ return I18N::translate('father');
+ case 'par':
+ return I18N::translate('parent');
+ case 'hus':
+ if ($person1 && $person2) {
+ foreach ($person1->getSpouseFamilies() as $family) {
+ if ($person2 === $family->getSpouse($person1)) {
+ if ($family->getFacts('_NMR')) {
+ if ($family->getFacts(WT_EVENTS_DIV)) {
+ return I18N::translateContext('MALE', 'ex-partner');
+ } else {
+ return I18N::translateContext('MALE', 'partner');
+ }
+ } elseif ($family->getFacts(WT_EVENTS_DIV)) {
+ return I18N::translate('ex-husband');
+ }
+ }
+ }
+ }
+
+ return I18N::translate('husband');
+ case 'wif':
+ if ($person1 && $person1) {
+ foreach ($person1->getSpouseFamilies() as $family) {
+ if ($person2 === $family->getSpouse($person1)) {
+ if ($family->getFacts('_NMR')) {
+ if ($family->getFacts(WT_EVENTS_DIV)) {
+ return I18N::translateContext('FEMALE', 'ex-partner');
+ } else {
+ return I18N::translateContext('FEMALE', 'partner');
+ }
+ } elseif ($family->getFacts(WT_EVENTS_DIV)) {
+ return I18N::translate('ex-wife');
+ }
+ }
+ }
+ }
+
+ return I18N::translate('wife');
+ case 'spo':
+ if ($person1 && $person2) {
+ foreach ($person1->getSpouseFamilies() as $family) {
+ if ($person2 === $family->getSpouse($person1)) {
+ if ($family->getFacts('_NMR')) {
+ if ($family->getFacts(WT_EVENTS_DIV)) {
+ return I18N::translateContext('MALE/FEMALE', 'ex-partner');
+ } else {
+ return I18N::translateContext('MALE/FEMALE', 'partner');
+ }
+ } elseif ($family->getFacts(WT_EVENTS_DIV)) {
+ return I18N::translate('ex-spouse');
+ }
+ }
+ }
+ }
+
+ return I18N::translate('spouse');
+ case 'son':
+ return I18N::translate('son');
+ case 'dau':
+ return I18N::translate('daughter');
+ case 'chi':
+ return I18N::translate('child');
+ case 'bro':
+ if ($person1 && $person2) {
+ $dob1 = $person1->getBirthDate();
+ $dob2 = $person2->getBirthDate();
+ if ($dob1->isOK() && $dob2->isOK()) {
+ if (abs($dob1->julianDay() - $dob2->julianDay()) < 2 && !$dob1->minimumDate()->d !== 0 && !$dob2->minimumDate()->d !== 0) {
+ // Exclude BEF, AFT, etc.
+ return I18N::translate('twin brother');
+ } elseif ($dob1->maximumJulianDay() < $dob2->minimumJulianDay()) {
+ return I18N::translate('younger brother');
+ } elseif ($dob1->minimumJulianDay() > $dob2->maximumJulianDay()) {
+ return I18N::translate('elder brother');
+ }
+ }
+ }
+
+ return I18N::translate('brother');
+ case 'sis':
+ if ($person1 && $person2) {
+ $dob1 = $person1->getBirthDate();
+ $dob2 = $person2->getBirthDate();
+ if ($dob1->isOK() && $dob2->isOK()) {
+ if (abs($dob1->julianDay() - $dob2->julianDay()) < 2 && !$dob1->minimumDate()->d !== 0 && !$dob2->minimumDate()->d !== 0) {
+ // Exclude BEF, AFT, etc.
+ return I18N::translate('twin sister');
+ } elseif ($dob1->maximumJulianDay() < $dob2->minimumJulianDay()) {
+ return I18N::translate('younger sister');
+ } elseif ($dob1->minimumJulianDay() > $dob2->maximumJulianDay()) {
+ return I18N::translate('elder sister');
+ }
+ }
+ }
+
+ return I18N::translate('sister');
+ case 'sib':
+ if ($person1 && $person2) {
+ $dob1 = $person1->getBirthDate();
+ $dob2 = $person2->getBirthDate();
+ if ($dob1->isOK() && $dob2->isOK()) {
+ if (abs($dob1->julianDay() - $dob2->julianDay()) < 2 && !$dob1->minimumDate()->d !== 0 && !$dob2->minimumDate()->d !== 0) {
+ // Exclude BEF, AFT, etc.
+ return I18N::translate('twin sibling');
+ } elseif ($dob1->maximumJulianDay() < $dob2->minimumJulianDay()) {
+ return I18N::translate('younger sibling');
+ } elseif ($dob1->minimumJulianDay() > $dob2->maximumJulianDay()) {
+ return I18N::translate('elder sibling');
+ }
+ }
+ }
+
+ return I18N::translate('sibling');
+
+ // Level Two relationships
+ case 'brochi':
+ return I18N::translateContext('brother’s child', 'nephew/niece');
+ case 'brodau':
+ return I18N::translateContext('brother’s daughter', 'niece');
+ case 'broson':
+ return I18N::translateContext('brother’s son', 'nephew');
+ case 'browif':
+ return I18N::translateContext('brother’s wife', 'sister-in-law');
+ case 'chichi':
+ return I18N::translateContext('child’s child', 'grandchild');
+ case 'chidau':
+ return I18N::translateContext('child’s daughter', 'granddaughter');
+ case 'chihus':
+ return I18N::translateContext('child’s husband', 'son-in-law');
+ case 'chison':
+ return I18N::translateContext('child’s son', 'grandson');
+ case 'chispo':
+ return I18N::translateContext('child’s spouse', 'son/daughter-in-law');
+ case 'chiwif':
+ return I18N::translateContext('child’s wife', 'daughter-in-law');
+ case 'dauchi':
+ return I18N::translateContext('daughter’s child', 'grandchild');
+ case 'daudau':
+ return I18N::translateContext('daughter’s daughter', 'granddaughter');
+ case 'dauhus':
+ return I18N::translateContext('daughter’s husband', 'son-in-law');
+ case 'dauson':
+ return I18N::translateContext('daughter’s son', 'grandson');
+ case 'fatbro':
+ return I18N::translateContext('father’s brother', 'uncle');
+ case 'fatchi':
+ return I18N::translateContext('father’s child', 'half-sibling');
+ case 'fatdau':
+ return I18N::translateContext('father’s daughter', 'half-sister');
+ case 'fatfat':
+ return I18N::translateContext('father’s father', 'paternal grandfather');
+ case 'fatmot':
+ return I18N::translateContext('father’s mother', 'paternal grandmother');
+ case 'fatpar':
+ return I18N::translateContext('father’s parent', 'paternal grandparent');
+ case 'fatsib':
+ return I18N::translateContext('father’s sibling', 'aunt/uncle');
+ case 'fatsis':
+ return I18N::translateContext('father’s sister', 'aunt');
+ case 'fatson':
+ return I18N::translateContext('father’s son', 'half-brother');
+ case 'fatwif':
+ return I18N::translateContext('father’s wife', 'step-mother');
+ case 'husbro':
+ return I18N::translateContext('husband’s brother', 'brother-in-law');
+ case 'huschi':
+ return I18N::translateContext('husband’s child', 'step-child');
+ case 'husdau':
+ return I18N::translateContext('husband’s daughter', 'step-daughter');
+ case 'husfat':
+ return I18N::translateContext('husband’s father', 'father-in-law');
+ case 'husmot':
+ return I18N::translateContext('husband’s mother', 'mother-in-law');
+ case 'hussib':
+ return I18N::translateContext('husband’s sibling', 'brother/sister-in-law');
+ case 'hussis':
+ return I18N::translateContext('husband’s sister', 'sister-in-law');
+ case 'husson':
+ return I18N::translateContext('husband’s son', 'step-son');
+ case 'motbro':
+ return I18N::translateContext('mother’s brother', 'uncle');
+ case 'motchi':
+ return I18N::translateContext('mother’s child', 'half-sibling');
+ case 'motdau':
+ return I18N::translateContext('mother’s daughter', 'half-sister');
+ case 'motfat':
+ return I18N::translateContext('mother’s father', 'maternal grandfather');
+ case 'mothus':
+ return I18N::translateContext('mother’s husband', 'step-father');
+ case 'motmot':
+ return I18N::translateContext('mother’s mother', 'maternal grandmother');
+ case 'motpar':
+ return I18N::translateContext('mother’s parent', 'maternal grandparent');
+ case 'motsib':
+ return I18N::translateContext('mother’s sibling', 'aunt/uncle');
+ case 'motsis':
+ return I18N::translateContext('mother’s sister', 'aunt');
+ case 'motson':
+ return I18N::translateContext('mother’s son', 'half-brother');
+ case 'parbro':
+ return I18N::translateContext('parent’s brother', 'uncle');
+ case 'parchi':
+ return I18N::translateContext('parent’s child', 'half-sibling');
+ case 'pardau':
+ return I18N::translateContext('parent’s daughter', 'half-sister');
+ case 'parfat':
+ return I18N::translateContext('parent’s father', 'grandfather');
+ case 'parmot':
+ return I18N::translateContext('parent’s mother', 'grandmother');
+ case 'parpar':
+ return I18N::translateContext('parent’s parent', 'grandparent');
+ case 'parsib':
+ return I18N::translateContext('parent’s sibling', 'aunt/uncle');
+ case 'parsis':
+ return I18N::translateContext('parent’s sister', 'aunt');
+ case 'parson':
+ return I18N::translateContext('parent’s son', 'half-brother');
+ case 'parspo':
+ return I18N::translateContext('parent’s spouse', 'step-parent');
+ case 'sibchi':
+ return I18N::translateContext('sibling’s child', 'nephew/niece');
+ case 'sibdau':
+ return I18N::translateContext('sibling’s daughter', 'niece');
+ case 'sibson':
+ return I18N::translateContext('sibling’s son', 'nephew');
+ case 'sibspo':
+ return I18N::translateContext('sibling’s spouse', 'brother/sister-in-law');
+ case 'sischi':
+ return I18N::translateContext('sister’s child', 'nephew/niece');
+ case 'sisdau':
+ return I18N::translateContext('sister’s daughter', 'niece');
+ case 'sishus':
+ return I18N::translateContext('sister’s husband', 'brother-in-law');
+ case 'sisson':
+ return I18N::translateContext('sister’s son', 'nephew');
+ case 'sonchi':
+ return I18N::translateContext('son’s child', 'grandchild');
+ case 'sondau':
+ return I18N::translateContext('son’s daughter', 'granddaughter');
+ case 'sonson':
+ return I18N::translateContext('son’s son', 'grandson');
+ case 'sonwif':
+ return I18N::translateContext('son’s wife', 'daughter-in-law');
+ case 'spobro':
+ return I18N::translateContext('spouse’s brother', 'brother-in-law');
+ case 'spochi':
+ return I18N::translateContext('spouse’s child', 'step-child');
+ case 'spodau':
+ return I18N::translateContext('spouse’s daughter', 'step-daughter');
+ case 'spofat':
+ return I18N::translateContext('spouse’s father', 'father-in-law');
+ case 'spomot':
+ return I18N::translateContext('spouse’s mother', 'mother-in-law');
+ case 'sposis':
+ return I18N::translateContext('spouse’s sister', 'sister-in-law');
+ case 'sposon':
+ return I18N::translateContext('spouse’s son', 'step-son');
+ case 'spopar':
+ return I18N::translateContext('spouse’s parent', 'mother/father-in-law');
+ case 'sposib':
+ return I18N::translateContext('spouse’s sibling', 'brother/sister-in-law');
+ case 'wifbro':
+ return I18N::translateContext('wife’s brother', 'brother-in-law');
+ case 'wifchi':
+ return I18N::translateContext('wife’s child', 'step-child');
+ case 'wifdau':
+ return I18N::translateContext('wife’s daughter', 'step-daughter');
+ case 'wiffat':
+ return I18N::translateContext('wife’s father', 'father-in-law');
+ case 'wifmot':
+ return I18N::translateContext('wife’s mother', 'mother-in-law');
+ case 'wifsib':
+ return I18N::translateContext('wife’s sibling', 'brother/sister-in-law');
+ case 'wifsis':
+ return I18N::translateContext('wife’s sister', 'sister-in-law');
+ case 'wifson':
+ return I18N::translateContext('wife’s son', 'step-son');
+
+ // Level Three relationships
+ // I have commented out some of the unknown-sex relationships that are unlikely to to occur.
+ // Feel free to add them in, if you think they might be needed
+ case 'brochichi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s child’s child', 'great-nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) brother’s child’s child', 'great-nephew/niece');
+ }
+ case 'brochidau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s child’s daughter', 'great-niece');
+ } else {
+ return I18N::translateContext('(a woman’s) brother’s child’s daughter', 'great-niece');
+ }
+ case 'brochison':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s child’s son', 'great-nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) brother’s child’s son', 'great-nephew');
+ }
+ case 'brodauchi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s daughter’s child', 'great-nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) brother’s daughter’s child', 'great-nephew/niece');
+ }
+ case 'brodaudau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s daughter’s daughter', 'great-niece');
+ } else {
+ return I18N::translateContext('(a woman’s) brother’s daughter’s daughter', 'great-niece');
+ }
+ case 'brodauhus':
+ return I18N::translateContext('brother’s daughter’s husband', 'nephew-in-law');
+ case 'brodauson':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s daughter’s son', 'great-nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) brother’s daughter’s son', 'great-nephew');
+ }
+ case 'brosonchi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s son’s child', 'great-nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) brother’s son’s child', 'great-nephew/niece');
+ }
+ case 'brosondau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s son’s daughter', 'great-niece');
+ } else {
+ return I18N::translateContext('(a woman’s) brother’s son’s daughter', 'great-niece');
+ }
+ case 'brosonson':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s son’s son', 'great-nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) brother’s son’s son', 'great-nephew');
+ }
+ case 'brosonwif':
+ return I18N::translateContext('brother’s son’s wife', 'niece-in-law');
+ case 'browifbro':
+ return I18N::translateContext('brother’s wife’s brother', 'brother-in-law');
+ case 'browifsib':
+ return I18N::translateContext('brother’s wife’s sibling', 'brother/sister-in-law');
+ case 'browifsis':
+ return I18N::translateContext('brother’s wife’s sister', 'sister-in-law');
+ case 'chichichi':
+ return I18N::translateContext('child’s child’s child', 'great-grandchild');
+ case 'chichidau':
+ return I18N::translateContext('child’s child’s daughter', 'great-granddaughter');
+ case 'chichison':
+ return I18N::translateContext('child’s child’s son', 'great-grandson');
+ case 'chidauchi':
+ return I18N::translateContext('child’s daughter’s child', 'great-grandchild');
+ case 'chidaudau':
+ return I18N::translateContext('child’s daughter’s daughter', 'great-granddaughter');
+ case 'chidauhus':
+ return I18N::translateContext('child’s daughter’s husband', 'granddaughter’s husband');
+ case 'chidauson':
+ return I18N::translateContext('child’s daughter’s son', 'great-grandson');
+ case 'chisonchi':
+ return I18N::translateContext('child’s son’s child', 'great-grandchild');
+ case 'chisondau':
+ return I18N::translateContext('child’s son’s daughter', 'great-granddaughter');
+ case 'chisonson':
+ return I18N::translateContext('child’s son’s son', 'great-grandson');
+ case 'chisonwif':
+ return I18N::translateContext('child’s son’s wife', 'grandson’s wife');
+ case 'dauchichi':
+ return I18N::translateContext('daughter’s child’s child', 'great-grandchild');
+ case 'dauchidau':
+ return I18N::translateContext('daughter’s child’s daughter', 'great-granddaughter');
+ case 'dauchison':
+ return I18N::translateContext('daughter’s child’s son', 'great-grandson');
+ case 'daudauchi':
+ return I18N::translateContext('daughter’s daughter’s child', 'great-grandchild');
+ case 'daudaudau':
+ return I18N::translateContext('daughter’s daughter’s daughter', 'great-granddaughter');
+ case 'daudauhus':
+ return I18N::translateContext('daughter’s daughter’s husband', 'granddaughter’s husband');
+ case 'daudauson':
+ return I18N::translateContext('daughter’s daughter’s son', 'great-grandson');
+ case 'dauhusfat':
+ return I18N::translateContext('daughter’s husband’s father', 'son-in-law’s father');
+ case 'dauhusmot':
+ return I18N::translateContext('daughter’s husband’s mother', 'son-in-law’s mother');
+ case 'dauhuspar':
+ return I18N::translateContext('daughter’s husband’s parent', 'son-in-law’s parent');
+ case 'dausonchi':
+ return I18N::translateContext('daughter’s son’s child', 'great-grandchild');
+ case 'dausondau':
+ return I18N::translateContext('daughter’s son’s daughter', 'great-granddaughter');
+ case 'dausonson':
+ return I18N::translateContext('daughter’s son’s son', 'great-grandson');
+ case 'dausonwif':
+ return I18N::translateContext('daughter’s son’s wife', 'grandson’s wife');
+ case 'fatbrochi':
+ return I18N::translateContext('father’s brother’s child', 'first cousin');
+ case 'fatbrodau':
+ return I18N::translateContext('father’s brother’s daughter', 'first cousin');
+ case 'fatbroson':
+ return I18N::translateContext('father’s brother’s son', 'first cousin');
+ case 'fatbrowif':
+ return I18N::translateContext('father’s brother’s wife', 'aunt');
+ case 'fatfatbro':
+ return I18N::translateContext('father’s father’s brother', 'great-uncle');
+ case 'fatfatfat':
+ return I18N::translateContext('father’s father’s father', 'great-grandfather');
+ case 'fatfatmot':
+ return I18N::translateContext('father’s father’s mother', 'great-grandmother');
+ case 'fatfatpar':
+ return I18N::translateContext('father’s father’s parent', 'great-grandparent');
+ case 'fatfatsib':
+ return I18N::translateContext('father’s father’s sibling', 'great-aunt/uncle');
+ case 'fatfatsis':
+ return I18N::translateContext('father’s father’s sister', 'great-aunt');
+ case 'fatmotbro':
+ return I18N::translateContext('father’s mother’s brother', 'great-uncle');
+ case 'fatmotfat':
+ return I18N::translateContext('father’s mother’s father', 'great-grandfather');
+ case 'fatmotmot':
+ return I18N::translateContext('father’s mother’s mother', 'great-grandmother');
+ case 'fatmotpar':
+ return I18N::translateContext('father’s mother’s parent', 'great-grandparent');
+ case 'fatmotsib':
+ return I18N::translateContext('father’s mother’s sibling', 'great-aunt/uncle');
+ case 'fatmotsis':
+ return I18N::translateContext('father’s mother’s sister', 'great-aunt');
+ case 'fatparbro':
+ return I18N::translateContext('father’s parent’s brother', 'great-uncle');
+ case 'fatparfat':
+ return I18N::translateContext('father’s parent’s father', 'great-grandfather');
+ case 'fatparmot':
+ return I18N::translateContext('father’s parent’s mother', 'great-grandmother');
+ case 'fatparpar':
+ return I18N::translateContext('father’s parent’s parent', 'great-grandparent');
+ case 'fatparsib':
+ return I18N::translateContext('father’s parent’s sibling', 'great-aunt/uncle');
+ case 'fatparsis':
+ return I18N::translateContext('father’s parent’s sister', 'great-aunt');
+ case 'fatsischi':
+ return I18N::translateContext('father’s sister’s child', 'first cousin');
+ case 'fatsisdau':
+ return I18N::translateContext('father’s sister’s daughter', 'first cousin');
+ case 'fatsishus':
+ return I18N::translateContext('father’s sister’s husband', 'uncle');
+ case 'fatsisson':
+ return I18N::translateContext('father’s sister’s son', 'first cousin');
+ case 'fatwifchi':
+ return I18N::translateContext('father’s wife’s child', 'step-sibling');
+ case 'fatwifdau':
+ return I18N::translateContext('father’s wife’s daughter', 'step-sister');
+ case 'fatwifson':
+ return I18N::translateContext('father’s wife’s son', 'step-brother');
+ case 'husbrowif':
+ return I18N::translateContext('husband’s brother’s wife', 'sister-in-law');
+ case 'hussishus':
+ return I18N::translateContext('husband’s sister’s husband', 'brother-in-law');
+ case 'motbrochi':
+ return I18N::translateContext('mother’s brother’s child', 'first cousin');
+ case 'motbrodau':
+ return I18N::translateContext('mother’s brother’s daughter', 'first cousin');
+ case 'motbroson':
+ return I18N::translateContext('mother’s brother’s son', 'first cousin');
+ case 'motbrowif':
+ return I18N::translateContext('mother’s brother’s wife', 'aunt');
+ case 'motfatbro':
+ return I18N::translateContext('mother’s father’s brother', 'great-uncle');
+ case 'motfatfat':
+ return I18N::translateContext('mother’s father’s father', 'great-grandfather');
+ case 'motfatmot':
+ return I18N::translateContext('mother’s father’s mother', 'great-grandmother');
+ case 'motfatpar':
+ return I18N::translateContext('mother’s father’s parent', 'great-grandparent');
+ case 'motfatsib':
+ return I18N::translateContext('mother’s father’s sibling', 'great-aunt/uncle');
+ case 'motfatsis':
+ return I18N::translateContext('mother’s father’s sister', 'great-aunt');
+ case 'mothuschi':
+ return I18N::translateContext('mother’s husband’s child', 'step-sibling');
+ case 'mothusdau':
+ return I18N::translateContext('mother’s husband’s daughter', 'step-sister');
+ case 'mothusson':
+ return I18N::translateContext('mother’s husband’s son', 'step-brother');
+ case 'motmotbro':
+ return I18N::translateContext('mother’s mother’s brother', 'great-uncle');
+ case 'motmotfat':
+ return I18N::translateContext('mother’s mother’s father', 'great-grandfather');
+ case 'motmotmot':
+ return I18N::translateContext('mother’s mother’s mother', 'great-grandmother');
+ case 'motmotpar':
+ return I18N::translateContext('mother’s mother’s parent', 'great-grandparent');
+ case 'motmotsib':
+ return I18N::translateContext('mother’s mother’s sibling', 'great-aunt/uncle');
+ case 'motmotsis':
+ return I18N::translateContext('mother’s mother’s sister', 'great-aunt');
+ case 'motparbro':
+ return I18N::translateContext('mother’s parent’s brother', 'great-uncle');
+ case 'motparfat':
+ return I18N::translateContext('mother’s parent’s father', 'great-grandfather');
+ case 'motparmot':
+ return I18N::translateContext('mother’s parent’s mother', 'great-grandmother');
+ case 'motparpar':
+ return I18N::translateContext('mother’s parent’s parent', 'great-grandparent');
+ case 'motparsib':
+ return I18N::translateContext('mother’s parent’s sibling', 'great-aunt/uncle');
+ case 'motparsis':
+ return I18N::translateContext('mother’s parent’s sister', 'great-aunt');
+ case 'motsischi':
+ return I18N::translateContext('mother’s sister’s child', 'first cousin');
+ case 'motsisdau':
+ return I18N::translateContext('mother’s sister’s daughter', 'first cousin');
+ case 'motsishus':
+ return I18N::translateContext('mother’s sister’s husband', 'uncle');
+ case 'motsisson':
+ return I18N::translateContext('mother’s sister’s son', 'first cousin');
+ case 'parbrowif':
+ return I18N::translateContext('parent’s brother’s wife', 'aunt');
+ case 'parfatbro':
+ return I18N::translateContext('parent’s father’s brother', 'great-uncle');
+ case 'parfatfat':
+ return I18N::translateContext('parent’s father’s father', 'great-grandfather');
+ case 'parfatmot':
+ return I18N::translateContext('parent’s father’s mother', 'great-grandmother');
+ case 'parfatpar':
+ return I18N::translateContext('parent’s father’s parent', 'great-grandparent');
+ case 'parfatsib':
+ return I18N::translateContext('parent’s father’s sibling', 'great-aunt/uncle');
+ case 'parfatsis':
+ return I18N::translateContext('parent’s father’s sister', 'great-aunt');
+ case 'parmotbro':
+ return I18N::translateContext('parent’s mother’s brother', 'great-uncle');
+ case 'parmotfat':
+ return I18N::translateContext('parent’s mother’s father', 'great-grandfather');
+ case 'parmotmot':
+ return I18N::translateContext('parent’s mother’s mother', 'great-grandmother');
+ case 'parmotpar':
+ return I18N::translateContext('parent’s mother’s parent', 'great-grandparent');
+ case 'parmotsib':
+ return I18N::translateContext('parent’s mother’s sibling', 'great-aunt/uncle');
+ case 'parmotsis':
+ return I18N::translateContext('parent’s mother’s sister', 'great-aunt');
+ case 'parparbro':
+ return I18N::translateContext('parent’s parent’s brother', 'great-uncle');
+ case 'parparfat':
+ return I18N::translateContext('parent’s parent’s father', 'great-grandfather');
+ case 'parparmot':
+ return I18N::translateContext('parent’s parent’s mother', 'great-grandmother');
+ case 'parparpar':
+ return I18N::translateContext('parent’s parent’s parent', 'great-grandparent');
+ case 'parparsib':
+ return I18N::translateContext('parent’s parent’s sibling', 'great-aunt/uncle');
+ case 'parparsis':
+ return I18N::translateContext('parent’s parent’s sister', 'great-aunt');
+ case 'parsishus':
+ return I18N::translateContext('parent’s sister’s husband', 'uncle');
+ case 'parspochi':
+ return I18N::translateContext('parent’s spouse’s child', 'step-sibling');
+ case 'parspodau':
+ return I18N::translateContext('parent’s spouse’s daughter', 'step-sister');
+ case 'parsposon':
+ return I18N::translateContext('parent’s spouse’s son', 'step-brother');
+ case 'sibchichi':
+ return I18N::translateContext('sibling’s child’s child', 'great-nephew/niece');
+ case 'sibchidau':
+ return I18N::translateContext('sibling’s child’s daughter', 'great-niece');
+ case 'sibchison':
+ return I18N::translateContext('sibling’s child’s son', 'great-nephew');
+ case 'sibdauchi':
+ return I18N::translateContext('sibling’s daughter’s child', 'great-nephew/niece');
+ case 'sibdaudau':
+ return I18N::translateContext('sibling’s daughter’s daughter', 'great-niece');
+ case 'sibdauhus':
+ return I18N::translateContext('sibling’s daughter’s husband', 'nephew-in-law');
+ case 'sibdauson':
+ return I18N::translateContext('sibling’s daughter’s son', 'great-nephew');
+ case 'sibsonchi':
+ return I18N::translateContext('sibling’s son’s child', 'great-nephew/niece');
+ case 'sibsondau':
+ return I18N::translateContext('sibling’s son’s daughter', 'great-niece');
+ case 'sibsonson':
+ return I18N::translateContext('sibling’s son’s son', 'great-nephew');
+ case 'sibsonwif':
+ return I18N::translateContext('sibling’s son’s wife', 'niece-in-law');
+ case 'sischichi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s child’s child', 'great-nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) sister’s child’s child', 'great-nephew/niece');
+ }
+ case 'sischidau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s child’s daughter', 'great-niece');
+ } else {
+ return I18N::translateContext('(a woman’s) sister’s child’s daughter', 'great-niece');
+ }
+ case 'sischison':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s child’s son', 'great-nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) sister’s child’s son', 'great-nephew');
+ }
+ case 'sisdauchi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s daughter’s child', 'great-nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) sister’s daughter’s child', 'great-nephew/niece');
+ }
+ case 'sisdaudau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s daughter’s daughter', 'great-niece');
+ } else {
+ return I18N::translateContext('(a woman’s) sister’s daughter’s daughter', 'great-niece');
+ }
+ case 'sisdauhus':
+ return I18N::translateContext('sisters’s daughter’s husband', 'nephew-in-law');
+ case 'sisdauson':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s daughter’s son', 'great-nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) sister’s daughter’s son', 'great-nephew');
+ }
+ case 'sishusbro':
+ return I18N::translateContext('sister’s husband’s brother', 'brother-in-law');
+ case 'sishussib':
+ return I18N::translateContext('sister’s husband’s sibling', 'brother/sister-in-law');
+ case 'sishussis':
+ return I18N::translateContext('sister’s husband’s sister', 'sister-in-law');
+ case 'sissonchi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s son’s child', 'great-nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) sister’s son’s child', 'great-nephew/niece');
+ }
+ case 'sissondau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s son’s daughter', 'great-niece');
+ } else {
+ return I18N::translateContext('(a woman’s) sister’s son’s daughter', 'great-niece');
+ }
+ case 'sissonson':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s son’s son', 'great-nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) sister’s son’s son', 'great-nephew');
+ }
+ case 'sissonwif':
+ return I18N::translateContext('sisters’s son’s wife', 'niece-in-law');
+ case 'sonchichi':
+ return I18N::translateContext('son’s child’s child', 'great-grandchild');
+ case 'sonchidau':
+ return I18N::translateContext('son’s child’s daughter', 'great-granddaughter');
+ case 'sonchison':
+ return I18N::translateContext('son’s child’s son', 'great-grandson');
+ case 'sondauchi':
+ return I18N::translateContext('son’s daughter’s child', 'great-grandchild');
+ case 'sondaudau':
+ return I18N::translateContext('son’s daughter’s daughter', 'great-granddaughter');
+ case 'sondauhus':
+ return I18N::translateContext('son’s daughter’s husband', 'granddaughter’s husband');
+ case 'sondauson':
+ return I18N::translateContext('son’s daughter’s son', 'great-grandson');
+ case 'sonsonchi':
+ return I18N::translateContext('son’s son’s child', 'great-grandchild');
+ case 'sonsondau':
+ return I18N::translateContext('son’s son’s daughter', 'great-granddaughter');
+ case 'sonsonson':
+ return I18N::translateContext('son’s son’s son', 'great-grandson');
+ case 'sonsonwif':
+ return I18N::translateContext('son’s son’s wife', 'grandson’s wife');
+ case 'sonwiffat':
+ return I18N::translateContext('son’s wife’s father', 'daughter-in-law’s father');
+ case 'sonwifmot':
+ return I18N::translateContext('son’s wife’s mother', 'daughter-in-law’s mother');
+ case 'sonwifpar':
+ return I18N::translateContext('son’s wife’s parent', 'daughter-in-law’s parent');
+ case 'wifbrowif':
+ return I18N::translateContext('wife’s brother’s wife', 'sister-in-law');
+ case 'wifsishus':
+ return I18N::translateContext('wife’s sister’s husband', 'brother-in-law');
+
+ // Some “special case” level four relationships that have specific names in certain languages
+ case 'fatfatbrowif':
+ return I18N::translateContext('father’s father’s brother’s wife', 'great-aunt');
+ case 'fatfatsibspo':
+ return I18N::translateContext('father’s father’s sibling’s spouse', 'great-aunt/uncle');
+ case 'fatfatsishus':
+ return I18N::translateContext('father’s father’s sister’s husband', 'great-uncle');
+ case 'fatmotbrowif':
+ return I18N::translateContext('father’s mother’s brother’s wife', 'great-aunt');
+ case 'fatmotsibspo':
+ return I18N::translateContext('father’s mother’s sibling’s spouse', 'great-aunt/uncle');
+ case 'fatmotsishus':
+ return I18N::translateContext('father’s mother’s sister’s husband', 'great-uncle');
+ case 'fatparbrowif':
+ return I18N::translateContext('father’s parent’s brother’s wife', 'great-aunt');
+ case 'fatparsibspo':
+ return I18N::translateContext('father’s parent’s sibling’s spouse', 'great-aunt/uncle');
+ case 'fatparsishus':
+ return I18N::translateContext('father’s parent’s sister’s husband', 'great-uncle');
+ case 'motfatbrowif':
+ return I18N::translateContext('mother’s father’s brother’s wife', 'great-aunt');
+ case 'motfatsibspo':
+ return I18N::translateContext('mother’s father’s sibling’s spouse', 'great-aunt/uncle');
+ case 'motfatsishus':
+ return I18N::translateContext('mother’s father’s sister’s husband', 'great-uncle');
+ case 'motmotbrowif':
+ return I18N::translateContext('mother’s mother’s brother’s wife', 'great-aunt');
+ case 'motmotsibspo':
+ return I18N::translateContext('mother’s mother’s sibling’s spouse', 'great-aunt/uncle');
+ case 'motmotsishus':
+ return I18N::translateContext('mother’s mother’s sister’s husband', 'great-uncle');
+ case 'motparbrowif':
+ return I18N::translateContext('mother’s parent’s brother’s wife', 'great-aunt');
+ case 'motparsibspo':
+ return I18N::translateContext('mother’s parent’s sibling’s spouse', 'great-aunt/uncle');
+ case 'motparsishus':
+ return I18N::translateContext('mother’s parent’s sister’s husband', 'great-uncle');
+ case 'parfatbrowif':
+ return I18N::translateContext('parent’s father’s brother’s wife', 'great-aunt');
+ case 'parfatsibspo':
+ return I18N::translateContext('parent’s father’s sibling’s spouse', 'great-aunt/uncle');
+ case 'parfatsishus':
+ return I18N::translateContext('parent’s father’s sister’s husband', 'great-uncle');
+ case 'parmotbrowif':
+ return I18N::translateContext('parent’s mother’s brother’s wife', 'great-aunt');
+ case 'parmotsibspo':
+ return I18N::translateContext('parent’s mother’s sibling’s spouse', 'great-aunt/uncle');
+ case 'parmotsishus':
+ return I18N::translateContext('parent’s mother’s sister’s husband', 'great-uncle');
+ case 'parparbrowif':
+ return I18N::translateContext('parent’s parent’s brother’s wife', 'great-aunt');
+ case 'parparsibspo':
+ return I18N::translateContext('parent’s parent’s sibling’s spouse', 'great-aunt/uncle');
+ case 'parparsishus':
+ return I18N::translateContext('parent’s parent’s sister’s husband', 'great-uncle');
+ case 'fatfatbrodau':
+ return I18N::translateContext('father’s father’s brother’s daughter', 'first cousin once removed ascending');
+ case 'fatfatbroson':
+ return I18N::translateContext('father’s father’s brother’s son', 'first cousin once removed ascending');
+ case 'fatfatbrochi':
+ return I18N::translateContext('father’s father’s brother’s child', 'first cousin once removed ascending');
+ case 'fatfatsisdau':
+ return I18N::translateContext('father’s father’s sister’s daughter', 'first cousin once removed ascending');
+ case 'fatfatsisson':
+ return I18N::translateContext('father’s father’s sister’s son', 'first cousin once removed ascending');
+ case 'fatfatsischi':
+ return I18N::translateContext('father’s father’s sister’s child', 'first cousin once removed ascending');
+ case 'fatmotbrodau':
+ return I18N::translateContext('father’s mother’s brother’s daughter', 'first cousin once removed ascending');
+ case 'fatmotbroson':
+ return I18N::translateContext('father’s mother’s brother’s son', 'first cousin once removed ascending');
+ case 'fatmotbrochi':
+ return I18N::translateContext('father’s mother’s brother’s child', 'first cousin once removed ascending');
+ case 'fatmotsisdau':
+ return I18N::translateContext('father’s mother’s sister’s daughter', 'first cousin once removed ascending');
+ case 'fatmotsisson':
+ return I18N::translateContext('father’s mother’s sister’s son', 'first cousin once removed ascending');
+ case 'fatmotsischi':
+ return I18N::translateContext('father’s mother’s sister’s child', 'first cousin once removed ascending');
+ case 'motfatbrodau':
+ return I18N::translateContext('mother’s father’s brother’s daughter', 'first cousin once removed ascending');
+ case 'motfatbroson':
+ return I18N::translateContext('mother’s father’s brother’s son', 'first cousin once removed ascending');
+ case 'motfatbrochi':
+ return I18N::translateContext('mother’s father’s brother’s child', 'first cousin once removed ascending');
+ case 'motfatsisdau':
+ return I18N::translateContext('mother’s father’s sister’s daughter', 'first cousin once removed ascending');
+ case 'motfatsisson':
+ return I18N::translateContext('mother’s father’s sister’s son', 'first cousin once removed ascending');
+ case 'motfatsischi':
+ return I18N::translateContext('mother’s father’s sister’s child', 'first cousin once removed ascending');
+ case 'motmotbrodau':
+ return I18N::translateContext('mother’s mother’s brother’s daughter', 'first cousin once removed ascending');
+ case 'motmotbroson':
+ return I18N::translateContext('mother’s mother’s brother’s son', 'first cousin once removed ascending');
+ case 'motmotbrochi':
+ return I18N::translateContext('mother’s mother’s brother’s child', 'first cousin once removed ascending');
+ case 'motmotsisdau':
+ return I18N::translateContext('mother’s mother’s sister’s daughter', 'first cousin once removed ascending');
+ case 'motmotsisson':
+ return I18N::translateContext('mother’s mother’s sister’s son', 'first cousin once removed ascending');
+ case 'motmotsischi':
+ return I18N::translateContext('mother’s mother’s sister’s child', 'first cousin once removed ascending');
+ }
+
+ // Some “special case” level five relationships that have specific names in certain languages
+ if (preg_match('/^(mot|fat|par)fatbro(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandfather’s brother’s granddaughter', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)fatbro(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandfather’s brother’s grandson', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)fatbro(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandfather’s brother’s grandchild', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)fatsis(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandfather’s sister’s granddaughter', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)fatsis(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandfather’s sister’s grandson', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)fatsis(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandfather’s sister’s grandchild', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)fatsib(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandfather’s sibling’s granddaughter', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)fatsib(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandfather’s sibling’s grandson', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)fatsib(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandfather’s sibling’s grandchild', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)motbro(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandmother’s brother’s granddaughter', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)motbro(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandmother’s brother’s grandson', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)motbro(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandmother’s brother’s grandchild', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)motsis(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandmother’s sister’s granddaughter', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)motsis(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandmother’s sister’s grandson', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)motsis(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandmother’s sister’s grandchild', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)motsib(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandmother’s sibling’s granddaughter', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)motsib(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandmother’s sibling’s grandson', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)motsib(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandmother’s sibling’s grandchild', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)parbro(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandparent’s brother’s granddaughter', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)parbro(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandparent’s brother’s grandson', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)parbro(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandparent’s brother’s grandchild', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)parsis(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandparent’s sister’s granddaughter', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)parsis(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandparent’s sister’s grandson', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)parsis(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandparent’s sister’s grandchild', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)parsib(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandparent’s sibling’s granddaughter', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)parsib(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandparent’s sibling’s grandson', 'second cousin');
+ } elseif (preg_match('/^(mot|fat|par)parsib(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandparent’s sibling’s grandchild', 'second cousin');
+ }
+
+ // Look for generic/pattern relationships.
+ if (preg_match('/^((?:mot|fat|par)+)(bro|sis|sib)$/', $path, $match)) {
+ // siblings of direct ancestors
+ $up = strlen($match[1]) / 3;
+ $bef_last = substr($path, -6, 3);
+ switch ($up) {
+ case 3:
+ switch ($sex2) {
+ case 'M':
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great-grandfather’s brother', 'great-great-uncle');
+ } elseif ($bef_last === 'mot') {
+ return I18N::translateContext('great-grandmother’s brother', 'great-great-uncle');
+ } else {
+ return I18N::translateContext('great-grandparent’s brother', 'great-great-uncle');
+ }
+ case 'F':
+ return I18N::translate('great-great-aunt');
+ default:
+ return I18N::translate('great-great-aunt/uncle');
+ }
+ case 4:
+ switch ($sex2) {
+ case 'M':
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great-great-grandfather’s brother', 'great-great-great-uncle');
+ } elseif ($bef_last === 'mot') {
+ return I18N::translateContext('great-great-grandmother’s brother', 'great-great-great-uncle');
+ } else {
+ return I18N::translateContext('great-great-grandparent’s brother', 'great-great-great-uncle');
+ }
+ case 'F':
+ return I18N::translate('great-great-great-aunt');
+ default:
+ return I18N::translate('great-great-great-aunt/uncle');
+ }
+ case 5:
+ switch ($sex2) {
+ case 'M':
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great-great-great-grandfather’s brother', 'great ×4 uncle');
+ } elseif ($bef_last === 'mot') {
+ return I18N::translateContext('great-great-great-grandmother’s brother', 'great ×4 uncle');
+ } else {
+ return I18N::translateContext('great-great-great-grandparent’s brother', 'great ×4 uncle');
+ }
+ case 'F':
+ return I18N::translate('great ×4 aunt');
+ default:
+ return I18N::translate('great ×4 aunt/uncle');
+ }
+ case 6:
+ switch ($sex2) {
+ case 'M':
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great ×4 grandfather’s brother', 'great ×5 uncle');
+ } elseif ($bef_last === 'mot') {
+ return I18N::translateContext('great ×4 grandmother’s brother', 'great ×5 uncle');
+ } else {
+ return I18N::translateContext('great ×4 grandparent’s brother', 'great ×5 uncle');
+ }
+ case 'F':
+ return I18N::translate('great ×5 aunt');
+ default:
+ return I18N::translate('great ×5 aunt/uncle');
+ }
+ case 7:
+ switch ($sex2) {
+ case 'M':
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great ×5 grandfather’s brother', 'great ×6 uncle');
+ } elseif ($bef_last === 'mot') {
+ return I18N::translateContext('great ×5 grandmother’s brother', 'great ×6 uncle');
+ } else {
+ return I18N::translateContext('great ×5 grandparent’s brother', 'great ×6 uncle');
+ }
+ case 'F':
+ return I18N::translate('great ×6 aunt');
+ default:
+ return I18N::translate('great ×6 aunt/uncle');
+ }
+ case 8:
+ switch ($sex2) {
+ case 'M':
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great ×6 grandfather’s brother', 'great ×7 uncle');
+ } elseif ($bef_last === 'mot') {
+ return I18N::translateContext('great ×6 grandmother’s brother', 'great ×7 uncle');
+ } else {
+ return I18N::translateContext('great ×6 grandparent’s brother', 'great ×7 uncle');
+ }
+ case 'F':
+ return I18N::translate('great ×7 aunt');
+ default:
+ return I18N::translate('great ×7 aunt/uncle');
+ }
+ default:
+ // Different languages have different rules for naming generations.
+ // An English great ×12 uncle is a Danish great ×10 uncle.
+ //
+ // Need to find out which languages use which rules.
+ switch (WT_LOCALE) {
+ case 'da':
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×%s uncle', I18N::number($up - 4));
+ case 'F':
+ return I18N::translate('great ×%s aunt', I18N::number($up - 4));
+ default:
+ return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 4));
+ }
+ case 'pl':
+ switch ($sex2) {
+ case 'M':
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great ×(%s-1) grandfather’s brother', 'great ×%s uncle', I18N::number($up - 2));
+ } elseif ($bef_last === 'mot') {
+ return I18N::translateContext('great ×(%s-1) grandmother’s brother', 'great ×%s uncle', I18N::number($up - 2));
+ } else {
+ return I18N::translateContext('great ×(%s-1) grandparent’s brother', 'great ×%s uncle', I18N::number($up - 2));
+ }
+ case 'F':
+ return I18N::translate('great ×%s aunt', I18N::number($up - 2));
+ default:
+ return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 2));
+ }
+ case 'it': // Source: Michele Locati
+ case 'en_AU':
+ case 'en_GB':
+ case 'en_US':
+ default:
+ switch ($sex2) {
+ case 'M': // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s uncle', I18N::number($up - 1));
+ case 'F':
+ return I18N::translate('great ×%s aunt', I18N::number($up - 1));
+ default:
+ return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 1));
+ }
+ }
+ }
+ }
+ if (preg_match('/^(?:bro|sis|sib)((?:son|dau|chi)+)$/', $path, $match)) {
+ // direct descendants of siblings
+ $down = strlen($match[1]) / 3 + 1; // Add one, as we count generations from the common ancestor
+ $first = substr($path, 0, 3);
+ switch ($down) {
+ case 4:
+ switch ($sex2) {
+ case 'M':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-grandson', 'great-great-nephew');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-grandson', 'great-great-nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) great-great-nephew', 'great-great-nephew');
+ }
+ case 'F':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-granddaughter', 'great-great-niece');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-granddaughter', 'great-great-niece');
+ } else {
+ return I18N::translateContext('(a woman’s) great-great-niece', 'great-great-niece');
+ }
+ default:
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-grandchild', 'great-great-nephew/niece');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-grandchild', 'great-great-nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) great-great-nephew/niece', 'great-great-nephew/niece');
+ }
+ }
+ case 5:
+ switch ($sex2) {
+ case 'M':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-grandson', 'great-great-great-nephew');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-grandson', 'great-great-great-nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) great-great-great-nephew', 'great-great-great-nephew');
+ }
+ case 'F':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-granddaughter', 'great-great-great-niece');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-granddaughter', 'great-great-great-niece');
+ } else {
+ return I18N::translateContext('(a woman’s) great-great-great-niece', 'great-great-great-niece');
+ }
+ default:
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-grandchild', 'great-great-great-nephew/niece');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-grandchild', 'great-great-great-nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) great-great-great-nephew/niece', 'great-great-great-nephew/niece');
+ }
+ }
+ case 6:
+ switch ($sex2) {
+ case 'M':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-great-grandson', 'great ×4 nephew');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-great-grandson', 'great ×4 nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) great ×4 nephew', 'great ×4 nephew');
+ }
+ case 'F':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-great-granddaughter', 'great ×4 niece');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-great-granddaughter', 'great ×4 niece');
+ } else {
+ return I18N::translateContext('(a woman’s) great ×4 niece', 'great ×4 niece');
+ }
+ default:
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-great-grandchild', 'great ×4 nephew/niece');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-great-grandchild', 'great ×4 nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) great ×4 nephew/niece', 'great ×4 nephew/niece');
+ }
+ }
+ case 7:
+ switch ($sex2) {
+ case 'M':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×4 grandson', 'great ×5 nephew');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×4 grandson', 'great ×5 nephew');
+ } else {
+ return I18N::translateContext('(a woman’s) great ×5 nephew', 'great ×5 nephew');
+ }
+ case 'F':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×4 granddaughter', 'great ×5 niece');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×4 granddaughter', 'great ×5 niece');
+ } else {
+ return I18N::translateContext('(a woman’s) great ×5 niece', 'great ×5 niece');
+ }
+ default:
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×4 grandchild', 'great ×5 nephew/niece');
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×4 grandchild', 'great ×5 nephew/niece');
+ } else {
+ return I18N::translateContext('(a woman’s) great ×5 nephew/niece', 'great ×5 nephew/niece');
+ }
+ }
+ default:
+ // Different languages have different rules for naming generations.
+ // An English great ×12 nephew is a Polish great ×11 nephew.
+ //
+ // Need to find out which languages use which rules.
+ switch (WT_LOCALE) {
+ case 'pl': // Source: Lukasz Wilenski
+ switch ($sex2) {
+ case 'M':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 3));
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 3));
+ } else {
+ return I18N::translateContext('(a woman’s) great ×%s nephew', 'great ×%s nephew', I18N::number($down - 3));
+ }
+ case 'F':
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 3));
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 3));
+ } else {
+ return I18N::translateContext('(a woman’s) great ×%s niece', 'great ×%s niece', I18N::number($down - 3));
+ }
+ default:
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 3));
+ } elseif ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 3));
+ } else {
+ return I18N::translateContext('(a woman’s) great ×%s nephew/niece', 'great ×%s nephew/niece', I18N::number($down - 3));
+ }
+ }
+ case 'he': // Source: Meliza Amity
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×%s nephew', I18N::number($down - 1));
+ case 'F':
+ return I18N::translate('great ×%s niece', I18N::number($down - 1));
+ default:
+ return I18N::translate('great ×%s nephew/niece', I18N::number($down - 1));
+ }
+ case 'it': // Source: Michele Locati.
+ case 'en_AU':
+ case 'en_GB':
+ case 'en_US':
+ default:
+ switch ($sex2) {
+ case 'M': // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s nephew', I18N::number($down - 2));
+ case 'F':
+ return I18N::translate('great ×%s niece', I18N::number($down - 2));
+ default:
+ return I18N::translate('great ×%s nephew/niece', I18N::number($down - 2));
+ }
+ }
+ }
+ }
+ if (preg_match('/^((?:mot|fat|par)*)$/', $path, $match)) {
+ // direct ancestors
+ $up = strlen($match[1]) / 3;
+ switch ($up) {
+ case 4:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great-great-grandfather');
+ case 'F':
+ return I18N::translate('great-great-grandmother');
+ default:
+ return I18N::translate('great-great-grandparent');
+ }
+ case 5:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great-great-great-grandfather');
+ case 'F':
+ return I18N::translate('great-great-great-grandmother');
+ default:
+ return I18N::translate('great-great-great-grandparent');
+ }
+ case 6:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×4 grandfather');
+ case 'F':
+ return I18N::translate('great ×4 grandmother');
+ default:
+ return I18N::translate('great ×4 grandparent');
+ }
+ case 7:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×5 grandfather');
+ case 'F':
+ return I18N::translate('great ×5 grandmother');
+ default:
+ return I18N::translate('great ×5 grandparent');
+ }
+ case 8:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×6 grandfather');
+ case 'F':
+ return I18N::translate('great ×6 grandmother');
+ default:
+ return I18N::translate('great ×6 grandparent');
+ }
+ case 9:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×7 grandfather');
+ case 'F':
+ return I18N::translate('great ×7 grandmother');
+ default:
+ return I18N::translate('great ×7 grandparent');
+ }
+ default:
+ // Different languages have different rules for naming generations.
+ // An English great ×12 grandfather is a Danish great ×11 grandfather.
+ //
+ // Need to find out which languages use which rules.
+ switch (WT_LOCALE) {
+ case 'da': // Source: Patrick Sorensen
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×%s grandfather', I18N::number($up - 3));
+ case 'F':
+ return I18N::translate('great ×%s grandmother', I18N::number($up - 3));
+ default:
+ return I18N::translate('great ×%s grandparent', I18N::number($up - 3));
+ }
+ case 'it': // Source: Michele Locati
+ case 'es': // Source: Wes Groleau
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×%s grandfather', I18N::number($up));
+ case 'F':
+ return I18N::translate('great ×%s grandmother', I18N::number($up));
+ default:
+ return I18N::translate('great ×%s grandparent', I18N::number($up));
+ }
+ case 'fr': // Source: Jacqueline Tetreault
+ case 'fr_CA':
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×%s grandfather', I18N::number($up - 1));
+ case 'F':
+ return I18N::translate('great ×%s grandmother', I18N::number($up - 1));
+ default:
+ return I18N::translate('great ×%s grandparent', I18N::number($up - 1));
+ }
+ case 'nn': // Source: Hogne Røed Nilsen (https://bugs.launchpad.net/webtrees/+bug/1168553)
+ case 'nb':
+ switch ($sex2) {
+ case 'M': // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandfather', I18N::number($up - 3));
+ case 'F':
+ return I18N::translate('great ×%s grandmother', I18N::number($up - 3));
+ default:
+ return I18N::translate('great ×%s grandparent', I18N::number($up - 3));
+ }
+ case 'en_AU':
+ case 'en_GB':
+ case 'en_US':
+ default:
+ switch ($sex2) {
+ case 'M': // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandfather', I18N::number($up - 2));
+ case 'F':
+ return I18N::translate('great ×%s grandmother', I18N::number($up - 2));
+ default:
+ return I18N::translate('great ×%s grandparent', I18N::number($up - 2));
+ }
+ }
+ }
+ }
+ if (preg_match('/^((?:son|dau|chi)*)$/', $path, $match)) {
+ // direct descendants
+ $up = strlen($match[1]) / 3;
+ switch ($up) {
+ case 4:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great-great-grandson');
+ case 'F':
+ return I18N::translate('great-great-granddaughter');
+ default:
+ return I18N::translate('great-great-grandchild');
+ }
+
+ case 5:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great-great-great-grandson');
+ case 'F':
+ return I18N::translate('great-great-great-granddaughter');
+ default:
+ return I18N::translate('great-great-great-grandchild');
+ }
+
+ case 6:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×4 grandson');
+ case 'F':
+ return I18N::translate('great ×4 granddaughter');
+ default:
+ return I18N::translate('great ×4 grandchild');
+ }
+
+ case 7:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×5 grandson');
+ case 'F':
+ return I18N::translate('great ×5 granddaughter');
+ default:
+ return I18N::translate('great ×5 grandchild');
+ }
+
+ case 8:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×6 grandson');
+ case 'F':
+ return I18N::translate('great ×6 granddaughter');
+ default:
+ return I18N::translate('great ×6 grandchild');
+ }
+
+ case 9:
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×7 grandson');
+ case 'F':
+ return I18N::translate('great ×7 granddaughter');
+ default:
+ return I18N::translate('great ×7 grandchild');
+ }
+
+ default:
+ // Different languages have different rules for naming generations.
+ // An English great ×12 grandson is a Danish great ×11 grandson.
+ //
+ // Need to find out which languages use which rules.
+ switch (WT_LOCALE) {
+ case 'nn': // Source: Hogne Røed Nilsen
+ case 'nb':
+ case 'da': // Source: Patrick Sorensen
+ switch ($sex2) {
+ case 'M':
+ return I18N::translate('great ×%s grandson', I18N::number($up - 3));
+ case 'F':
+ return I18N::translate('great ×%s granddaughter', I18N::number($up - 3));
+ default:
+ return I18N::translate('great ×%s grandchild', I18N::number($up - 3));
+ }
+ case 'it': // Source: Michele Locati
+ case 'es': // Source: Wes Groleau (adding doesn’t change behavior, but needs to be better researched)
+ case 'en_AU':
+ case 'en_GB':
+ case 'en_US':
+ default:
+ switch ($sex2) {
+
+ case 'M': // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandson', I18N::number($up - 2));
+ case 'F':
+ return I18N::translate('great ×%s granddaughter', I18N::number($up - 2));
+ default:
+ return I18N::translate('great ×%s grandchild', I18N::number($up - 2));
+ }
+ }
+ }
+ }
+ if (preg_match('/^((?:mot|fat|par)+)(?:bro|sis|sib)((?:son|dau|chi)+)$/', $path, $match)) {
+ // cousins in English
+ $ascent = $match[1];
+ $descent = $match[2];
+ $up = strlen($ascent) / 3;
+ $down = strlen($descent) / 3;
+ $cousin = min($up, $down); // Moved out of switch (en/default case) so that
+ $removed = abs($down - $up); // Spanish (and other languages) can use it, too.
+
+ // Different languages have different rules for naming cousins. For example,
+ // an English “second cousin once removed” is a Polish “cousin of 7th degree”.
+ //
+ // Need to find out which languages use which rules.
+ switch (WT_LOCALE) {
+ case 'pl': // Source: Lukasz Wilenski
+ return self::cousinName($up + $down + 2, $sex2);
+ case 'it':
+ // Source: Michele Locati. See italian_cousins_names.zip
+ // http://webtrees.net/forums/8-translation/1200-great-xn-grandparent?limit=6&start=6
+ return self::cousinName($up + $down - 3, $sex2);
+ case 'es':
+ // Source: Wes Groleau. See http://UniGen.us/Parentesco.html & http://UniGen.us/Parentesco-D.html
+ if ($down == $up) {
+ return self::cousinName($cousin, $sex2);
+ } elseif ($down < $up) {
+ return self::cousinName2($cousin + 1, $sex2, get_relationship_name_from_path('sib' . $descent, null, null));
+ } else {
+ switch ($sex2) {
+ case 'M':
+ return self::cousinName2($cousin + 1, $sex2, get_relationship_name_from_path('bro' . $descent, null, null));
+ case 'F':
+ return self::cousinName2($cousin + 1, $sex2, get_relationship_name_from_path('sis' . $descent, null, null));
+ default:
+ return self::cousinName2($cousin + 1, $sex2, get_relationship_name_from_path('sib' . $descent, null, null));
+ }
+ }
+ case 'en_AU': // See: http://en.wikipedia.org/wiki/File:CousinTree.svg
+ case 'en_GB':
+ case 'en_US':
+ default:
+ switch ($removed) {
+ case 0:
+ return self::cousinName($cousin, $sex2);
+ case 1:
+ if ($up > $down) {
+ /* I18N: %s=“fifth cousin”, etc. http://www.ancestry.com/learn/library/article.aspx?article=2856 */
+ return I18N::translate('%s once removed ascending', self::cousinName($cousin, $sex2));
+ } else {
+ /* I18N: %s=“fifth cousin”, etc. http://www.ancestry.com/learn/library/article.aspx?article=2856 */
+ return I18N::translate('%s once removed descending', self::cousinName($cousin, $sex2));
+ }
+ case 2:
+ if ($up > $down) {
+ /* I18N: %s=“fifth cousin”, etc. */
+ return I18N::translate('%s twice removed ascending', self::cousinName($cousin, $sex2));
+ } else {
+ /* I18N: %s=“fifth cousin”, etc. */
+ return I18N::translate('%s twice removed descending', self::cousinName($cousin, $sex2));
+ }
+ case 3:
+ if ($up > $down) {
+ /* I18N: %s=“fifth cousin”, etc. */
+ return I18N::translate('%s three times removed ascending', self::cousinName($cousin, $sex2));
+ } else {
+ /* I18N: %s=“fifth cousin”, etc. */
+ return I18N::translate('%s three times removed descending', self::cousinName($cousin, $sex2));
+ }
+ default:
+ if ($up > $down) {
+ /* I18N: %1$s=“fifth cousin”, etc., %2$s>=4 */
+ return I18N::translate('%1$s %2$s times removed ascending', self::cousinName($cousin, $sex2), I18N::number($removed));
+ } else {
+ /* I18N: %1$s=“fifth cousin”, etc., %2$s>=4 */
+ return I18N::translate('%1$s %2$s times removed descending', self::cousinName($cousin, $sex2), I18N::number($removed));
+ }
+ }
+ }
+ }
+
+ // Split the relationship into sub-relationships, e.g., third-cousin’s great-uncle.
+ // Try splitting at every point, and choose the path with the shorted translated name.
+
+ $relationship = null;
+ $path1 = substr($path, 0, 3);
+ $path2 = substr($path, 3);
+ while ($path2) {
+ $tmp = I18N::translate(
+ // I18N: A complex relationship, such as “third-cousin’s great-uncle”
+ '%1$s’s %2$s',
+ self::getRelationshipNameFromPath($path1, null, null), // TODO: need the actual people
+ self::getRelationshipNameFromPath($path2, null, null)
+ );
+ if (!$relationship || strlen($tmp) < strlen($relationship)) {
+ $relationship = $tmp;
+ }
+ $path1 .= substr($path2, 0, 3);
+ $path2 = substr($path2, 3);
+ }
+
+ return $relationship;
+ }
+
+ /**
+ * Function to build an URL querystring from GET variables
+ * Optionally, add/replace specified values
+ *
+ * @param null|string[] $overwrite
+ * @param null|string $separator
+ *
+ * @return string
+ */
+ public static function getQueryUrl($overwrite = null, $separator = '&') {
+ if (empty($_GET)) {
+ $get = array();
+ } else {
+ $get = $_GET;
+ }
+ if (is_array($overwrite)) {
+ foreach ($overwrite as $key => $value) {
+ $get[$key] = $value;
+ }
+ }
+
+ $query_string = '';
+ foreach ($get as $key => $value) {
+ if (!is_array($value)) {
+ $query_string .= $separator . rawurlencode($key) . '=' . rawurlencode($value);
+ } else {
+ foreach ($value as $k => $v) {
+ $query_string .= $separator . rawurlencode($key) . '%5B' . rawurlencode($k) . '%5D=' . rawurlencode($v);
+ }
+ }
+ }
+ $query_string = substr($query_string, strlen($separator)); // Remove leading “&amp;”
+ if ($query_string) {
+ return WT_SCRIPT_NAME . '?' . $query_string;
+ } else {
+ return WT_SCRIPT_NAME;
+ }
+ }
+
+ /**
+ * Determines whether the passed in filename is a link to an external source (i.e. contains “://”)
+ *
+ * @param string $file
+ *
+ * @return bool
+ */
+ public static function isFileExternal($file) {
+ return strpos($file, '://') !== false;
+ }
+}
diff --git a/app/GedcomRecord.php b/app/GedcomRecord.php
index 74a92adf65..51241f0cd8 100644
--- a/app/GedcomRecord.php
+++ b/app/GedcomRecord.php
@@ -15,6 +15,10 @@ namespace Fisharebest\Webtrees;
* 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\Functions\Functions;
+use Fisharebest\Webtrees\Functions\FunctionsDate;
+use Fisharebest\Webtrees\Functions\FunctionsImport;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
/**
* Class GedcomRecord - Base class for all gedcom records
@@ -809,9 +813,9 @@ class GedcomRecord {
if ($event->getDate()->isOK() || !$event->getPlace()->isEmpty()) {
switch ($style) {
case 1:
- return '<br><em>' . $event->getLabel() . ' ' . format_fact_date($event, $this, false, false) . ' ' . format_fact_place($event) . '</em>';
+ return '<br><em>' . $event->getLabel() . ' ' . FunctionsPrint::formatFactDate($event, $this, false, false) . ' ' . FunctionsPrint::formatFactPlace($event) . '</em>';
case 2:
- return '<dl><dt class="label">' . $event->getLabel() . '</dt><dd class="field">' . format_fact_date($event, $this, false, false) . ' ' . format_fact_place($event) . '</dd></dl>';
+ return '<dl><dt class="label">' . $event->getLabel() . '</dt><dd class="field">' . FunctionsPrint::formatFactDate($event, $this, false, false) . ' ' . FunctionsPrint::formatFactPlace($event) . '</dd></dl>';
}
}
}
@@ -1096,7 +1100,7 @@ class GedcomRecord {
}
}
if ($sort) {
- sort_facts($facts);
+ Functions::sortFacts($facts);
}
return $facts;
@@ -1126,7 +1130,7 @@ class GedcomRecord {
if ($sorting) {
return $t;
} else {
- return strip_tags(format_timestamp($t));
+ return strip_tags(FunctionsDate::formatTimestamp($t));
}
} else {
// The record does not have a CHAN event
@@ -1245,7 +1249,7 @@ class GedcomRecord {
$this->pending = $new_gedcom;
if (Auth::user()->getPreference('auto_accept')) {
- accept_all_changes($this->xref, $this->tree->getTreeId());
+ FunctionsImport::acceptAllChanges($this->xref, $this->tree->getTreeId());
$this->gedcom = $new_gedcom;
$this->pending = null;
}
@@ -1286,7 +1290,7 @@ class GedcomRecord {
// Accept this pending change
if (Auth::user()->getPreference('auto_accept')) {
- accept_all_changes($this->xref, $this->tree->getTreeId());
+ FunctionsImport::acceptAllChanges($this->xref, $this->tree->getTreeId());
$this->gedcom = $gedcom;
$this->pending = null;
}
@@ -1312,7 +1316,7 @@ class GedcomRecord {
// Accept this pending change
if (Auth::user()->getPreference('auto_accept')) {
- accept_all_changes($this->xref, $this->tree->getTreeId());
+ FunctionsImport::acceptAllChanges($this->xref, $this->tree->getTreeId());
}
// Clear the cache
diff --git a/app/I18N.php b/app/I18N.php
index 16a81bc528..6b94a84904 100644
--- a/app/I18N.php
+++ b/app/I18N.php
@@ -276,7 +276,7 @@ class I18N {
*
* NB: The import function will have normalised this, so we don't need
* to worry about badly formatted strings
- * NOTE: this function is not yet complete - eventually it will replace get_age_at_event()
+ * NOTE: this function is not yet complete - eventually it will replace FunctionsDate::get_age_at_event()
*
* @param $string
*
diff --git a/app/Media.php b/app/Media.php
index bef4091329..33a26485f6 100644
--- a/app/Media.php
+++ b/app/Media.php
@@ -15,6 +15,8 @@ namespace Fisharebest\Webtrees;
* 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\Functions\FunctionsMedia;
+use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
/**
* Class Media - Class that defines a media object
@@ -163,7 +165,7 @@ class Media extends GedcomRecord {
Log::addMediaLog('Thumbnail could not be created for ' . $main_file . ' (copy of main image)');
}
} else {
- if (hasMemoryForImage($main_file)) {
+ if (FunctionsMedia::hasMemoryForImage($main_file)) {
try {
switch ($imgsize['mime']) {
case 'image/png':
@@ -576,7 +578,7 @@ class Media extends GedcomRecord {
/** {@inheritdoc} */
public function formatListDetails() {
ob_start();
- print_media_links('1 OBJE @' . $this->getXref() . '@', 1);
+ FunctionsPrintFacts::printMediaLinks('1 OBJE @' . $this->getXref() . '@', 1);
return ob_get_clean();
}
diff --git a/app/Module/AlbumModule.php b/app/Module/AlbumModule.php
index adc6e171dd..7090e37b3e 100644
--- a/app/Module/AlbumModule.php
+++ b/app/Module/AlbumModule.php
@@ -17,6 +17,7 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Media;
use Fisharebest\Webtrees\Menu;
@@ -97,7 +98,7 @@ class AlbumModule extends AbstractModule implements ModuleTabInterface {
$needle = '1 NOTE';
$before = substr($haystack, 0, strpos($haystack, $needle));
$after = substr(strstr($haystack, $needle), strlen($needle));
- $notes = print_fact_notes($before . $needle . $after, 1, true);
+ $notes = FunctionsPrint::printFactNotes($before . $needle . $after, 1, true);
// Prepare Below Thumbnail menu ----------------------------------------------------
$menu = new Menu('<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap">' . $media->getFullName() . '</div>');
diff --git a/app/Module/BatchUpdateModule.php b/app/Module/BatchUpdateModule.php
index 3d0a5751f3..81dc4913d9 100644
--- a/app/Module/BatchUpdateModule.php
+++ b/app/Module/BatchUpdateModule.php
@@ -20,6 +20,7 @@ use Fisharebest\Webtrees\Controller\PageController;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -181,7 +182,7 @@ class BatchUpdateModule extends AbstractModule implements ModuleConfigInterface
'<input type="hidden" name="data" value="">' . // will be set by javascript for next update
'<table id="batch_update"><tr>' .
'<th>' . I18N::translate('Family tree') . '</th>' .
- '<td>' . select_edit_control('ged', Tree::getNameList(), '', $WT_TREE->getName(), 'onchange="reset_reload();"') .
+ '<td>' . FunctionsEdit::selectEditControl('ged', Tree::getNameList(), '', $WT_TREE->getName(), 'onchange="reset_reload();"') .
'</td></tr><tr><th>' . I18N::translate('Batch update') . '</th><td><select name="plugin" onchange="reset_reload();">';
if (!$this->plugin) {
$html .= '<option value="" selected></option>';
diff --git a/app/Module/CensusAssistantModule.php b/app/Module/CensusAssistantModule.php
index 24160ef5c3..33c70f62a5 100644
--- a/app/Module/CensusAssistantModule.php
+++ b/app/Module/CensusAssistantModule.php
@@ -18,6 +18,7 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Controller\SimpleController;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDb;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
@@ -112,7 +113,7 @@ class CensusAssistantModule extends AbstractModule {
// Output Individual for GEDFact Assistant ======================
echo "<table class=\"tabs_table width90\">";
- $myindilist = search_indis_names($filter_array, array($WT_TREE));
+ $myindilist = FunctionsDb::searchIndividualNames($filter_array, array($WT_TREE));
if ($myindilist) {
echo "<tr><td class=\"list_value_wrap\"><ul>";
usort($myindilist, '\Fisharebest\Webtrees\GedcomRecord::compare');
@@ -290,7 +291,7 @@ class CensusAssistantModule extends AbstractModule {
$filter = trim($filter);
$filter_array = explode(' ', preg_replace('/ {2,}/', ' ', $filter));
echo "<table class=\"tabs_table width90\"><tr>";
- $myindilist = search_indis_names($filter_array, array($WT_TREE));
+ $myindilist = FunctionsDb::searchIndividualNames($filter_array, array($WT_TREE));
if ($myindilist) {
echo "<td class=\"list_value_wrap\"><ul>";
usort($myindilist, '\Fisharebest\Webtrees\GedcomRecord::compare');
diff --git a/app/Module/ChartsBlockModule.php b/app/Module/ChartsBlockModule.php
index ba7a1dd465..e2b5d1b2ad 100644
--- a/app/Module/ChartsBlockModule.php
+++ b/app/Module/ChartsBlockModule.php
@@ -18,6 +18,8 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Controller\HourglassController;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Module\InteractiveTree\TreeView;
@@ -80,7 +82,7 @@ class ChartsBlockModule extends AbstractModule implements ModuleBlockInterface {
$controller->addInlineJavascript($chartController->setupJavascript());
$content .= '<td valign="middle">';
ob_start();
- print_pedigree_person($person, $details);
+ FunctionsPrint::printPedigreePerson($person, $details);
$content .= ob_get_clean();
$content .= '</td>';
$content .= '<td valign="middle">';
@@ -177,7 +179,7 @@ class ChartsBlockModule extends AbstractModule implements ModuleBlockInterface {
<tr>
<td class="descriptionbox wrap width33"><?php echo I18N::translate('Chart type'); ?></td>
<td class="optionbox">
- <?php echo select_edit_control('type',
+ <?php echo FunctionsEdit::selectEditControl('type',
array(
'pedigree' => I18N::translate('Pedigree'),
'descendants' => I18N::translate('Descendants'),
@@ -190,7 +192,7 @@ class ChartsBlockModule extends AbstractModule implements ModuleBlockInterface {
<tr>
<td class="descriptionbox wrap width33"><?php echo I18N::translate('Show details'); ?></td>
<td class="optionbox">
- <?php echo edit_field_yes_no('details', $details); ?>
+ <?php echo FunctionsEdit::editFieldYesNo('details', $details); ?>
</td>
</tr>
<tr>
@@ -198,7 +200,7 @@ class ChartsBlockModule extends AbstractModule implements ModuleBlockInterface {
<td class="optionbox">
<input data-autocomplete-type="INDI" type="text" name="pid" id="pid" value="<?php echo $pid; ?>" size="5">
<?php
- echo print_findindi_link('pid');
+ echo FunctionsPrint::printFindIndividualLink('pid');
$root = Individual::getInstance($pid, $WT_TREE);
if ($root) {
echo ' <span class="list_item">', $root->getFullName(), $root->formatFirstMajorFact(WT_EVENTS_BIRT, 1), '</span>';
diff --git a/app/Module/ClippingsCart/ClippingsCartController.php b/app/Module/ClippingsCart/ClippingsCartController.php
index 241ab25043..30dd81d7a0 100644
--- a/app/Module/ClippingsCart/ClippingsCartController.php
+++ b/app/Module/ClippingsCart/ClippingsCartController.php
@@ -20,6 +20,7 @@ use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsExport;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -164,7 +165,7 @@ class ClippingsCartController {
} elseif ($this->action === 'download') {
$media = array();
$mediacount = 0;
- $filetext = gedcom_header($WT_TREE);
+ $filetext = FunctionsExport::gedcomHeader($WT_TREE);
// Include SUBM/SUBN records, if they exist
$subn =
Database::prepare("SELECT o_gedcom FROM `##other` WHERE o_type=? AND o_file=?")
@@ -224,7 +225,7 @@ class ClippingsCartController {
$record = str_replace($match[0], '', $record);
}
}
- $record = convert_media_path($record, $this->conv_path);
+ $record = FunctionsExport::convertMediaPath($record, $this->conv_path);
$savedRecord = $record; // Save this for the "does this file exist" check
if ($convert === 'yes') {
$record = utf8_decode($record);
diff --git a/app/Module/ClippingsCartModule.php b/app/Module/ClippingsCartModule.php
index d1b7aaf16b..10bd9f01ff 100644
--- a/app/Module/ClippingsCartModule.php
+++ b/app/Module/ClippingsCartModule.php
@@ -19,6 +19,7 @@ use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Controller\PageController;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -304,9 +305,9 @@ class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface,
<input type="text" data-autocomplete-type="IFSRO" name="id" id="cart_item_id" size="5">
</td>
<td class="optionbox">
- <?php echo print_findindi_link('cart_item_id'); ?>
- <?php echo print_findfamily_link('cart_item_id'); ?>
- <?php echo print_findsource_link('cart_item_id', ''); ?>
+ <?php echo FunctionsPrint::printFindIndividualLink('cart_item_id'); ?>
+ <?php echo FunctionsPrint::printFindFamilyLink('cart_item_id'); ?>
+ <?php echo FunctionsPrint::printFindSourceLink('cart_item_id', ''); ?>
<input type="submit" value="<?php echo I18N::translate('Add'); ?>">
</td>
</tr>
@@ -399,9 +400,9 @@ class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface,
<input type="text" data-autocomplete-type="IFSRO" name="id" id="cart_item_id" size="8">
</td>
<td class="optionbox">
- <?php echo print_findindi_link('cart_item_id'); ?>
- <?php echo print_findfamily_link('cart_item_id'); ?>
- <?php echo print_findsource_link('cart_item_id'); ?>
+ <?php echo FunctionsPrint::printFindIndividualLink('cart_item_id'); ?>
+ <?php echo FunctionsPrint::printFindFamilyLink('cart_item_id'); ?>
+ <?php echo FunctionsPrint::printFindSourceLink('cart_item_id'); ?>
<input type="submit" value="<?php echo I18N::translate('Add'); ?>">
</td>
</tr>
diff --git a/app/Module/ExtraInformationModule.php b/app/Module/ExtraInformationModule.php
index d923c7ff1c..364597140f 100644
--- a/app/Module/ExtraInformationModule.php
+++ b/app/Module/ExtraInformationModule.php
@@ -16,6 +16,7 @@ namespace Fisharebest\Webtrees\Module;
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use Fisharebest\Webtrees\Fact;
+use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
use Fisharebest\Webtrees\I18N;
/**
@@ -60,7 +61,7 @@ class ExtraInformationModule extends AbstractModule implements ModuleSidebarInte
echo I18N::translate('There are no facts for this individual.');
} else {
foreach ($indifacts as $fact) {
- print_fact($fact, $controller->record);
+ FunctionsPrintFacts::printFact($fact, $controller->record);
}
}
diff --git a/app/Module/FamilyNavigatorModule.php b/app/Module/FamilyNavigatorModule.php
index b34e59cb07..dc7483a3bd 100644
--- a/app/Module/FamilyNavigatorModule.php
+++ b/app/Module/FamilyNavigatorModule.php
@@ -17,6 +17,7 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Menu;
@@ -117,7 +118,7 @@ class FamilyNavigatorModule extends AbstractModule implements ModuleSidebarInter
</tr>
<?php
foreach ($family->getSpouses() as $spouse) {
- $menu = new Menu(get_close_relationship_name($controller->record, $spouse));
+ $menu = new Menu(Functions::getCloseRelationshipName($controller->record, $spouse));
$menu->addClass('', 'submenu flyout');
$menu->addSubmenu(new Menu($this->getParents($spouse)));
?>
@@ -138,7 +139,7 @@ class FamilyNavigatorModule extends AbstractModule implements ModuleSidebarInter
}
foreach ($family->getChildren() as $child) {
- $menu = new Menu(get_close_relationship_name($controller->record, $child));
+ $menu = new Menu(Functions::getCloseRelationshipName($controller->record, $child));
$menu->addClass('', 'submenu flyout');
$menu->addSubmenu(new Menu($this->getFamily($child)));
?>
diff --git a/app/Module/FamilyTreeFavoritesModule.php b/app/Module/FamilyTreeFavoritesModule.php
index 61d83832e6..9cf2f685a7 100644
--- a/app/Module/FamilyTreeFavoritesModule.php
+++ b/app/Module/FamilyTreeFavoritesModule.php
@@ -20,6 +20,8 @@ use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Filter;
use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
@@ -188,12 +190,12 @@ class FamilyTreeFavoritesModule extends AbstractModule implements ModuleBlockInt
$content .= '<input type="radio" name="fav_category" value="record" checked onclick="jQuery(\'#gid' . $uniqueID . '\').removeAttr(\'disabled\'); jQuery(\'#url, #favtitle\').attr(\'disabled\',\'disabled\').val(\'\');">';
$content .= '<label for="gid' . $uniqueID . '">' . I18N::translate('Enter an individual, family, or source ID') . '</label>';
$content .= '<input class="pedigree_form" data-autocomplete-type="IFSRO" type="text" name="gid" id="gid' . $uniqueID . '" size="5" value="">';
- $content .= ' ' . print_findindi_link('gid' . $uniqueID);
- $content .= ' ' . print_findfamily_link('gid' . $uniqueID);
- $content .= ' ' . print_findsource_link('gid' . $uniqueID);
- $content .= ' ' . print_findrepository_link('gid' . $uniqueID);
- $content .= ' ' . print_findnote_link('gid' . $uniqueID);
- $content .= ' ' . print_findmedia_link('gid' . $uniqueID);
+ $content .= ' ' . FunctionsPrint::printFindIndividualLink('gid' . $uniqueID);
+ $content .= ' ' . FunctionsPrint::printFindFamilyLink('gid' . $uniqueID);
+ $content .= ' ' . FunctionsPrint::printFindSourceLink('gid' . $uniqueID);
+ $content .= ' ' . FunctionsPrint::printFindRepositoryLink('gid' . $uniqueID);
+ $content .= ' ' . FunctionsPrint::printFindNoteLink('gid' . $uniqueID);
+ $content .= ' ' . FunctionsPrint::printFindMediaLink('gid' . $uniqueID);
$content .= '</div>';
$content .= '<div class="add_fav_url">';
$content .= '<input type="radio" name="fav_category" value="url" onclick="jQuery(\'#url, #favtitle\').removeAttr(\'disabled\'); jQuery(\'#gid' . $uniqueID . '\').attr(\'disabled\',\'disabled\').val(\'\');">';
@@ -243,7 +245,7 @@ class FamilyTreeFavoritesModule extends AbstractModule implements ModuleBlockInt
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
diff --git a/app/Module/FamilyTreeNewsModule.php b/app/Module/FamilyTreeNewsModule.php
index fbb1077720..142b2f8028 100644
--- a/app/Module/FamilyTreeNewsModule.php
+++ b/app/Module/FamilyTreeNewsModule.php
@@ -18,6 +18,8 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDate;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Theme;
@@ -106,7 +108,7 @@ class FamilyTreeNewsModule extends AbstractModule implements ModuleBlockInterfac
}
$content .= '<div class="news_box" id="article' . $news->news_id . '">';
$content .= '<div class="news_title">' . Filter::escapeHtml($news->subject) . '</div>';
- $content .= '<div class="news_date">' . format_timestamp($news->updated) . '</div>';
+ $content .= '<div class="news_date">' . FunctionsDate::formatTimestamp($news->updated) . '</div>';
if ($news->body == strip_tags($news->body)) {
$news->body = nl2br($news->body, false);
}
@@ -127,7 +129,7 @@ class FamilyTreeNewsModule extends AbstractModule implements ModuleBlockInterfac
$content .= '&nbsp;&nbsp;|&nbsp;&nbsp;';
}
$content .= '<a href="index.php?gedcom_news_archive=yes&amp;ctype=' . $ctype . '">' . I18N::translate('View archive') . "</a>";
- $content .= help_link('gedcom_news_archive') . '<br>';
+ $content .= FunctionsPrint::helpLink('gedcom_news_archive') . '<br>';
}
if ($template) {
diff --git a/app/Module/FamilyTreeStatisticsModule.php b/app/Module/FamilyTreeStatisticsModule.php
index 3365a92d7c..1825afd6cb 100644
--- a/app/Module/FamilyTreeStatisticsModule.php
+++ b/app/Module/FamilyTreeStatisticsModule.php
@@ -17,6 +17,8 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDb;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Stats;
use Fisharebest\Webtrees\Theme;
@@ -187,7 +189,7 @@ class FamilyTreeStatisticsModule extends AbstractModule implements ModuleBlockIn
}
// NOTE: Print the most common surnames
if ($show_common_surnames) {
- $surnames = get_common_surnames($WT_TREE->getPreference('COMMON_NAMES_THRESHOLD'), $WT_TREE);
+ $surnames = FunctionsDb::getCommonSurnames($WT_TREE->getPreference('COMMON_NAMES_THRESHOLD'), $WT_TREE);
if (count($surnames) > 0) {
$content .= '<p><b>' . I18N::translate('Most common surnames') . '</b></p>';
$content .= '<div class="common_surnames">';
@@ -272,13 +274,13 @@ class FamilyTreeStatisticsModule extends AbstractModule implements ModuleBlockIn
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for yes/no option */ I18N::translate('Show date of last update?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('show_last_update', $show_last_update);
+ echo FunctionsEdit::editFieldYesNo('show_last_update', $show_last_update);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show common surnames?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('show_common_surnames', $show_common_surnames);
+ echo FunctionsEdit::editFieldYesNo('show_common_surnames', $show_common_surnames);
echo '</td></tr>';
?>
diff --git a/app/Module/FrequentlyAskedQuestionsModule.php b/app/Module/FrequentlyAskedQuestionsModule.php
index 08fcf34a87..f65c829fec 100644
--- a/app/Module/FrequentlyAskedQuestionsModule.php
+++ b/app/Module/FrequentlyAskedQuestionsModule.php
@@ -19,6 +19,7 @@ use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Controller\PageController;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Menu;
use Fisharebest\Webtrees\Module;
@@ -175,7 +176,7 @@ class FrequentlyAskedQuestionsModule extends AbstractModule implements ModuleMen
</label>
<div class="col-sm-9">
- <?php echo edit_language_checkboxes('lang', explode(',', $this->getBlockSetting($block_id, 'languages'))); ?>
+ <?php echo FunctionsEdit::editLanguageCheckboxes('lang', explode(',', $this->getBlockSetting($block_id, 'languages'))); ?>
</div>
</div>
@@ -195,7 +196,7 @@ class FrequentlyAskedQuestionsModule extends AbstractModule implements ModuleMen
</label>
<div class="col-sm-9">
- <?php echo select_edit_control('gedcom_id', Tree::getIdList(), I18N::translate('All'), $gedcom_id, 'class="form-control"'); ?>
+ <?php echo FunctionsEdit::selectEditControl('gedcom_id', Tree::getIdList(), I18N::translate('All'), $gedcom_id, 'class="form-control"'); ?>
<p class="small text-muted">
<?php echo I18N::translate('A FAQ item can be displayed on just one of the family trees, or on all the family trees.'); ?>
</p>
@@ -431,7 +432,7 @@ class FrequentlyAskedQuestionsModule extends AbstractModule implements ModuleMen
I18N::translate('Family tree'), ' ',
'<input type="hidden" name="mod", value="', $this->getName(), '">',
'<input type="hidden" name="mod_action" value="admin_config">',
- select_edit_control('ged', Tree::getNameList(), null, $WT_TREE->getNameHtml()),
+ FunctionsEdit::selectEditControl('ged', Tree::getNameList(), null, $WT_TREE->getNameHtml()),
'<input type="submit" value="', I18N::translate('show'), '">',
'</form></p>';
diff --git a/app/Module/GoogleMapsModule.php b/app/Module/GoogleMapsModule.php
index 88185cad5d..830468a396 100644
--- a/app/Module/GoogleMapsModule.php
+++ b/app/Module/GoogleMapsModule.php
@@ -24,6 +24,10 @@ use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\Functions\Functions;
+use Fisharebest\Webtrees\Functions\FunctionsCharts;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -299,7 +303,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
</tr>
<tr>
<th><?php echo /* I18N: http://en.wikipedia.org/wiki/Google_street_view */ I18N::translate('Google Street View™'); ?></th>
- <td><?php echo radio_buttons('GM_USE_STREETVIEW', array(false => I18N::translate('hide'), true => I18N::translate('show')), $this->getSetting('GM_USE_STREETVIEW'), 'class="radio-inline"'); ?></td>
+ <td><?php echo FunctionsEdit::radioButtons('GM_USE_STREETVIEW', array(false => I18N::translate('hide'), true => I18N::translate('show')), $this->getSetting('GM_USE_STREETVIEW'), 'class="radio-inline"'); ?></td>
</tr>
<tr>
<th><?php echo I18N::translate('Size of map (in pixels)'); ?></th>
@@ -440,7 +444,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
<table class="gm_edit_config">
<tr>
<th><?php echo I18N::translate('Use Google Maps™ for the place hierarchy'); ?></th>
- <td><?php echo edit_field_yes_no('GM_PLACE_HIERARCHY', $this->getSetting('GM_PLACE_HIERARCHY'), 'class="radio-inline"'); ?></td>
+ <td><?php echo FunctionsEdit::editFieldYesNo('GM_PLACE_HIERARCHY', $this->getSetting('GM_PLACE_HIERARCHY'), 'class="radio-inline"'); ?></td>
</tr>
<tr>
<th><?php echo I18N::translate('Size of map (in pixels)'); ?></th>
@@ -465,7 +469,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
<?php echo I18N::translate('Display short placenames'); ?>
</th>
<td>
- <?php echo edit_field_yes_no('GM_DISP_SHORT_PLACE', $this->getSetting('GM_DISP_SHORT_PLACE'), 'class="radio-inline"'); ?>
+ <?php echo FunctionsEdit::editFieldYesNo('GM_DISP_SHORT_PLACE', $this->getSetting('GM_DISP_SHORT_PLACE'), 'class="radio-inline"'); ?>
<p class="small text-muted">
<?php echo I18N::translate('Hide the flags that are configured in the googlemap module. Usually these are for countries and states. This serves as a visual cue that the markers around the flag are from the general area, and not the specific spot.'); ?>
</p>
@@ -476,7 +480,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
<?php echo I18N::translate('Display map coordinates'); ?>
</th>
<td>
- <?php echo edit_field_yes_no('GM_COORD', $this->getSetting('GM_COORD'), 'class="radio-inline"'); ?>
+ <?php echo FunctionsEdit::editFieldYesNo('GM_COORD', $this->getSetting('GM_COORD'), 'class="radio-inline"'); ?>
<p class="small text-muted">
<?php echo I18N::translate('This options sets whether latitude and longitude are displayed on the pop-up window attached to map markers.'); ?>
</p>
@@ -758,7 +762,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
</td>
<td class="optionbox">
<input class="pedigree_form" data-autocomplete-type="INDI" type="text" id="rootid" name="rootid" size="3" value="<?php echo $controller->root->getXref(); ?>">
- <?php echo print_findindi_link('rootid'); ?>
+ <?php echo FunctionsPrint::printFindIndividualLink('rootid'); ?>
</td>
<td class="topbottombar" rowspan="2">
<input type="submit" value="<?php echo I18N::translate('View'); ?>">
@@ -1313,7 +1317,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
$curgen++;
}
- $relationship = get_sosa_name($i + 1);
+ $relationship = FunctionsCharts::getSosaName($i + 1);
$event = '<img src="' . WT_STATIC_URL . WT_MODULES_DIR . 'googlemap/images/sq' . $curgen . '.png" width="10" height="10"> ' .
'<strong>' . $relationship . '</strong>';
@@ -1521,7 +1525,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
<input type="hidden" name="mod_action" value="admin_placecheck">
<div class="gm_check">
<label>', I18N::translate('Family tree'), '</label>';
- echo select_edit_control('gedcom_id', Tree::getIdList(), null, $gedcom_id, ' onchange="this.form.submit();"');
+ echo FunctionsEdit::selectEditControl('gedcom_id', Tree::getIdList(), null, $gedcom_id, ' onchange="this.form.submit();"');
echo '<label>', I18N::translate('Country'), '</label>
<select name="country" onchange="this.form.submit();">
<option value="XYZ" selected>', /* I18N: first/default option in a drop-down listbox */ I18N::translate('&lt;select&gt;'), '</option>
@@ -1918,7 +1922,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
$indifacts = array_merge($indifacts, $family->getFacts());
}
- sort_facts($indifacts);
+ Functions::sortFacts($indifacts);
// Create the markers list array
$gmarks = array();
@@ -3952,7 +3956,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
<td class="descriptionbox"><?php echo GedcomTag::getLabel('PLAC'); ?></td>
<td class="optionbox"><input type="text" id="new_pl_name" name="NEW_PLACE_NAME" value="<?php echo Filter::escapeHtml($place_name); ?>" size="25" class="address_input">
<div id="INDI_PLAC_pop" style="display: inline;">
- <?php echo print_specialchar_link('new_pl_name'); ?></div></td><td class="optionbox">
+ <?php echo FunctionsPrint::printSpecialCharacterLink('new_pl_name'); ?></div></td><td class="optionbox">
<label for="new_pl_name"><a href="#" onclick="showLocation_all(document.getElementById('new_pl_name').value); return false">&nbsp;<?php echo I18N::translate('Search globally'); ?></a></label>
&nbsp;&nbsp;|&nbsp;&nbsp;
<label for="new_pl_name"><a href="#" onclick="showLocation_level(document.getElementById('new_pl_name').value); return false">&nbsp;<?php echo I18N::translate('Search locally'); ?></a></label>
@@ -4138,7 +4142,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
->fetchOneColumn();
foreach ($gedcom_records as $gedrec) {
$i = 1;
- $placerec = get_sub_record(2, '2 PLAC', $gedrec, $i);
+ $placerec = Functions::getSubRecord(2, '2 PLAC', $gedrec, $i);
while (!empty($placerec)) {
if (preg_match("/2 PLAC (.+)/", $placerec, $match)) {
$placelist[$j] = array();
@@ -4168,7 +4172,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
$j = $j + 1;
}
$i = $i + 1;
- $placerec = get_sub_record(2, '2 PLAC', $gedrec, $i);
+ $placerec = Functions::getSubRecord(2, '2 PLAC', $gedrec, $i);
}
}
asort($placelist);
@@ -4489,7 +4493,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
function delete_place(placeid) {
var answer=confirm('<?php echo I18N::translate('Remove this location?'); ?>');
if (answer == true) {
- window.location = '<?php echo get_query_url(array('action' => 'DeleteRecord')); ?>&action=DeleteRecord&deleteRecord=' + placeid;
+ window.location = '<?php echo Functions::getQueryUrl(array('action' => 'DeleteRecord')); ?>&action=DeleteRecord&deleteRecord=' + placeid;
}
}
</script>
@@ -4583,7 +4587,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
</td>
<td>
<form action="?" onsubmit="add_place_location(this.parent_id.options[this.parent_id.selectedIndex].value); return false;">
- <?php echo select_edit_control('parent_id', $where_am_i, I18N::translate('Top level'), $parent); ?>
+ <?php echo FunctionsEdit::selectEditControl('parent_id', $where_am_i, I18N::translate('Top level'), $parent); ?>
<input type="submit" value="<?php echo I18N::translate('Add'); ?>">
</form>
</td>
@@ -4597,7 +4601,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
<input type="hidden" name="mod" value="googlemap">
<input type="hidden" name="mod_action" value="admin_places">
<input type="hidden" name="action" value="ImportGedcom">
- <?php echo select_edit_control('ged', Tree::getNameList(), null, $WT_TREE->getName()); ?>
+ <?php echo FunctionsEdit::selectEditControl('ged', Tree::getNameList(), null, $WT_TREE->getName()); ?>
<input type="submit" value="<?php echo I18N::translate('Import'); ?>">
</form>
</td>
@@ -4624,7 +4628,7 @@ class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface,
<input type="hidden" name="mod" value="googlemap">
<input type="hidden" name="mod_action" value="admin_places">
<input type="hidden" name="action" value="ExportFile">
- <?php echo select_edit_control('parent', $where_am_i, I18N::translate('All'), $WT_TREE->getTreeId()); ?>
+ <?php echo FunctionsEdit::selectEditControl('parent', $where_am_i, I18N::translate('All'), $WT_TREE->getTreeId()); ?>
<input type="submit" value="<?php echo I18N::translate('Download'); ?>">
</form>
</td>
diff --git a/app/Module/HtmlBlockModule.php b/app/Module/HtmlBlockModule.php
index 48b47bb0a0..d4002fc419 100644
--- a/app/Module/HtmlBlockModule.php
+++ b/app/Module/HtmlBlockModule.php
@@ -17,6 +17,8 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDate;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Module;
@@ -99,7 +101,7 @@ class HtmlBlockModule extends AbstractModule implements ModuleBlockInterface {
$content = $html;
if ($show_timestamp) {
- $content .= '<br>' . format_timestamp($this->getBlockSetting($block_id, 'timestamp', WT_TIMESTAMP) + WT_TIMESTAMP_OFFSET);
+ $content .= '<br>' . FunctionsDate::formatTimestamp($this->getBlockSetting($block_id, 'timestamp', WT_TIMESTAMP) + WT_TIMESTAMP_OFFSET);
}
if ($template) {
@@ -310,14 +312,14 @@ class HtmlBlockModule extends AbstractModule implements ModuleBlockInterface {
echo '<tr><td class="descriptionbox wrap">';
echo I18N::translate('Show the date and time of update');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('show_timestamp', $show_timestamp);
+ echo FunctionsEdit::editFieldYesNo('show_timestamp', $show_timestamp);
echo '<input type="hidden" name="timestamp" value="', WT_TIMESTAMP, '">';
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap">';
echo I18N::translate('Show this block for which languages?');
echo '</td><td class="optionbox">';
- echo edit_language_checkboxes('lang', $languages);
+ echo FunctionsEdit::editLanguageCheckboxes('lang', $languages);
echo '</td></tr>';
}
}
diff --git a/app/Module/IndividualFactsTabModule.php b/app/Module/IndividualFactsTabModule.php
index 3454f15042..4b83605908 100644
--- a/app/Module/IndividualFactsTabModule.php
+++ b/app/Module/IndividualFactsTabModule.php
@@ -19,6 +19,9 @@ use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Date;
use Fisharebest\Webtrees\Fact;
use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\Functions\Functions;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
+use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Module;
@@ -113,7 +116,7 @@ class IndividualFactsTabModule extends AbstractModule implements ModuleTabInterf
$indifacts[] = $fact;
}
- sort_facts($indifacts);
+ Functions::sortFacts($indifacts);
ob_start();
@@ -134,12 +137,12 @@ class IndividualFactsTabModule extends AbstractModule implements ModuleTabInterf
echo '</form></td></tr>';
foreach ($indifacts as $fact) {
- print_fact($fact, $controller->record);
+ FunctionsPrintFacts::printFact($fact, $controller->record);
}
//-- new fact link
if ($controller->record->canEdit()) {
- print_add_new_fact($controller->record->getXref(), $indifacts, 'INDI');
+ FunctionsPrint::printAddNewFact($controller->record->getXref(), $indifacts, 'INDI');
}
echo '</tbody>';
echo '</table>';
diff --git a/app/Module/LoggedInUsersModule.php b/app/Module/LoggedInUsersModule.php
index e4a4d2e074..34798457c6 100644
--- a/app/Module/LoggedInUsersModule.php
+++ b/app/Module/LoggedInUsersModule.php
@@ -17,6 +17,7 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Theme;
@@ -78,7 +79,7 @@ class LoggedInUsersModule extends AbstractModule implements ModuleBlockInterface
}
$content .= ' - ' . Filter::escapeHtml($user->getUserName());
if (Auth::id() != $user->getUserId() && $user->getPreference('contactmethod') != 'none') {
- $content .= ' <a class="icon-email" href="#" onclick="return message(\'' . Filter::escapeHtml($user->getUserName()) . '\', \'\', \'' . Filter::escapeHtml(get_query_url()) . '\');" title="' . I18N::translate('Send a message') . '"></a>';
+ $content .= ' <a class="icon-email" href="#" onclick="return message(\'' . Filter::escapeHtml($user->getUserName()) . '\', \'\', \'' . Filter::escapeHtml(Functions::getQueryUrl()) . '\');" title="' . I18N::translate('Send a message') . '"></a>';
}
$content .= '</div>';
}
diff --git a/app/Module/MediaTabModule.php b/app/Module/MediaTabModule.php
index c44aada02f..c1b6535a10 100644
--- a/app/Module/MediaTabModule.php
+++ b/app/Module/MediaTabModule.php
@@ -17,6 +17,9 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Fact;
+use Fisharebest\Webtrees\Functions\Functions;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
+use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
@@ -61,10 +64,10 @@ class MediaTabModule extends AbstractModule implements ModuleTabInterface {
echo '<table class="facts_table">';
foreach ($this->getFactsWithMedia() as $fact) {
if ($fact->getTag() == 'OBJE') {
- print_main_media($fact, 1);
+ FunctionsPrintFacts::printMainMedia($fact, 1);
} else {
for ($i = 2; $i < 4; ++$i) {
- print_main_media($fact, $i);
+ FunctionsPrintFacts::printMainMedia($fact, $i);
}
}
}
@@ -82,7 +85,7 @@ class MediaTabModule extends AbstractModule implements ModuleTabInterface {
<a href="#" onclick="window.open('addmedia.php?action=showmediaform&amp;linktoid=<?php echo $controller->record->getXref(); ?>&amp;ged=<?php echo $controller->record->getTree()->getNameUrl(); ?>', '_blank', edit_window_specs); return false;">
<?php echo I18N::translate('Add a new media object'); ?>
</a>
- <?php echo help_link('OBJE'); ?>
+ <?php echo FunctionsPrint::helpLink('OBJE'); ?>
<br>
<a href="#" onclick="window.open('inverselink.php?linktoid=<?php echo $controller->record->getXref(); ?>&amp;ged=<?php echo $WT_TREE->getNameUrl(); ?>&amp;linkto=person', '_blank', find_window_specs); return false;">
<?php echo I18N::translate('Link to an existing media object'); ?>
@@ -120,7 +123,7 @@ class MediaTabModule extends AbstractModule implements ModuleTabInterface {
$this->facts[] = $fact;
}
}
- sort_facts($this->facts);
+ Functions::sortFacts($this->facts);
}
return $this->facts;
diff --git a/app/Module/NotesTabModule.php b/app/Module/NotesTabModule.php
index 3baae34ed0..e2ed55fccf 100644
--- a/app/Module/NotesTabModule.php
+++ b/app/Module/NotesTabModule.php
@@ -17,6 +17,8 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Fact;
+use Fisharebest\Webtrees\Functions\Functions;
+use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
@@ -69,10 +71,10 @@ class NotesTabModule extends AbstractModule implements ModuleTabInterface {
<?php
foreach ($this->getFactsWithNotes() as $fact) {
if ($fact->getTag() == 'NOTE') {
- print_main_notes($fact, 1);
+ FunctionsPrintFacts::printMainNotes($fact, 1);
} else {
for ($i = 2; $i < 4; ++$i) {
- print_main_notes($fact, $i);
+ FunctionsPrintFacts::printMainNotes($fact, $i);
}
}
}
@@ -138,7 +140,7 @@ class NotesTabModule extends AbstractModule implements ModuleTabInterface {
$this->facts[] = $fact;
}
}
- sort_facts($this->facts);
+ Functions::sortFacts($this->facts);
}
return $this->facts;
diff --git a/app/Module/OnThisDayModule.php b/app/Module/OnThisDayModule.php
index 56080143e1..c78b1c1c3b 100644
--- a/app/Module/OnThisDayModule.php
+++ b/app/Module/OnThisDayModule.php
@@ -17,6 +17,8 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
+use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Theme;
@@ -67,12 +69,12 @@ class OnThisDayModule extends AbstractModule implements ModuleBlockInterface {
switch ($infoStyle) {
case 'list':
// Output style 1: Old format, no visible tables, much smaller text. Better suited to right side of page.
- $content .= print_events_list($todayjd, $todayjd, $onlyBDM ? 'BIRT MARR DEAT' : '', $filter, $sortStyle);
+ $content .= FunctionsPrintLists::eventsList($todayjd, $todayjd, $onlyBDM ? 'BIRT MARR DEAT' : '', $filter, $sortStyle);
break;
case 'table':
// Style 2: New format, tables, big text, etc. Not too good on right side of page
ob_start();
- $content .= print_events_table($todayjd, $todayjd, $onlyBDM ? 'BIRT MARR DEAT' : '', $filter, $sortStyle);
+ $content .= FunctionsPrintLists::eventsTable($todayjd, $todayjd, $onlyBDM ? 'BIRT MARR DEAT' : '', $filter, $sortStyle);
$content .= ob_get_clean();
break;
}
@@ -122,25 +124,25 @@ class OnThisDayModule extends AbstractModule implements ModuleBlockInterface {
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show only events of living individuals?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('filter', $filter);
+ echo FunctionsEdit::editFieldYesNo('filter', $filter);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show only births, deaths, and marriages?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('onlyBDM', $onlyBDM);
+ echo FunctionsEdit::editFieldYesNo('onlyBDM', $onlyBDM);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Presentation style');
echo '</td><td class="optionbox">';
- echo select_edit_control('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
+ echo FunctionsEdit::selectEditControl('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Sort order');
echo '</td><td class="optionbox">';
- echo select_edit_control('sortStyle', array(
+ echo FunctionsEdit::selectEditControl('sortStyle', array(
/* I18N: An option in a list-box */ 'alpha' => I18N::translate('sort by name'),
/* I18N: An option in a list-box */ 'anniv' => I18N::translate('sort by date'),
), null, $sortStyle, '');
@@ -149,7 +151,7 @@ class OnThisDayModule extends AbstractModule implements ModuleBlockInterface {
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
}
diff --git a/app/Module/RecentChangesModule.php b/app/Module/RecentChangesModule.php
index fa61f21736..190d358a17 100644
--- a/app/Module/RecentChangesModule.php
+++ b/app/Module/RecentChangesModule.php
@@ -17,6 +17,9 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDb;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
+use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Theme;
@@ -55,7 +58,7 @@ class RecentChangesModule extends AbstractModule implements ModuleBlockInterface
}
}
- $found_facts = get_recent_changes(WT_CLIENT_JD - $days);
+ $found_facts = FunctionsDb::getRecentChanges(WT_CLIENT_JD - $days);
if (!$found_facts && $hide_empty) {
return '';
@@ -78,11 +81,11 @@ class RecentChangesModule extends AbstractModule implements ModuleBlockInterface
ob_start();
switch ($infoStyle) {
case 'list':
- $content .= print_changes_list($found_facts, $sortStyle);
+ $content .= FunctionsPrintLists::changesList($found_facts, $sortStyle);
break;
case 'table':
// sortable table
- $content .= print_changes_table($found_facts, $sortStyle);
+ $content .= FunctionsPrintLists::changesTable($found_facts, $sortStyle);
break;
}
$content .= ob_get_clean();
@@ -140,13 +143,13 @@ class RecentChangesModule extends AbstractModule implements ModuleBlockInterface
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Presentation style');
echo '</td><td class="optionbox">';
- echo select_edit_control('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
+ echo FunctionsEdit::selectEditControl('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Sort order');
echo '</td><td class="optionbox">';
- echo select_edit_control('sortStyle', array(
+ echo FunctionsEdit::selectEditControl('sortStyle', array(
'name' => /* I18N: An option in a list-box */ I18N::translate('sort by name'),
'date_asc' => /* I18N: An option in a list-box */ I18N::translate('sort by date, oldest first'),
'date_desc' => /* I18N: An option in a list-box */ I18N::translate('sort by date, newest first'),
@@ -156,13 +159,13 @@ class RecentChangesModule extends AbstractModule implements ModuleBlockInterface
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Should this block be hidden when it is empty?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('hide_empty', $hide_empty);
+ echo FunctionsEdit::editFieldYesNo('hide_empty', $hide_empty);
echo '</td></tr>';
echo '<tr><td colspan="2" class="optionbox wrap">';
echo '<span class="error">', I18N::translate('If you hide an empty block, you will not be able to change its configuration until it becomes visible by no longer being empty.'), '</span>';
diff --git a/app/Module/RelativesTabModule.php b/app/Module/RelativesTabModule.php
index bbb17801a8..4be0781440 100644
--- a/app/Module/RelativesTabModule.php
+++ b/app/Module/RelativesTabModule.php
@@ -18,6 +18,7 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Date;
use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -120,7 +121,7 @@ class RelativesTabModule extends AbstractModule implements ModuleTabInterface {
?>
<tr>
<td class="<?php echo $class; ?>">
- <?php echo get_close_relationship_name($controller->record, $person); ?>
+ <?php echo Functions::getCloseRelationshipName($controller->record, $person); ?>
</td>
<td class="<?php echo $controller->getPersonStyle($person); ?>">
<?php echo Theme::theme()->individualBoxLarge($person); ?>
@@ -154,7 +155,7 @@ class RelativesTabModule extends AbstractModule implements ModuleTabInterface {
?>
<tr>
<td class="<?php echo $class; ?>">
- <?php echo get_close_relationship_name($controller->record, $person); ?>
+ <?php echo Functions::getCloseRelationshipName($controller->record, $person); ?>
</td>
<td class="<?php echo $controller->getPersonStyle($person); ?>">
<?php echo Theme::theme()->individualBoxLarge($person); ?>
@@ -239,7 +240,7 @@ class RelativesTabModule extends AbstractModule implements ModuleTabInterface {
<tr>
<td class="<?php echo $class; ?>">
<?php echo self::ageDifference($prev, $next, $child_number); ?>
- <?php echo get_close_relationship_name($controller->record, $person); ?>
+ <?php echo Functions::getCloseRelationshipName($controller->record, $person); ?>
</td>
<td class="<?php echo $controller->getPersonStyle($person); ?>">
<?php echo Theme::theme()->individualBoxLarge($person); ?>
diff --git a/app/Module/ResearchTaskModule.php b/app/Module/ResearchTaskModule.php
index 8fbd921984..351c1b5ba1 100644
--- a/app/Module/ResearchTaskModule.php
+++ b/app/Module/ResearchTaskModule.php
@@ -18,6 +18,8 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDb;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Theme;
@@ -104,7 +106,7 @@ class ResearchTaskModule extends AbstractModule implements ModuleBlockInterface
$found = false;
$end_jd = $show_future ? 99999999 : WT_CLIENT_JD;
- foreach (get_calendar_events(0, $end_jd, '_TODO', $WT_TREE) as $fact) {
+ foreach (FunctionsDb::getCalendarEvents(0, $end_jd, '_TODO', $WT_TREE) as $fact) {
$record = $fact->getParent();
$user_name = $fact->getAttribute('_WT_USER');
if ($user_name === Auth::user()->getUserName() || !$user_name && $show_unassigned || $user_name && $show_other) {
@@ -183,25 +185,25 @@ class ResearchTaskModule extends AbstractModule implements ModuleBlockInterface
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show research tasks that are assigned to other users');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('show_other', $show_other);
+ echo FunctionsEdit::editFieldYesNo('show_other', $show_other);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show research tasks that are not assigned to any user');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('show_unassigned', $show_unassigned);
+ echo FunctionsEdit::editFieldYesNo('show_unassigned', $show_unassigned);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show research tasks that have a date in the future');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('show_future', $show_future);
+ echo FunctionsEdit::editFieldYesNo('show_future', $show_future);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
}
diff --git a/app/Module/ReviewChangesModule.php b/app/Module/ReviewChangesModule.php
index 42373a2935..3200b5c37e 100644
--- a/app/Module/ReviewChangesModule.php
+++ b/app/Module/ReviewChangesModule.php
@@ -18,6 +18,8 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDate;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Mail;
@@ -103,8 +105,8 @@ class ReviewChangesModule extends AbstractModule implements ModuleBlockInterface
$content .= "<a href=\"#\" onclick=\"window.open('edit_changes.php','_blank', chan_window_specs); return false;\">" . I18N::translate('There are pending changes for you to moderate.') . "</a><br>";
}
if ($sendmail == "yes") {
- $content .= I18N::translate('Last email reminder was sent ') . format_timestamp(Site::getPreference('LAST_CHANGE_EMAIL')) . "<br>";
- $content .= I18N::translate('Next email reminder will be sent after ') . format_timestamp(Site::getPreference('LAST_CHANGE_EMAIL') + (60 * 60 * 24 * $days)) . "<br><br>";
+ $content .= I18N::translate('Last email reminder was sent ') . FunctionsDate::formatTimestamp(Site::getPreference('LAST_CHANGE_EMAIL')) . "<br>";
+ $content .= I18N::translate('Next email reminder will be sent after ') . FunctionsDate::formatTimestamp(Site::getPreference('LAST_CHANGE_EMAIL') + (60 * 60 * 24 * $days)) . "<br><br>";
}
$content .= '<ul>';
$changes = Database::prepare(
@@ -172,7 +174,7 @@ class ReviewChangesModule extends AbstractModule implements ModuleBlockInterface
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Send out reminder emails?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('sendmail', $sendmail);
+ echo FunctionsEdit::editFieldYesNo('sendmail', $sendmail);
echo '<br>';
echo I18N::translate('Reminder email frequency (days)') . "&nbsp;<input type='text' name='days' value='" . $days . "' size='2'>";
echo '</td></tr>';
@@ -180,7 +182,7 @@ class ReviewChangesModule extends AbstractModule implements ModuleBlockInterface
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
}
diff --git a/app/Module/SlideShowModule.php b/app/Module/SlideShowModule.php
index c4f2b20203..42ebec0c7a 100644
--- a/app/Module/SlideShowModule.php
+++ b/app/Module/SlideShowModule.php
@@ -18,6 +18,8 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Media;
@@ -173,7 +175,7 @@ class SlideShowModule extends AbstractModule implements ModuleBlockInterface {
$content .= '<a href="' . $source->getHtmlUrl() . '">' . I18N::translate('View source') . ' — ' . $source->getFullname() . '</a><br>';
}
$content .= '<br><div class="indent">';
- $content .= print_fact_notes($random_media->getGedcom(), "1", false);
+ $content .= FunctionsPrint::printFactNotes($random_media->getGedcom(), "1", false);
$content .= '</div>';
$content .= '</td></tr></table>';
$content .= '</div>'; // random_picture_content
@@ -247,7 +249,7 @@ class SlideShowModule extends AbstractModule implements ModuleBlockInterface {
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show only individuals, events, or all?');
echo '</td><td class="optionbox">';
- echo select_edit_control('filter', array('indi' => I18N::translate('Individuals'), 'event' => I18N::translate('Facts and events'), 'all' => I18N::translate('All')), null, $filter, '');
+ echo FunctionsEdit::selectEditControl('filter', array('indi' => I18N::translate('Individuals'), 'event' => I18N::translate('Facts and events'), 'all' => I18N::translate('All')), null, $filter, '');
echo '</td></tr>';
$filters = array(
@@ -393,13 +395,13 @@ class SlideShowModule extends AbstractModule implements ModuleBlockInterface {
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show slide show controls?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('controls', $controls);
+ echo FunctionsEdit::editFieldYesNo('controls', $controls);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Start slide show on page load?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('start', $start);
+ echo FunctionsEdit::editFieldYesNo('start', $start);
echo '</td></tr>';
}
}
diff --git a/app/Module/SourcesTabModule.php b/app/Module/SourcesTabModule.php
index 4840385366..b0a0c3add0 100644
--- a/app/Module/SourcesTabModule.php
+++ b/app/Module/SourcesTabModule.php
@@ -17,6 +17,8 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Fact;
+use Fisharebest\Webtrees\Functions\Functions;
+use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
@@ -69,9 +71,9 @@ class SourcesTabModule extends AbstractModule implements ModuleTabInterface {
<?php
foreach ($this->getFactsWithSources() as $fact) {
if ($fact->getTag() == 'SOUR') {
- print_main_sources($fact, 1);
+ FunctionsPrintFacts::printMainSources($fact, 1);
} else {
- print_main_sources($fact, 2);
+ FunctionsPrintFacts::printMainSources($fact, 2);
}
}
if (!$this->getFactsWithSources()) {
@@ -126,7 +128,7 @@ class SourcesTabModule extends AbstractModule implements ModuleTabInterface {
$this->facts[] = $fact;
}
}
- sort_facts($this->facts);
+ Functions::sortFacts($this->facts);
}
return $this->facts;
diff --git a/app/Module/StoriesModule.php b/app/Module/StoriesModule.php
index f97375fbba..61d7d8b40a 100644
--- a/app/Module/StoriesModule.php
+++ b/app/Module/StoriesModule.php
@@ -19,6 +19,8 @@ use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Controller\PageController;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Menu;
@@ -240,7 +242,7 @@ class StoriesModule extends AbstractModule implements ModuleTabInterface, Module
</label>
<div class="col-sm-9">
<input data-autocomplete-type="INDI" type="text" name="xref" id="xref" size="4" value="<?php echo $xref; ?>">
- <?php echo print_findindi_link('xref'); ?>
+ <?php echo FunctionsPrint::printFindIndividualLink('xref'); ?>
<?php if ($individual): ?>
<?php echo $individual->formatList('span'); ?>
<?php endif; ?>
@@ -252,7 +254,7 @@ class StoriesModule extends AbstractModule implements ModuleTabInterface, Module
<?php echo I18N::translate('Show this block for which languages?'); ?>
</label>
<div class="col-sm-9">
- <?php echo edit_language_checkboxes('lang', explode(',', $this->getBlockSetting($block_id, 'languages'))); ?>
+ <?php echo FunctionsEdit::editLanguageCheckboxes('lang', explode(',', $this->getBlockSetting($block_id, 'languages'))); ?>
</div>
</div>
@@ -350,7 +352,7 @@ class StoriesModule extends AbstractModule implements ModuleTabInterface, Module
</label>
<input type="hidden" name="mod" value="<?php echo $this->getName(); ?>">
<input type="hidden" name="mod_action" value="admin_config">
- <?php echo select_edit_control('ged', Tree::getNameList(), null, $WT_TREE->getName(), 'class="form-control"'); ?>
+ <?php echo FunctionsEdit::selectEditControl('ged', Tree::getNameList(), null, $WT_TREE->getName(), 'class="form-control"'); ?>
<input type="submit" class="btn btn-primary" value="<?php echo I18N::translate('show'); ?>">
</form>
diff --git a/app/Module/TopGivenNamesModule.php b/app/Module/TopGivenNamesModule.php
index 1be79fb682..c7e93f1a79 100644
--- a/app/Module/TopGivenNamesModule.php
+++ b/app/Module/TopGivenNamesModule.php
@@ -17,6 +17,7 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Stats;
use Fisharebest\Webtrees\Theme;
@@ -147,13 +148,13 @@ class TopGivenNamesModule extends AbstractModule implements ModuleBlockInterface
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Presentation style');
echo '</td><td class="optionbox">';
- echo select_edit_control('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
+ echo FunctionsEdit::selectEditControl('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
}
diff --git a/app/Module/TopPageViewsModule.php b/app/Module/TopPageViewsModule.php
index b4b143ad82..641c41e142 100644
--- a/app/Module/TopPageViewsModule.php
+++ b/app/Module/TopPageViewsModule.php
@@ -18,6 +18,7 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Theme;
@@ -141,13 +142,13 @@ class TopPageViewsModule extends AbstractModule implements ModuleBlockInterface
echo "<tr><td class=\"descriptionbox wrap width33\">";
echo I18N::translate('Place counts before or after name?');
echo "</td><td class=\"optionbox\">";
- echo select_edit_control('count_placement', array('before' => I18N::translate('before'), 'after' => I18N::translate('after')), null, $count_placement, '');
+ echo FunctionsEdit::selectEditControl('count_placement', array('before' => I18N::translate('before'), 'after' => I18N::translate('after')), null, $count_placement, '');
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
}
diff --git a/app/Module/TopSurnamesModule.php b/app/Module/TopSurnamesModule.php
index f64906de43..85e0423737 100644
--- a/app/Module/TopSurnamesModule.php
+++ b/app/Module/TopSurnamesModule.php
@@ -17,6 +17,9 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDb;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
+use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Query\QueryName;
use Fisharebest\Webtrees\Theme;
@@ -55,7 +58,7 @@ class TopSurnamesModule extends AbstractModule implements ModuleBlockInterface {
}
// This next function is a bit out of date, and doesn't cope well with surname variants
- $top_surnames = get_top_surnames($WT_TREE->getTreeId(), $COMMON_NAMES_THRESHOLD, $num);
+ $top_surnames = FunctionsDb::getTopSurnames($WT_TREE->getTreeId(), $COMMON_NAMES_THRESHOLD, $num);
// Remove names found in the "Remove Names" list
if ($COMMON_NAMES_REMOVE) {
@@ -95,20 +98,20 @@ class TopSurnamesModule extends AbstractModule implements ModuleBlockInterface {
switch ($infoStyle) {
case 'tagcloud':
uksort($all_surnames, '\Fisharebest\Webtrees\I18N::strcasecmp');
- $content = format_surname_tagcloud($all_surnames, 'indilist.php', true, $WT_TREE);
+ $content = FunctionsPrintLists::surnameTagCloud($all_surnames, 'indilist.php', true, $WT_TREE);
break;
case 'list':
uasort($all_surnames, '\Fisharebest\Webtrees\Module\TopSurnamesModule::surnameCountSort');
- $content = format_surname_list($all_surnames, '1', true, 'indilist.php', $WT_TREE);
+ $content = FunctionsPrintLists::surnameList($all_surnames, '1', true, 'indilist.php', $WT_TREE);
break;
case 'array':
uasort($all_surnames, '\Fisharebest\Webtrees\Module\TopSurnamesModule::surnameCountSort');
- $content = format_surname_list($all_surnames, '2', true, 'indilist.php', $WT_TREE);
+ $content = FunctionsPrintLists::surnameList($all_surnames, '2', true, 'indilist.php', $WT_TREE);
break;
case 'table':
default:
uasort($all_surnames, '\Fisharebest\Webtrees\Module\TopSurnamesModule::surnameCountSort');
- $content = format_surname_table($all_surnames, 'indilist.php', $WT_TREE);
+ $content = FunctionsPrintLists::surnameTable($all_surnames, 'indilist.php', $WT_TREE);
break;
}
@@ -160,13 +163,13 @@ class TopSurnamesModule extends AbstractModule implements ModuleBlockInterface {
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Presentation style');
echo '</td><td class="optionbox">';
- echo select_edit_control('infoStyle', array('list' => I18N::translate('bullet list'), 'array' => I18N::translate('compact list'), 'table' => I18N::translate('table'), 'tagcloud' => I18N::translate('tag cloud')), null, $infoStyle, '');
+ echo FunctionsEdit::selectEditControl('infoStyle', array('list' => I18N::translate('bullet list'), 'array' => I18N::translate('compact list'), 'table' => I18N::translate('table'), 'tagcloud' => I18N::translate('tag cloud')), null, $infoStyle, '');
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
diff --git a/app/Module/UpcomingAnniversariesModule.php b/app/Module/UpcomingAnniversariesModule.php
index 64351d1977..a41a6c210f 100644
--- a/app/Module/UpcomingAnniversariesModule.php
+++ b/app/Module/UpcomingAnniversariesModule.php
@@ -17,6 +17,8 @@ namespace Fisharebest\Webtrees\Module;
*/
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
+use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Theme;
@@ -69,12 +71,12 @@ class UpcomingAnniversariesModule extends AbstractModule implements ModuleBlockI
switch ($infoStyle) {
case 'list':
// Output style 1: Old format, no visible tables, much smaller text. Better suited to right side of page.
- $content .= print_events_list($startjd, $endjd, $onlyBDM ? 'BIRT MARR DEAT' : '', $filter, $sortStyle);
+ $content .= FunctionsPrintLists::eventsList($startjd, $endjd, $onlyBDM ? 'BIRT MARR DEAT' : '', $filter, $sortStyle);
break;
case 'table':
// Style 2: New format, tables, big text, etc. Not too good on right side of page
ob_start();
- $content .= print_events_table($startjd, $endjd, $onlyBDM ? 'BIRT MARR DEAT' : '', $filter, $sortStyle);
+ $content .= FunctionsPrintLists::eventsTable($startjd, $endjd, $onlyBDM ? 'BIRT MARR DEAT' : '', $filter, $sortStyle);
$content .= ob_get_clean();
break;
}
@@ -133,25 +135,25 @@ class UpcomingAnniversariesModule extends AbstractModule implements ModuleBlockI
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show only events of living individuals?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('filter', $filter);
+ echo FunctionsEdit::editFieldYesNo('filter', $filter);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Show only births, deaths, and marriages?');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('onlyBDM', $onlyBDM);
+ echo FunctionsEdit::editFieldYesNo('onlyBDM', $onlyBDM);
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Presentation style');
echo '</td><td class="optionbox">';
- echo select_edit_control('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
+ echo FunctionsEdit::selectEditControl('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Sort order');
echo '</td><td class="optionbox">';
- echo select_edit_control('sortStyle', array(
+ echo FunctionsEdit::selectEditControl('sortStyle', array(
/* I18N: An option in a list-box */ 'alpha' => I18N::translate('sort by name'),
/* I18N: An option in a list-box */ 'anniv' => I18N::translate('sort by date'),
), null, $sortStyle, '');
@@ -160,7 +162,7 @@ class UpcomingAnniversariesModule extends AbstractModule implements ModuleBlockI
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
}
diff --git a/app/Module/UserJournalModule.php b/app/Module/UserJournalModule.php
index 7330517656..026d5e3730 100644
--- a/app/Module/UserJournalModule.php
+++ b/app/Module/UserJournalModule.php
@@ -18,6 +18,7 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDate;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Theme;
@@ -78,7 +79,7 @@ class UserJournalModule extends AbstractModule implements ModuleBlockInterface {
foreach ($usernews as $news) {
$content .= '<div class="journal_box">';
$content .= '<div class="news_title">' . $news->subject . '</div>';
- $content .= '<div class="news_date">' . format_timestamp($news->updated) . '</div>';
+ $content .= '<div class="news_date">' . FunctionsDate::formatTimestamp($news->updated) . '</div>';
if ($news->body == strip_tags($news->body)) {
// No HTML?
$news->body = nl2br($news->body, false);
diff --git a/app/Module/UserMessagesModule.php b/app/Module/UserMessagesModule.php
index 780f62cf49..01739de07a 100644
--- a/app/Module/UserMessagesModule.php
+++ b/app/Module/UserMessagesModule.php
@@ -18,6 +18,8 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDate;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Theme;
use Fisharebest\Webtrees\User;
@@ -89,7 +91,7 @@ class UserMessagesModule extends AbstractModule implements ModuleBlockInterface
$content .= '<tr>';
$content .= '<td class="list_value_wrap"><input type="checkbox" id="cb_message' . $message->message_id . '" name="message_id[]" value="' . $message->message_id . '"></td>';
$content .= '<td class="list_value_wrap"><a href="#" onclick="return expand_layer(\'message' . $message->message_id . '\');"><i id="message' . $message->message_id . '_img" class="icon-plus"></i> <b dir="auto">' . Filter::escapeHtml($message->subject) . '</b></a></td>';
- $content .= '<td class="list_value_wrap">' . format_timestamp($message->created + WT_TIMESTAMP_OFFSET) . '</td>';
+ $content .= '<td class="list_value_wrap">' . FunctionsDate::formatTimestamp($message->created + WT_TIMESTAMP_OFFSET) . '</td>';
$content .= '<td class="list_value_wrap">';
$user = User::findByIdentifier($message->sender);
if ($user) {
@@ -151,7 +153,7 @@ class UserMessagesModule extends AbstractModule implements ModuleBlockInterface
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
}
diff --git a/app/Module/YahrzeitModule.php b/app/Module/YahrzeitModule.php
index 231e667cc2..301d5c3f61 100644
--- a/app/Module/YahrzeitModule.php
+++ b/app/Module/YahrzeitModule.php
@@ -22,6 +22,8 @@ use Fisharebest\Webtrees\Date;
use Fisharebest\Webtrees\Date\GregorianDate;
use Fisharebest\Webtrees\Date\JewishDate;
use Fisharebest\Webtrees\Filter;
+use Fisharebest\Webtrees\Functions\FunctionsDb;
+use Fisharebest\Webtrees\Functions\FunctionsEdit;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Theme;
@@ -76,7 +78,7 @@ class YahrzeitModule extends AbstractModule implements ModuleBlockInterface {
// Fetch normal anniversaries...
$yahrzeits = array();
for ($jd = $startjd - 1; $jd <= $endjd + $days; ++$jd) {
- foreach (get_anniversary_events($jd, 'DEAT _YART', $WT_TREE) as $fact) {
+ foreach (FunctionsDb::getAnniversaryEvents($jd, 'DEAT _YART', $WT_TREE) as $fact) {
// Exact hebrew dates only
$date = $fact->getDate();
if ($date->minimumDate() instanceof JewishDate && $date->minimumJulianDay() === $date->maximumJulianDay()) {
@@ -262,13 +264,13 @@ class YahrzeitModule extends AbstractModule implements ModuleBlockInterface {
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Presentation style');
echo '</td><td class="optionbox">';
- echo select_edit_control('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
+ echo FunctionsEdit::selectEditControl('infoStyle', array('list' => I18N::translate('list'), 'table' => I18N::translate('table')), null, $infoStyle, '');
echo '</td></tr>';
echo '<tr><td class="descriptionbox wrap width33">';
echo I18N::translate('Calendar');
echo '</td><td class="optionbox">';
- echo select_edit_control('calendar', array(
+ echo FunctionsEdit::selectEditControl('calendar', array(
'jewish' => /* I18N: The Hebrew/Jewish calendar */ I18N::translate('Jewish'),
'gregorian' => /* I18N: The gregorian calendar */ I18N::translate('Gregorian'),
), null, $calendar, '');
@@ -277,7 +279,7 @@ class YahrzeitModule extends AbstractModule implements ModuleBlockInterface {
echo '<tr><td class="descriptionbox wrap width33">';
echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
echo '</td><td class="optionbox">';
- echo edit_field_yes_no('block', $block);
+ echo FunctionsEdit::editFieldYesNo('block', $block);
echo '</td></tr>';
}
}
diff --git a/app/Report/ReportHtml.php b/app/Report/ReportHtml.php
index e88cda069d..aa5ad15eb3 100644
--- a/app/Report/ReportHtml.php
+++ b/app/Report/ReportHtml.php
@@ -16,6 +16,7 @@ namespace Fisharebest\Webtrees\Report;
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use Fisharebest\Webtrees\Controller\SimpleController;
+use Fisharebest\Webtrees\Functions\FunctionsRtl;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Media;
@@ -671,7 +672,7 @@ class ReportHtml extends ReportBase {
$lfct = count($lines);
$wraptext = '';
foreach ($lines as $line) {
- $wtext = utf8_wordwrap($line, $lw, "\n", true);
+ $wtext = FunctionsRtl::utf8WordWrap($line, $lw, "\n", true);
$wraptext .= $wtext;
// Add a new line as long as it’s not the last line
if ($lfct > 1) {
diff --git a/app/Report/ReportParserGenerate.php b/app/Report/ReportParserGenerate.php
index ce257dd067..4fa56c07f1 100644
--- a/app/Report/ReportParserGenerate.php
+++ b/app/Report/ReportParserGenerate.php
@@ -19,6 +19,8 @@ use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Date;
use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\Functions\Functions;
+use Fisharebest\Webtrees\Functions\FunctionsDate;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
@@ -533,7 +535,7 @@ class ReportParserGenerate extends ReportParserBase {
* XML <Now /> element handler
*/
private function nowStartHandler() {
- $g = timestamp_to_gedcom_date(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET);
+ $g = FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET);
$this->current_element->addText($g->display());
}
@@ -598,7 +600,7 @@ class ReportParserGenerate extends ReportParserBase {
} else {
$temp = explode(" ", trim($tgedrec));
$level = $temp[0] + 1;
- $newgedrec = get_sub_record($level, "$level $tag", $tgedrec);
+ $newgedrec = Functions::getSubRecord($level, "$level $tag", $tgedrec);
$tgedrec = $newgedrec;
}
}
@@ -919,7 +921,7 @@ class ReportParserGenerate extends ReportParserBase {
$level = $attrs['level'];
}
$tags = preg_split('/[: ]/', $tag);
- $value = $this->get_gedcom_value($tag, $level, $this->gedrec);
+ $value = $this->getGedcomValue($tag, $level, $this->gedrec);
switch (end($tags)) {
case 'DATE':
$tmp = new Date($value);
@@ -985,10 +987,10 @@ class ReportParserGenerate extends ReportParserBase {
$t = $tags[$i];
if (!empty($t)) {
if ($i < ($count - 1)) {
- $subrec = get_sub_record($level, "$level $t", $subrec);
+ $subrec = Functions::getSubRecord($level, "$level $t", $subrec);
if (empty($subrec)) {
$level--;
- $subrec = get_sub_record($level, "@ $t", $this->gedrec);
+ $subrec = Functions::getSubRecord($level, "@ $t", $this->gedrec);
if (empty($subrec)) {
return;
}
@@ -1002,7 +1004,7 @@ class ReportParserGenerate extends ReportParserBase {
$count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER);
$i = 0;
while ($i < $count) {
- $this->repeats[] = get_sub_record($level, "$level $t", $subrec, $i + 1);
+ $this->repeats[] = Functions::getSubRecord($level, "$level $t", $subrec, $i + 1);
$i++;
}
}
@@ -1158,7 +1160,7 @@ class ReportParserGenerate extends ReportParserBase {
$record = GedcomRecord::getInstance($id, $WT_TREE);
if (empty($attrs['diff']) && !empty($id)) {
$facts = $record->getFacts();
- sort_facts($facts);
+ Functions::sortFacts($facts);
$this->repeats = array();
$nonfacts = explode(',', $tag);
foreach ($facts as $event) {
@@ -1231,7 +1233,7 @@ class ReportParserGenerate extends ReportParserBase {
}
}
$this->desc = trim($match[2]);
- $this->desc .= get_cont(2, $this->gedrec);
+ $this->desc .= Functions::getCont(2, $this->gedrec);
}
$repeat_parser = xml_parser_create();
$this->parser = $repeat_parser;
@@ -1343,7 +1345,7 @@ class ReportParserGenerate extends ReportParserBase {
}
$condition = $attrs['condition'];
- $condition = $this->substitute_vars($condition);
+ $condition = $this->substituteVars($condition);
$condition = str_replace(array(" LT ", " GT "), array("<", ">"), $condition);
// Replace the first accurance only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
$condition = str_replace("@fact", $this->fact, $condition);
@@ -1370,10 +1372,10 @@ class ReportParserGenerate extends ReportParserBase {
if ($level == 0) {
$level++;
}
- $value = $this->get_gedcom_value($id, $level, $this->gedrec);
+ $value = $this->getGedcomValue($id, $level, $this->gedrec);
if (empty($value)) {
$level++;
- $value = $this->get_gedcom_value($id, $level, $this->gedrec);
+ $value = $this->getGedcomValue($id, $level, $this->gedrec);
}
$value = preg_replace("/^@(" . WT_REGEX_XREF . ")@$/", "$1", $value);
$value = "\"" . addslashes($value) . "\"";
@@ -1456,7 +1458,7 @@ class ReportParserGenerate extends ReportParserBase {
* XML <AgeAtDeath /> element handler
*/
private function ageAtDeathStartHandler() {
- // This duplicates functionality in format_fact_date()
+ // This duplicates functionality in FunctionsPrint::format_fact_date()
global $factrec, $WT_TREE;
$match = array();
@@ -1497,7 +1499,7 @@ class ReportParserGenerate extends ReportParserBase {
if ($age != '' && $age != "0d") {
if ($fact_age != '' && $fact_age != $age || $fact_age == '' && $husb_age == '' && $wife_age == '' || $husb_age != '' && $person->getSex() == 'M' && $husb_age != $age || $wife_age != '' && $person->getSex() == 'F' && $wife_age != $age
) {
- $value = get_age_at_event($age, false);
+ $value = FunctionsDate::getAgeAtEvent($age, false);
$abbrev = substr($value, 0, strpos($value, ' ') + 5);
if ($value !== $abbrev) {
$value = $abbrev . '.';
@@ -1843,7 +1845,7 @@ class ReportParserGenerate extends ReportParserBase {
$sql_order_by = "";
foreach ($attrs as $attr => $value) {
if (strpos($attr, 'filter') === 0 && $value) {
- $value = $this->substitute_vars($value);
+ $value = $this->substituteVars($value);
// Convert the various filters into SQL
if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
$sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=i_file AND {$attr}.d_gid=i_id)";
@@ -1922,7 +1924,7 @@ class ReportParserGenerate extends ReportParserBase {
$sql_order_by = "";
foreach ($attrs as $attr => $value) {
if (strpos($attr, 'filter') === 0 && $value) {
- $value = $this->substitute_vars($value);
+ $value = $this->substituteVars($value);
// Convert the various filters into SQL
if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
$sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=f_file AND {$attr}.d_gid=f_id)";
@@ -2092,13 +2094,13 @@ class ReportParserGenerate extends ReportParserBase {
}
$tags = explode(":", $tag);
$t = end($tags);
- $v = $this->get_gedcom_value($tag, 1, $grec);
+ $v = $this->getGedcomValue($tag, 1, $grec);
//-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
if ($t == "EMAIL" && empty($v)) {
$tag = str_replace("EMAIL", "_EMAIL", $tag);
$tags = explode(":", $tag);
$t = end($tags);
- $v = get_sub_record(1, $tag, $grec);
+ $v = Functions::getSubRecord(1, $tag, $grec);
}
switch ($expr) {
@@ -2296,7 +2298,7 @@ class ReportParserGenerate extends ReportParserBase {
if (isset($attrs['id'])) {
$id = $attrs['id'];
}
- $match = $this->substitute_vars($match);
+ $match = $this->substituteVars($match);
if (preg_match("/\\$(\w+)/", $id, $match)) {
$id = $this->vars[$match[1]]['id'];
$id = trim($id);
@@ -2344,18 +2346,18 @@ class ReportParserGenerate extends ReportParserBase {
}
break;
case "direct-ancestors":
- $this->add_ancestors($this->list, $id, false, $maxgen);
+ $this->addAncestors($this->list, $id, false, $maxgen);
break;
case "ancestors":
- $this->add_ancestors($this->list, $id, true, $maxgen);
+ $this->addAncestors($this->list, $id, true, $maxgen);
break;
case "descendants":
$this->list[$id]->generation = 1;
- $this->add_descendancy($this->list, $id, false, $maxgen);
+ $this->addDescendancy($this->list, $id, false, $maxgen);
break;
case "all":
- $this->add_ancestors($this->list, $id, true, $maxgen);
- $this->add_descendancy($this->list, $id, true, $maxgen);
+ $this->addAncestors($this->list, $id, true, $maxgen);
+ $this->addDescendancy($this->list, $id, true, $maxgen);
break;
}
}
@@ -2568,7 +2570,7 @@ class ReportParserGenerate extends ReportParserBase {
* @param bool $parents
* @param int $generations
*/
- function add_descendancy(&$list, $pid, $parents = false, $generations = -1) {
+ public function addDescendancy(&$list, $pid, $parents = false, $generations = -1) {
global $WT_TREE;
$person = Individual::getInstance($pid, $WT_TREE);
@@ -2615,7 +2617,7 @@ class ReportParserGenerate extends ReportParserBase {
}
if ($generations == -1 || $list[$pid]->generation + 1 < $generations) {
foreach ($children as $child) {
- $this->add_descendancy($list, $child->getXref(), $parents, $generations); // recurse on the childs family
+ $this->addDescendancy($list, $child->getXref(), $parents, $generations); // recurse on the childs family
}
}
}
@@ -2627,7 +2629,7 @@ class ReportParserGenerate extends ReportParserBase {
* @param bool $children
* @param int $generations
*/
- function add_ancestors(&$list, $pid, $children = false, $generations = -1) {
+ public function addAncestors(&$list, $pid, $children = false, $generations = -1) {
global $WT_TREE;
$genlist = array($pid);
@@ -2680,7 +2682,7 @@ class ReportParserGenerate extends ReportParserBase {
*
* @return string the value of a gedcom tag from the given gedcom record
*/
- function get_gedcom_value($tag, $level, $gedrec) {
+ public function getGedcomValue($tag, $level, $gedrec) {
global $WT_TREE;
if (empty($gedrec)) {
@@ -2695,14 +2697,14 @@ class ReportParserGenerate extends ReportParserBase {
$subrec = $gedrec;
foreach ($tags as $t) {
$lastsubrec = $subrec;
- $subrec = get_sub_record($level, "$level $t", $subrec);
+ $subrec = Functions::getSubRecord($level, "$level $t", $subrec);
if (empty($subrec) && $origlevel == 0) {
$level--;
- $subrec = get_sub_record($level, "$level $t", $lastsubrec);
+ $subrec = Functions::getSubRecord($level, "$level $t", $lastsubrec);
}
if (empty($subrec)) {
if ($t == "TITL") {
- $subrec = get_sub_record($level, "$level ABBR", $lastsubrec);
+ $subrec = Functions::getSubRecord($level, "$level ABBR", $lastsubrec);
if (!empty($subrec)) {
$t = "ABBR";
}
@@ -2711,7 +2713,7 @@ class ReportParserGenerate extends ReportParserBase {
if ($level > 0) {
$level--;
}
- $subrec = get_sub_record($level, "@ $t", $gedrec);
+ $subrec = Functions::getSubRecord($level, "@ $t", $gedrec);
if (empty($subrec)) {
return '';
}
@@ -2739,7 +2741,7 @@ class ReportParserGenerate extends ReportParserBase {
}
}
if ($level != 0 || $t != "NOTE") {
- $value .= get_cont($level + 1, $subrec);
+ $value .= Functions::getCont($level + 1, $subrec);
}
return $value;
@@ -2755,7 +2757,7 @@ class ReportParserGenerate extends ReportParserBase {
*
* @return string
*/
- private function substitute_vars($expression) {
+ private function substituteVars($expression) {
$tmp = $this->vars; // PHP5.3 cannot use $this in closures
return preg_replace_callback(
@@ -2765,6 +2767,7 @@ class ReportParserGenerate extends ReportParserBase {
return '"' . $tmp[$matches[1]]['id'] . '"';
} else {
Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
+
return '$' . $matches[1];
}
},
diff --git a/app/Report/ReportPdfCell.php b/app/Report/ReportPdfCell.php
index 0f367df039..b25d5972ba 100644
--- a/app/Report/ReportPdfCell.php
+++ b/app/Report/ReportPdfCell.php
@@ -15,6 +15,7 @@ namespace Fisharebest\Webtrees\Report;
* 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\Functions\FunctionsRtl;
/**
* Class ReportPdfCell
@@ -108,7 +109,7 @@ class ReportPdfCell extends ReportBaseCell {
if ($renderer->checkPageBreakPDF($cHT)) {
$this->top = $renderer->GetY();
}
- $temptext = spanLTRRTL($temptext, "BOTH");
+ $temptext = FunctionsRtl::spanLtrRtl($temptext, "BOTH");
}
// HTML ready - last value is true
$renderer->MultiCell(
diff --git a/app/Report/ReportPdfText.php b/app/Report/ReportPdfText.php
index 6c00a3d493..f129532a5c 100644
--- a/app/Report/ReportPdfText.php
+++ b/app/Report/ReportPdfText.php
@@ -15,6 +15,7 @@ namespace Fisharebest\Webtrees\Report;
* 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\Functions\FunctionsRtl;
/**
* Class ReportPdfText
@@ -44,7 +45,7 @@ class ReportPdfText extends ReportBaseText {
} else {
$renderer->SetTextColor(0, 0, 0);
}
- $temptext = spanLTRRTL($temptext, "BOTH");
+ $temptext = FunctionsRtl::spanLtrRtl($temptext, "BOTH");
$temptext = str_replace(
array('<br><span dir="rtl" >', '<br><span dir="ltr" >', '> ', ' <'),
array('<span dir="rtl" ><br>', '<span dir="ltr" ><br>', '>&nbsp;', '&nbsp;<'),
diff --git a/app/Schema/Migration22.php b/app/Schema/Migration22.php
index 8a73348adf..1f14eb27ef 100644
--- a/app/Schema/Migration22.php
+++ b/app/Schema/Migration22.php
@@ -51,7 +51,7 @@ class Migration22 implements MigrationInterface {
// $WT_DATA_DIR = /home/fisharebest/public_html/webtrees/data/
// Therefore we need to calculate ../../../my_pictures/
$_media_dir = '';
- $_tmp_dir = $WT_DATA_DIR;
+ $_tmp_dir = $WT_DATA_DIR;
while (strpos($_mf_dir, $_tmp_dir) !== 0) {
$_media_dir .= '../';
$_tmp_dir = preg_replace('~[^/\\\\]+[/\\\\]$~', '', $_tmp_dir);
diff --git a/app/Stats.php b/app/Stats.php
index 53e3119767..5666d8f4c8 100644
--- a/app/Stats.php
+++ b/app/Stats.php
@@ -16,6 +16,11 @@ namespace Fisharebest\Webtrees;
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+use Fisharebest\Webtrees\Functions\Functions;
+use Fisharebest\Webtrees\Functions\FunctionsDate;
+use Fisharebest\Webtrees\Functions\FunctionsDb;
+use Fisharebest\Webtrees\Functions\FunctionsPrint;
+use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule;
use Fisharebest\Webtrees\Module\UserFavoritesModule;
use Fisharebest\Webtrees\Query\QueryName;
@@ -1291,7 +1296,7 @@ class Stats {
case 'place':
$fact = GedcomRecord::getInstance($row['d_gid'], $this->tree)->getFirstFact($row['d_fact']);
if ($fact) {
- $result = format_fact_place($fact, true, true, true);
+ $result = FunctionsPrint::formatFactPlace($fact, true, true, true);
} else {
$result = I18N::translate('Private');
}
@@ -2108,7 +2113,7 @@ class Stats {
} else {
$age = $age . 'd';
}
- $age = get_age_at_event($age, true);
+ $age = FunctionsDate::getAgeAtEvent($age, true);
if ($person->canShow()) {
if ($type == 'list') {
$top10[] = "<li><a href=\"" . $person->getHtmlUrl() . "\">" . $person->getFullName() . "</a> (" . $age . ")" . "</li>";
@@ -2185,7 +2190,7 @@ class Stats {
} else {
$age = $age . 'd';
}
- $age = get_age_at_event($age, true);
+ $age = FunctionsDate::getAgeAtEvent($age, true);
if ($type === 'list') {
$top10[] = "<li><a href=\"" . $person->getHtmlUrl() . "\">" . $person->getFullName() . "</a> (" . $age . ")" . "</li>";
} else {
@@ -2254,7 +2259,7 @@ class Stats {
$age = $age . 'd';
}
- return get_age_at_event($age, true);
+ return FunctionsDate::getAgeAtEvent($age, true);
} else {
return I18N::number($age / 365.25);
}
@@ -2682,7 +2687,7 @@ class Stats {
case 'place':
$fact = $record->getFirstFact($row['fact']);
if ($fact) {
- $result = format_fact_place($fact, true, true, true);
+ $result = FunctionsPrint::formatFactPlace($fact, true, true, true);
} else {
$result = I18N::translate('Private');
}
@@ -2832,7 +2837,7 @@ class Stats {
} else {
$age = $age . 'd';
}
- $result = get_age_at_event($age, true);
+ $result = FunctionsDate::getAgeAtEvent($age, true);
} else {
$result = I18N::number((int) ($age / 365.25));
}
@@ -2941,7 +2946,7 @@ class Stats {
} else {
$age = $age . 'd';
}
- $age = get_age_at_event($age, true);
+ $age = FunctionsDate::getAgeAtEvent($age, true);
if ($type === 'age') {
return $age;
}
@@ -3036,7 +3041,7 @@ class Stats {
} else {
$age = $age . 'd';
}
- $age = get_age_at_event($age, true);
+ $age = FunctionsDate::getAgeAtEvent($age, true);
if ($family->canShow()) {
if ($type === 'list') {
$top10[] = '<li><a href="' . $family->getHtmlUrl() . '">' . $family->getFullName() . '</a> (' . $age . ')' . "</li>";
@@ -3124,7 +3129,7 @@ class Stats {
} else {
$age = $age . 'd';
}
- $result = get_age_at_event($age, true);
+ $result = FunctionsDate::getAgeAtEvent($age, true);
} else {
$result = (int) ($age / 365.25);
}
@@ -4110,7 +4115,7 @@ class Stats {
} else {
$age = $age . 'd';
}
- $age = get_age_at_event($age, true);
+ $age = FunctionsDate::getAgeAtEvent($age, true);
if ($type == 'age') {
return $age;
}
@@ -4835,7 +4840,7 @@ class Stats {
} else {
$sorting = 'alpha';
}
- $surname_list = get_common_surnames($threshold, $this->tree);
+ $surname_list = FunctionsDb::getCommonSurnames($threshold, $this->tree);
if (count($surname_list) == 0) {
return '';
}
@@ -4863,14 +4868,14 @@ class Stats {
$surnames = array_merge($surnames, QueryName::surnames($this->tree, $surname, '', false, false));
}
- return format_surname_list($surnames, ($type == 'list' ? 1 : 2), $show_tot, 'indilist.php', $this->tree);
+ return FunctionsPrintLists::surnameList($surnames, ($type == 'list' ? 1 : 2), $show_tot, 'indilist.php', $this->tree);
}
/**
* @return string
*/
public function getCommonSurname() {
- $surnames = array_keys(get_top_surnames($this->tree->getTreeId(), 1, 1));
+ $surnames = array_keys(FunctionsDb::getTopSurnames($this->tree->getTreeId(), 1, 1));
return array_shift($surnames);
}
@@ -4949,7 +4954,7 @@ class Stats {
}
$sizes = explode('x', $size);
$tot_indi = $this->totalIndividualsQuery();
- $surnames = get_common_surnames($threshold, $this->tree);
+ $surnames = FunctionsDb::getCommonSurnames($threshold, $this->tree);
if (count($surnames) <= 0) {
return '';
}
@@ -5425,9 +5430,9 @@ class Stats {
}
if (Auth::id() != $user->getUserId() && $user->getPreference('contactmethod') != 'none') {
if ($type == 'list') {
- $content .= '<br><a class="icon-email" href="#" onclick="return message(\'' . $user->getUserId() . '\', \'\', \'' . Filter::escapeJs(get_query_url()) . '\');" title="' . I18N::translate('Send a message') . '"></a>';
+ $content .= '<br><a class="icon-email" href="#" onclick="return message(\'' . $user->getUserId() . '\', \'\', \'' . Filter::escapeJs(Functions::getQueryUrl()) . '\');" title="' . I18N::translate('Send a message') . '"></a>';
} else {
- $content .= ' <a class="icon-email" href="#" onclick="return message(\'' . $user->getUserId() . '\', \'\', \'' . Filter::escapeJs(get_query_url()) . '\');" title="' . I18N::translate('Send a message') . '"></a>';
+ $content .= ' <a class="icon-email" href="#" onclick="return message(\'' . $user->getUserId() . '\', \'\', \'' . Filter::escapeJs(Functions::getQueryUrl()) . '\');" title="' . I18N::translate('Send a message') . '"></a>';
}
}
if ($type == 'list') {
@@ -5561,7 +5566,7 @@ class Stats {
$datestamp = I18N::dateFormat();
}
- return timestamp_to_gedcom_date($user->getPreference('reg_timestamp'))->display(false, $datestamp);
+ return FunctionsDate::timestampToGedcomDate($user->getPreference('reg_timestamp'))->display(false, $datestamp);
case 'regtime':
if (is_array($params) && isset($params[0]) && $params[0] != '') {
$datestamp = $params[0];
@@ -5664,7 +5669,7 @@ class Stats {
* @return string
*/
public function serverDate() {
- return timestamp_to_gedcom_date(WT_TIMESTAMP)->display();
+ return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP)->display();
}
/**
@@ -5695,7 +5700,7 @@ class Stats {
* @return string
*/
public function browserDate() {
- return timestamp_to_gedcom_date(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET)->display();
+ return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET)->display();
}
/**
diff --git a/app/Theme/AdministrationTheme.php b/app/Theme/AdministrationTheme.php
index ea3823e144..5eddaa8b2d 100644
--- a/app/Theme/AdministrationTheme.php
+++ b/app/Theme/AdministrationTheme.php
@@ -212,7 +212,7 @@ class AdministrationTheme extends BaseTheme {
/** {@inheritdoc} */
protected function secondaryMenuContent(array $menus) {
- return implode('', array_map(function ($menu) { return $menu->bootstrap(); }, $menus));
+ return implode('', array_map(function (Menu $menu) { return $menu->bootstrap(); }, $menus));
}
/** {@inheritdoc} */
diff --git a/app/Theme/BaseTheme.php b/app/Theme/BaseTheme.php
index 1d5f4ec78a..d9e6785b5d 100644
--- a/app/Theme/BaseTheme.php
+++ b/app/Theme/BaseTheme.php
@@ -21,6 +21,7 @@ use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Fact;
use Fisharebest\Webtrees\Filter;
use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\HitCounter;
@@ -253,7 +254,7 @@ abstract class BaseTheme {
case 'mailto':
return '<a href="mailto:' . Filter::escapeHtml($user->getEmail()) . '">' . $user->getRealNameHtml() . '</a>';
default:
- return "<a href='#' onclick='message(\"" . Filter::escapeHtml($user->getUserName()) . "\", \"" . $method . "\", \"" . WT_BASE_URL . Filter::escapeHtml(get_query_url()) . "\", \"\");return false;'>" . $user->getRealNameHtml() . '</a>';
+ return "<a href='#' onclick='message(\"" . Filter::escapeHtml($user->getUserName()) . "\", \"" . $method . "\", \"" . WT_BASE_URL . Filter::escapeHtml(Functions::getQueryUrl()) . "\", \"\");return false;'>" . $user->getRealNameHtml() . '</a>';
}
}
@@ -1355,7 +1356,7 @@ abstract class BaseTheme {
$language_tag = $locale->languageTag();
$menu->addSubmenu(new Menu(
$locale->endonym(),
- get_query_url(array('lang' => $language_tag), '&amp;'),
+ Functions::getQueryUrl(array('lang' => $language_tag), '&amp;'),
'menu-language-' . $language_tag . (WT_LOCALE === $language_tag ? ' active' : '')
));
}
@@ -1509,7 +1510,7 @@ abstract class BaseTheme {
if (Auth::check() || Auth::isSearchEngine() || WT_SCRIPT_NAME === 'login.php') {
return null;
} else {
- return new Menu(I18N::translate('Login'), WT_LOGIN_URL . '?url=' . rawurlencode(get_query_url()));
+ return new Menu(I18N::translate('Login'), WT_LOGIN_URL . '?url=' . rawurlencode(Functions::getQueryUrl()));
}
}
@@ -1695,7 +1696,7 @@ abstract class BaseTheme {
foreach (Theme::installedThemes() as $theme) {
$submenu = new Menu(
$theme->themeName(),
- get_query_url(array('theme' => $theme->themeId()), '&amp;'),
+ Functions::getQueryUrl(array('theme' => $theme->themeId()), '&amp;'),
'menu-theme-' . $theme->themeId() . ($theme === $this ? ' active' : '')
);
$submenus[] = $submenu;
@@ -1957,7 +1958,7 @@ abstract class BaseTheme {
* @return string
*/
protected function primaryMenuContent(array $menus) {
- return implode('', array_map(function ($menu) { return $menu->getMenuAsList(); }, $menus));
+ return implode('', array_map(function (Menu $menu) { return $menu->getMenuAsList(); }, $menus));
}
/**
@@ -1996,7 +1997,7 @@ abstract class BaseTheme {
* @return string
*/
protected function secondaryMenuContent(array $menus) {
- return implode('', array_map(function ($menu) { return $menu->getMenuAsList(); }, $menus));
+ return implode('', array_map(function (Menu $menu) { return $menu->getMenuAsList(); }, $menus));
}
/**
diff --git a/app/Theme/ColorsTheme.php b/app/Theme/ColorsTheme.php
index 0c04b80a4e..2d081bd537 100644
--- a/app/Theme/ColorsTheme.php
+++ b/app/Theme/ColorsTheme.php
@@ -16,6 +16,7 @@ namespace Fisharebest\Webtrees\Theme;
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use Fisharebest\Webtrees\Auth;
+use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Menu;
use Fisharebest\Webtrees\Session;
@@ -138,7 +139,7 @@ class ColorsTheme extends CloudsTheme {
foreach ($this->palettes as $palette_id => $palette_name) {
$menu->addSubmenu(new Menu(
$palette_name,
- get_query_url(array('themecolor' => $palette_id), '&amp;'),
+ Functions::getQueryUrl(array('themecolor' => $palette_id), '&amp;'),
'menu-color-' . $palette_id . ($this->palette === $palette_id ? ' active' : '')
));
}
diff --git a/app/Tree.php b/app/Tree.php
index b8e8b18982..660c92efe4 100644
--- a/app/Tree.php
+++ b/app/Tree.php
@@ -16,6 +16,8 @@ namespace Fisharebest\Webtrees;
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+use Fisharebest\Webtrees\Functions\FunctionsExport;
+use Fisharebest\Webtrees\Functions\FunctionsImport;
use PDOException;
/**
@@ -677,9 +679,9 @@ class Tree {
'tree_id_5' => $this->tree_id,
));
- $buffer = reformat_record_export(gedcom_header($this));
+ $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this));
while ($row = $stmt->fetch()) {
- $buffer .= reformat_record_export($row->gedcom);
+ $buffer .= FunctionsExport::reformatRecord($row->gedcom);
if (strlen($buffer) > 65535) {
fwrite($stream, $buffer);
$buffer = '';
@@ -858,7 +860,7 @@ class Tree {
// Accept this pending change
if (Auth::user()->getPreference('auto_accept')) {
- accept_all_changes($xref, $this->tree_id);
+ FunctionsImport::acceptAllChanges($xref, $this->tree_id);
}
// Return the newly created record. Note that since GedcomRecord
// has a cache of pending changes, we cannot use it to create a