1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16namespace Fisharebest\Webtrees; 17 18/** 19 * Defined in session.php 20 * 21 * @global Tree $WT_TREE 22 */ 23global $WT_TREE; 24 25use Fisharebest\Webtrees\Controller\PageController; 26use Fisharebest\Webtrees\Functions\FunctionsDb; 27use Fisharebest\Webtrees\Functions\FunctionsPrint; 28 29define('WT_SCRIPT_NAME', 'admin_site_merge.php'); 30require './includes/session.php'; 31 32$controller = new PageController; 33$controller 34 ->restrictAccess(Auth::isManager($WT_TREE)) 35 ->setPageTitle(I18N::translate('Merge records') . ' — ' . $WT_TREE->getTitleHtml()) 36 ->addExternalJavascript(WT_AUTOCOMPLETE_JS_URL) 37 ->addInlineJavascript('autocomplete();'); 38 39$gid1 = Filter::post('gid1', WT_REGEX_XREF, Filter::get('gid1', WT_REGEX_XREF)); 40$gid2 = Filter::post('gid2', WT_REGEX_XREF, Filter::get('gid2', WT_REGEX_XREF)); 41$keep1 = Filter::postArray('keep1'); 42$keep2 = Filter::postArray('keep2'); 43$rec1 = GedcomRecord::getInstance($gid1, $WT_TREE); 44$rec2 = GedcomRecord::getInstance($gid2, $WT_TREE); 45 46if ($gid1 && !$rec1) { 47 FlashMessages::addMessage(I18N::translate('%1$s does not exist.', $gid1), 'danger'); 48} 49 50if ($gid2 && !$rec2) { 51 FlashMessages::addMessage(I18N::translate('%1$s does not exist.', $gid2), 'danger'); 52} 53 54if ($rec1 && $rec2 && $rec1->getXref() === $rec2->getXref()) { 55 FlashMessages::addMessage(I18N::translate('You entered the same IDs. You cannot merge the same records.'), 'danger'); 56} 57 58if ($rec1 && $rec2 && $rec1::RECORD_TYPE !== $rec2::RECORD_TYPE) { 59 FlashMessages::addMessage(I18N::translate('Records are not the same type. Cannot merge records that are not the same type.'), 'danger'); 60} 61 62// Facts found both records 63$facts = array(); 64// Facts found in only one record 65$facts1 = array(); 66$facts2 = array(); 67 68if ($rec1) { 69 foreach ($rec1->getFacts() as $fact) { 70 if (!$fact->isPendingDeletion() && $fact->getTag() !== 'CHAN') { 71 $facts1[$fact->getFactId()] = $fact; 72 } 73 } 74} 75 76if ($rec2) { 77 foreach ($rec2->getFacts() as $fact) { 78 if (!$fact->isPendingDeletion() && $fact->getTag() !== 'CHAN') { 79 $facts2[$fact->getFactId()] = $fact; 80 } 81 } 82} 83 84foreach ($facts1 as $id1 => $fact1) { 85 foreach ($facts2 as $id2 => $fact2) { 86 if ($fact1->getFactId() === $fact2->getFactId()) { 87 $facts[] = $fact1; 88 unset($facts1[$id1]); 89 unset($facts2[$id2]); 90 } 91 } 92} 93 94if ($rec1 && $rec2 && $rec1->getXref() !== $rec2->getXref() && $rec1::RECORD_TYPE === $rec2::RECORD_TYPE && Filter::post('action') === 'merge' && Filter::checkCsrf()) { 95 // Use the XREF of the record. 96 $gid1 = $rec1->getXref(); 97 $gid2 = $rec2->getXref(); 98 99 $ids = FunctionsDb::fetchAllLinks($gid2, $WT_TREE->getTreeId()); 100 101 // If we are not auto-accepting, then we can show a link to the pending deletion 102 if (Auth::user()->getPreference('auto_accept')) { 103 $record2_name = $rec2->getFullName(); 104 } else { 105 $record2_name = '<a class="alert-link" href="' . $rec2->getHtmlUrl() . '">' . $rec2->getFullName() . '</a>'; 106 } 107 108 foreach ($ids as $id) { 109 $record = GedcomRecord::getInstance($id, $WT_TREE); 110 if (!$record->isPendingDeletion()) { 111 FlashMessages::addMessage(I18N::translate( 112 /* I18N: The placeholders are the names of individuals, sources, etc. */ 113 'The link from “%1$s” to “%2$s” has been updated.', 114 '<a class="alert-link" href="' . $record->getHtmlUrl() . '">' . $record->getFullName() . '</a>', 115 $record2_name 116 ), 'info'); 117 $gedcom = str_replace("@$gid2@", "@$gid1@", $record->getGedcom()); 118 $gedcom = preg_replace( 119 '/(\n1.*@.+@.*(?:(?:\n[2-9].*)*))((?:\n1.*(?:\n[2-9].*)*)*\1)/', 120 '$2', 121 $gedcom 122 ); 123 $record->updateRecord($gedcom, true); 124 } 125 } 126 // Update any linked user-accounts 127 Database::prepare( 128 "UPDATE `##user_gedcom_setting`" . 129 " SET setting_value=?" . 130 " WHERE gedcom_id=? AND setting_name='gedcomid' AND setting_value=?" 131 )->execute(array($gid2, $WT_TREE->getTreeId(), $gid1)); 132 133 // Merge hit counters 134 $hits = Database::prepare( 135 "SELECT page_name, SUM(page_count)" . 136 " FROM `##hit_counter`" . 137 " WHERE gedcom_id=? AND page_parameter IN (?, ?)" . 138 " GROUP BY page_name" 139 )->execute(array($WT_TREE->getTreeId(), $gid1, $gid2))->fetchAssoc(); 140 141 foreach ($hits as $page_name => $page_count) { 142 Database::prepare( 143 "UPDATE `##hit_counter` SET page_count=?" . 144 " WHERE gedcom_id=? AND page_name=? AND page_parameter=?" 145 )->execute(array($page_count, $WT_TREE->getTreeId(), $page_name, $gid1)); 146 } 147 Database::prepare( 148 "DELETE FROM `##hit_counter`" . 149 " WHERE gedcom_id=? AND page_parameter=?" 150 )->execute(array($WT_TREE->getTreeId(), $gid2)); 151 152 $gedcom = "0 @" . $rec1->getXref() . "@ " . $rec1::RECORD_TYPE; 153 foreach ($facts as $fact_id => $fact) { 154 $gedcom .= "\n" . $fact->getGedcom(); 155 } 156 foreach ($facts1 as $fact_id => $fact) { 157 if (in_array($fact_id, $keep1)) { 158 $gedcom .= "\n" . $fact->getGedcom(); 159 } 160 } 161 foreach ($facts2 as $fact_id => $fact) { 162 if (in_array($fact_id, $keep2)) { 163 $gedcom .= "\n" . $fact->getGedcom(); 164 } 165 } 166 167 $rec1->updateRecord($gedcom, true); 168 $rec2->deleteRecord(); 169 FunctionsDb::updateFavorites($gid2, $gid1, $WT_TREE); 170 FlashMessages::addMessage(I18N::translate( 171 /* I18N: Records are individuals, sources, etc. */ 172 'The records “%1$s” and “%2$s” have been merged.', 173 '<a class="alert-link" href="' . $rec1->getHtmlUrl() . '">' . $rec1->getFullName() . '</a>', 174 $record2_name 175 ), 'success'); 176 177 header('Location: ' . WT_BASE_URL . Filter::post('url', 'admin_trees_duplicates\.php', WT_SCRIPT_NAME)); 178 179 return; 180} 181 182$controller->pageHeader(); 183 184?> 185<ol class="breadcrumb small"> 186 <li><a href="admin.php"><?php echo I18N::translate('Control panel'); ?></a></li> 187 <li><a href="admin_trees_manage.php"><?php echo I18N::translate('Manage family trees'); ?></a></li> 188 <li class="active"><?php echo $controller->getPageTitle(); ?></li> 189</ol> 190<h1><?php echo $controller->getPageTitle(); ?></h1> 191 192<?php if ($rec1 && $rec2 && $rec1->getXref() !== $rec2->getXref() && $rec1::RECORD_TYPE === $rec2::RECORD_TYPE): ?> 193 194<form method="post"> 195 <input type="hidden" name="action" value="merge"> 196 <input type="hidden" name="ged" value="<?php echo $WT_TREE->getNameHtml(); ?>"> 197 <input type="hidden" name="url" value="<?php echo Filter::get('url', 'admin_trees_duplicates\.php'); ?>"> 198 <?php echo Filter::getCsrf(); ?> 199 <p> 200 <?php echo I18N::translate('Select the facts and events to keep from both records.'); ?> 201 </p> 202 <div class="panel panel-default"> 203 <div class="panel-heading"> 204 <h2 class="panel-title"> 205 <?php echo I18N::translate('The following facts and events were found in both records.'); ?> 206 </h2> 207 </div> 208 <div class="panel-body"> 209 <?php if ($facts): ?> 210 <table class="table table-bordered table-condensed"> 211 <thead> 212 <tr> 213 <th> 214 <?php echo I18N::translate('Select'); ?> 215 </th> 216 <th> 217 <?php echo I18N::translate('Details'); ?> 218 </th> 219 </tr> 220 </thead> 221 <tbody> 222 <?php foreach ($facts as $fact_id => $fact): ?> 223 <tr> 224 <td> 225 <input type="checkbox" name="keep1[]" value="<?php echo $fact->getFactId(); ?>" checked> 226 </td> 227 <td> 228 <div class="gedcom-data" dir="ltr"><?php echo Filter::escapeHtml($fact->getGedcom()); ?></div> 229 <?php if ($fact->getTarget()): ?> 230 <a href="<?php echo $fact->getTarget()->getHtmlUrl(); ?>"> 231 <?php echo $fact->getTarget()->getFullName(); ?> 232 </a> 233 <?php endif; ?> 234 </td> 235 </tr> 236 <?php endforeach; ?> 237 </tbody> 238 </table> 239 <?php else: ?> 240 <p> 241 <?php echo I18N::translate('No matching facts found'); ?> 242 </p> 243 <?php endif; ?> 244 </div> 245 </div> 246 247 <div class="row"> 248 <div class="col-sm-6"> 249 <div class="panel panel-default"> 250 <div class="panel-heading"> 251 <h2 class="panel-title"> 252 <?php echo /* I18N: the name of an individual, source, etc. */ I18N::translate('The following facts and events were only found in the record of %s.', '<a href="' . $rec1->getHtmlUrl() . '">' . $rec1->getFullName()) . '</a>'; ?> 253 </h2> 254 </div> 255 <div class="panel-body"> 256 <?php if ($facts1): ?> 257 <table class="table table-bordered table-condensed"> 258 <thead> 259 <tr> 260 <th> 261 <?php echo I18N::translate('Select'); ?> 262 </th> 263 <th> 264 <?php echo I18N::translate('Details'); ?> 265 </th> 266 </tr> 267 </thead> 268 <tbody> 269 <?php foreach ($facts1 as $fact_id => $fact): ?> 270 <tr> 271 <td> 272 <input type="checkbox" name="keep1[]" value="<?php echo $fact->getFactId(); ?>" checked> 273 </td> 274 <td> 275 <div class="gedcom-data" dir="ltr"><?php echo Filter::escapeHtml($fact->getGedcom()); ?></div> 276 <?php if ($fact->getTarget()): ?> 277 <a href="<?php echo $fact->getTarget()->getHtmlUrl(); ?>"> 278 <?php echo $fact->getTarget()->getFullName(); ?> 279 </a> 280 <?php endif; ?> 281 </td> 282 </tr> 283 <?php endforeach; ?> 284 </tbody> 285 </table> 286 <?php else: ?> 287 <p> 288 <?php echo I18N::translate('No matching facts found'); ?> 289 </p> 290 <?php endif; ?> 291 </div> 292 </div> 293 </div> 294 <div class="col-sm-6"> 295 <div class="panel panel-default"> 296 <div class="panel-heading"> 297 <h2 class="panel-title"> 298 <?php echo /* I18N: the name of an individual, source, etc. */ I18N::translate('The following facts and events were only found in the record of %s.', '<a href="' . $rec2->getHtmlUrl() . '">' . $rec2->getFullName()) . '</a>'; ?> 299 </h2> 300 </div> 301 <div class="panel-body"> 302 <?php if ($facts2): ?> 303 <table class="table table-bordered table-condensed"> 304 <thead> 305 <tr> 306 <th> 307 <?php echo I18N::translate('Select'); ?> 308 </th> 309 <th> 310 <?php echo I18N::translate('Details'); ?> 311 </th> 312 </tr> 313 </thead> 314 <tbody> 315 <?php foreach ($facts2 as $fact_id => $fact): ?> 316 <tr> 317 <td> 318 <input type="checkbox" name="keep2[]" value="<?php echo $fact->getFactId(); ?>" checked> 319 </td> 320 <td> 321 <div class="gedcom-data" dir="ltr"><?php echo Filter::escapeHtml($fact->getGedcom()); ?></div> 322 <?php if ($fact->getTarget()): ?> 323 <a href="<?php echo $fact->getTarget()->getHtmlUrl(); ?>"> 324 <?php echo $fact->getTarget()->getFullName(); ?> 325 </a> 326 <?php endif; ?> 327 </td> 328 </tr> 329 <?php endforeach; ?> 330 </tbody> 331 </table> 332 <?php else: ?> 333 <p> 334 <?php echo I18N::translate('No matching facts found'); ?> 335 </p> 336 <?php endif; ?> 337 </div> 338 </div> 339 </div> 340 </div> 341 342 <button type="submit" class="btn btn-primary"> 343 <i class="fa fa-check"></i> 344 <?php echo I18N::translate('save'); ?> 345 </button> 346</form> 347 348<?php else: ?> 349 350<form class="form form-horizontal"> 351 <input type="hidden" name="ged" value="<?php echo $WT_TREE->getNameHtml(); ?>"> 352 <p><?php echo /* I18N: Records are indviduals, sources, etc. */ I18N::translate('Select two records to merge.'); ?></p> 353 354 <div class="form-group"> 355 <div class="control-label col-sm-3"> 356 <label for="gid1"> 357 <?php echo /* I18N: Record is an indvidual, source, etc. */ I18N::translate('First record'); ?> 358 </label> 359 </div> 360 <div class="col-sm-9"> 361 <input data-autocomplete-type="IFSRO" type="text" name="gid1" id="gid1" maxlength="20" value="<?php echo $gid1; ?>"> 362 <?php echo FunctionsPrint::printFindIndividualLink('gid1'); ?> 363 <?php echo FunctionsPrint::printFindFamilyLink('gid1'); ?> 364 <?php echo FunctionsPrint::printFindSourceLink('gid1'); ?> 365 <?php echo FunctionsPrint::printFindRepositoryLink('gid1'); ?> 366 <?php echo FunctionsPrint::printFindMediaLink('gid1'); ?> 367 <?php echo FunctionsPrint::printFindNoteLink('gid1'); ?> 368 </div> 369 </div> 370 371 <div class="form-group"> 372 <div class="control-label col-sm-3"> 373 <label for="gid2"> 374 <?php echo /* I18N: Record is an indvidual, source, etc. */ I18N::translate('Second record'); ?> 375 </label> 376 </div> 377 <div class="col-sm-9"> 378 <input data-autocomplete-type="IFSRO" type="text" name="gid2" id="gid2" maxlength="20" value="<?php echo $gid2; ?>" > 379 <?php echo FunctionsPrint::printFindIndividualLink('gid2'); ?> 380 <?php echo FunctionsPrint::printFindFamilyLink('gid2'); ?> 381 <?php echo FunctionsPrint::printFindSourceLink('gid2'); ?> 382 <?php echo FunctionsPrint::printFindRepositoryLink('gid2'); ?> 383 <?php echo FunctionsPrint::printFindMediaLink('gid2'); ?> 384 <?php echo FunctionsPrint::printFindNoteLink('gid2'); ?> 385 </div> 386 </div> 387 388 <div class="form-group"> 389 <div class="col-sm-offset-3 col-sm-9"> 390 <button type="submit" class="btn btn-primary"> 391 <?php echo I18N::translate('continue'); ?> 392 </button> 393 </div> 394 </div> 395 396</form> 397 398<?php endif; ?> 399