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